about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules9
-rw-r--r--.idea/.idea.LibMatrix/.idea/vcs.xml1
m---------ArcaneLibs0
m---------ExampleBots/LibMatrix.ExampleBot0
m---------ExampleBots/ModerationBot0
m---------ExampleBots/PluralContactBotPoC0
-rw-r--r--LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/EventContent.cs22
-rw-r--r--LibMatrix.EventTypes/Interop/Draupnir/DraupnirProtectedRoomsData.cs11
-rw-r--r--LibMatrix.EventTypes/LibMatrix.EventTypes.csproj14
-rw-r--r--LibMatrix.EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/IgnoredUserListEventContent.cs31
-rw-r--r--LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs29
-rw-r--r--LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs72
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs21
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs4
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs10
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs11
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs14
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs18
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs8
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTombstoneEventContent.cs14
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/Space/SpaceChildEventContent.cs2
-rw-r--r--LibMatrix.EventTypes/Spec/State/Space/SpaceParentEventContent.cs2
-rw-r--r--LibMatrix.Federation/AuthenticatedFederationClient.cs20
-rw-r--r--LibMatrix.Federation/Extensions/Ed25519Extensions.cs8
-rw-r--r--LibMatrix.Federation/LibMatrix.Federation.csproj19
-rw-r--r--LibMatrix.Federation/Utilities/JsonSigning.cs108
-rw-r--r--LibMatrix.Federation/Utilities/UnpaddedBase64.cs17
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs71
-rw-r--r--LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj13
-rw-r--r--LibMatrix.sln424
-rw-r--r--LibMatrix/Extensions/CanonicalJsonSerializer.cs96
-rw-r--r--LibMatrix/Extensions/EnumerableExtensions.cs85
-rw-r--r--LibMatrix/Extensions/JsonElementExtensions.cs1
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Multi.cs11
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs106
-rw-r--r--LibMatrix/Extensions/UnicodeJsonEncoder.cs173
-rw-r--r--LibMatrix/Filters/SyncFilter.cs16
-rw-r--r--LibMatrix/Helpers/HomeserverWeightEstimation.cs58
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs34
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs173
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs160
-rw-r--r--LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs210
-rw-r--r--LibMatrix/Helpers/SyncProcessors/SimpleSyncProcessors.cs47
-rw-r--r--LibMatrix/Helpers/SyncStateResolver.cs677
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs180
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs16
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs3
-rw-r--r--LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs65
-rw-r--r--LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs10
-rw-r--r--LibMatrix/Homeservers/FederationClient.cs70
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs27
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs (renamed from LibMatrix/Filters/LocalRoomQueryFilter.cs)4
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs5
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs23
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs31
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs60
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs28
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs56
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs169
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs31
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs (renamed from LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs)8
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs11
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs11
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs8
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs22
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs250
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs40
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs71
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs487
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs27
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs93
-rw-r--r--LibMatrix/Homeservers/UserInteractiveAuthClient.cs14
-rw-r--r--LibMatrix/Interfaces/Services/IStorageProvider.cs33
-rw-r--r--LibMatrix/LibMatrix.csproj23
-rw-r--r--LibMatrix/LibMatrixException.cs3
-rw-r--r--LibMatrix/MatrixException.cs83
-rw-r--r--LibMatrix/MxcUri.cs43
-rw-r--r--LibMatrix/Responses/CreateRoomRequest.cs21
-rw-r--r--LibMatrix/Responses/DeviceKeysUploadRequest.cs24
-rw-r--r--LibMatrix/Responses/EventIdResponse.cs (renamed from LibMatrix/EventIdResponse.cs)4
-rw-r--r--LibMatrix/Responses/LoginResponse.cs20
-rw-r--r--LibMatrix/Responses/MessagesResponse.cs (renamed from LibMatrix/MessagesResponse.cs)2
-rw-r--r--LibMatrix/Responses/SyncResponse.cs62
-rw-r--r--LibMatrix/Responses/UserDirectoryResponse.cs30
-rw-r--r--LibMatrix/Responses/UserIdAndReason.cs (renamed from LibMatrix/UserIdAndReason.cs)2
-rw-r--r--LibMatrix/Responses/UserProfileResponse.cs15
-rw-r--r--LibMatrix/Responses/WhoAmIResponse.cs (renamed from LibMatrix/WhoAmIResponse.cs)2
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs217
-rw-r--r--LibMatrix/RoomTypes/PolicyRoom.cs51
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs7
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs19
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs85
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs22
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs49
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs91
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs52
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs42
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs38
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs44
-rw-r--r--LibMatrix/StateEvent.cs93
-rw-r--r--LibMatrix/Utilities/CommonSyncFilters.cs2
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs69
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs34
-rw-r--r--Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj23
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs91
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs14
-rw-r--r--Tests/LibMatrix.Tests/Config.cs37
-rw-r--r--Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs2
-rw-r--r--Tests/LibMatrix.Tests/Fixtures/TestFixture.cs29
-rw-r--r--Tests/LibMatrix.Tests/LibMatrix.Tests.csproj16
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs11
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthTests.cs15
-rw-r--r--Tests/LibMatrix.Tests/Tests/CanonicalJsonTests.cs49
-rw-r--r--Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs41
-rw-r--r--Tests/LibMatrix.Tests/Tests/LegacyHomeserverResolverTests.cs36
-rw-r--r--Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs (renamed from Tests/LibMatrix.Tests/Tests/ResolverTest.cs)39
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests.cs248
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs101
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs27
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs42
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs27
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs (renamed from Tests/LibMatrix.Tests/Tests/RoomEventTests.cs)60
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs174
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs316
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs97
-rw-r--r--Tests/LibMatrix.Tests/Tests/TestCleanup.cs74
-rw-r--r--Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj6
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Commands/CmdCommand.cs4
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Commands/DbgAniRainbowTest.cs8
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Commands/HelpCommand.cs4
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Commands/PingCommand.cs19
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/DevTestBot.cs51
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Interfaces/CommandContext.cs11
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/Interfaces/ICommand.cs10
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/PingTestBot.cs125
-rw-r--r--Utilities/LibMatrix.DevTestBot/Bot/StartupTasks/ServerRoomSizeCalulator.cs4
-rw-r--r--Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj9
-rw-r--r--Utilities/LibMatrix.DevTestBot/Program.cs20
-rw-r--r--Utilities/LibMatrix.DevTestBot/appsettings.json4
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/App.razor12
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor16
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor.css77
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor39
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor.css83
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj23
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Pages/CSTJApiComparison.razor134
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Pages/CSTJTest.razor38
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Pages/Home.razor7
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Program.cs11
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/Properties/launchSettings.json31
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/_Imports.razor10
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/css/app.css103
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css7
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css.map1
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/favicon.pngbin0 -> 1148 bytes
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-192.pngbin0 -> 2626 bytes
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-512.pngbin0 -> 6311 bytes
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/index.html36
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/manifest.webmanifest22
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.js4
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.published.js55
-rw-r--r--Utilities/LibMatrix.FederationTest/.gitignore1
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs41
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs18
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/TestController.cs52
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs19
-rw-r--r--Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj18
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs34
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs31
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs10
-rw-r--r--Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs28
-rw-r--r--Utilities/LibMatrix.FederationTest/appsettings.Development.json13
-rw-r--r--Utilities/LibMatrix.FederationTest/appsettings.json9
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/.gitignore (renamed from Tests/LibMatrix.HomeserverEmulator/.gitignore)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs138
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs154
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs18
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs34
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs)34
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs)19
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs)6
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs)19
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs)2
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs)1
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs)68
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs)3
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs)60
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs)4
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs)4
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs)4
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs)5
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs)7
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs)64
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj1034
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http (renamed from Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Program.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Program.cs)9
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Properties/launchSettings.json (renamed from Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs)18
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/MediaStore.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs)16
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs)6
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/RoomStore.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs)71
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/TokenService.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs (renamed from Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs)49
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/appsettings.Development.json (renamed from Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json)0
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/appsettings.json (renamed from Tests/LibMatrix.HomeserverEmulator/appsettings.json)0
-rw-r--r--Utilities/LibMatrix.JsonSerializerContextGenerator/LibMatrix.JsonSerializerContextGenerator.csproj2
-rw-r--r--Utilities/LibMatrix.JsonSerializerContextGenerator/Program.cs2
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcher.cs (renamed from Tests/TestDataGenerator/Bot/DataFetcher.cs)0
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcherConfiguration.cs (renamed from Tests/TestDataGenerator/Bot/DataFetcherConfiguration.cs)0
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj (renamed from Tests/TestDataGenerator/TestDataGenerator.csproj)6
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/Program.cs (renamed from Tests/TestDataGenerator/Program.cs)1
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/Properties/launchSettings.json (renamed from Tests/TestDataGenerator/Properties/launchSettings.json)0
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/appsettings.Development.json (renamed from Tests/TestDataGenerator/appsettings.Development.json)0
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/appsettings.json (renamed from Tests/TestDataGenerator/appsettings.json)0
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs (renamed from Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs)44
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs68
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/BotServiceInstaller.cs98
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Commands/AliassesCommand.cs1
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs5
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs24
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Configuration/CommandListenerConfiguration.cs24
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Configuration/InviteListenerConfiguration.cs20
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Configuration/LibMatrixBotConfiguration.cs17
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs1
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs4
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Interfaces/IRoomInviteHandler.cs5
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Interfaces/RoomInviteContext.cs71
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj8
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs13
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs167
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs103
243 files changed, 9349 insertions, 1949 deletions
diff --git a/.gitmodules b/.gitmodules

index 838d197..126aadf 100644 --- a/.gitmodules +++ b/.gitmodules
@@ -1,12 +1,3 @@ [submodule "ArcaneLibs"] path = ArcaneLibs url = https://github.com/TheArcaneBrony/ArcaneLibs.git -[submodule "ExampleBots/LibMatrix.ExampleBot"] - path = ExampleBots/LibMatrix.ExampleBot - url = https://cgit.rory.gay/matrix/bots/LibMatrix.ExampleBot.git -[submodule "ExampleBots/ModerationBot"] - path = ExampleBots/ModerationBot - url = https://cgit.rory.gay/matrix/bots/ModerationBot.git -[submodule "ExampleBots/PluralContactBotPoC"] - path = ExampleBots/PluralContactBotPoC - url = https://cgit.rory.gay/matrix/bots/PluralContactBotPoC.git diff --git a/.idea/.idea.LibMatrix/.idea/vcs.xml b/.idea/.idea.LibMatrix/.idea/vcs.xml
index 35eb1dd..63ea78b 100644 --- a/.idea/.idea.LibMatrix/.idea/vcs.xml +++ b/.idea/.idea.LibMatrix/.idea/vcs.xml
@@ -2,5 +2,6 @@ <project version="4"> <component name="VcsDirectoryMappings"> <mapping directory="" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/ArcaneLibs" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/ArcaneLibs b/ArcaneLibs -Subproject 3fb65c126b591ca05e76394d4c40f74cbb70de4 +Subproject 7cbf235cceb82dc711622314c49edfc7e2614dc diff --git a/ExampleBots/LibMatrix.ExampleBot b/ExampleBots/LibMatrix.ExampleBot deleted file mode 160000 -Subproject 4f9311ed047c6e6a62e3d49be330a83a829b72f diff --git a/ExampleBots/ModerationBot b/ExampleBots/ModerationBot deleted file mode 160000 -Subproject e4fbb948bce90fdfe757c70081c6dbec84b7024 diff --git a/ExampleBots/PluralContactBotPoC b/ExampleBots/PluralContactBotPoC deleted file mode 160000 -Subproject 4072e5922b671c9d93dbf9727123046054ca6da diff --git a/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs b/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs
index a31cbbb..a1ebd79 100644 --- a/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs +++ b/LibMatrix.EventTypes/Common/MjolnirShortcodeEventContent.cs
@@ -3,7 +3,7 @@ using System.Text.Json.Serialization; namespace LibMatrix.EventTypes.Common; [MatrixEvent(EventName = EventId)] -public class MjolnirShortcodeEventContent : TimelineEventContent { +public class MjolnirShortcodeEventContent : EventContent { public const string EventId = "org.matrix.mjolnir.shortcode"; [JsonPropertyName("shortcode")] diff --git a/LibMatrix.EventTypes/EventContent.cs b/LibMatrix.EventTypes/EventContent.cs
index c582cf2..07f56e2 100644 --- a/LibMatrix.EventTypes/EventContent.cs +++ b/LibMatrix.EventTypes/EventContent.cs
@@ -1,10 +1,23 @@ +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace LibMatrix.EventTypes; -public abstract class EventContent; +public abstract class EventContent { + [JsonExtensionData] + public Dictionary<string, object>? AdditionalData { get; set; } = []; + + public static List<string> GetMatchingEventTypes<T>() where T : EventContent { + var type = typeof(T); + var eventTypes = new List<string>(); + foreach (var attr in type.GetCustomAttributes<MatrixEventAttribute>(true)) { + eventTypes.Add(attr.EventName); + } + return eventTypes; + } +} public class UnknownEventContent : TimelineEventContent; @@ -37,6 +50,13 @@ public abstract class TimelineEventContent : EventContent { [JsonPropertyName("rel_type")] public string? RelationType { get; set; } + // used for reactions + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonExtensionData] + public Dictionary<string, object>? AdditionalData { get; set; } = []; + public class EventInReplyTo { [JsonPropertyName("event_id")] public string? EventId { get; set; } diff --git a/LibMatrix.EventTypes/Interop/Draupnir/DraupnirProtectedRoomsData.cs b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirProtectedRoomsData.cs new file mode 100644
index 0000000..1917239 --- /dev/null +++ b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirProtectedRoomsData.cs
@@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.EventTypes.Interop.Draupnir; + +[MatrixEvent(EventName = EventId)] +public class DraupnirProtectedRoomsData : EventContent { + public const string EventId = "org.matrix.mjolnir.protected_rooms"; + + [JsonPropertyName("rooms")] + public List<string> Rooms { get; set; } = new(); +} \ No newline at end of file diff --git a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj
index 4276003..0924aba 100644 --- a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj +++ b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj
@@ -1,22 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> - <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/> - <!-- This is dangerous, but eases development since locking the version will drift out of sync without noticing, - which causes build errors due to missing functions. - Using the NuGet version in development is annoying due to delays between pushing and being able to consume. - If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git - instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. --> - <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/> + <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250419-174711" Condition="'$(Configuration)' == 'Release'" /> + <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/> </ItemGroup> - <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild"> - <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/> - </Target> </Project> diff --git a/LibMatrix.EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs b/LibMatrix.EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
index 494936d..a7d431c 100644 --- a/LibMatrix.EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs +++ b/LibMatrix.EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.Ephemeral; [MatrixEvent(EventName = EventId)] public class RoomTypingEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/IgnoredUserListEventContent.cs b/LibMatrix.EventTypes/Spec/IgnoredUserListEventContent.cs new file mode 100644
index 0000000..59e17c9 --- /dev/null +++ b/LibMatrix.EventTypes/Spec/IgnoredUserListEventContent.cs
@@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace LibMatrix.EventTypes.Spec; + +[MatrixEvent(EventName = EventId)] +public class IgnoredUserListEventContent : EventContent { + public const string EventId = "m.ignored_user_list"; + + [JsonPropertyName("ignored_users")] + public Dictionary<string, IgnoredUserContent> IgnoredUsers { get; set; } = new(); + + // Dummy type to provide easy access to the by-spec empty content + public class IgnoredUserContent { + [JsonExtensionData] + public Dictionary<string, object>? AdditionalData { get; set; } = []; + + public T? GetAdditionalData<T>(string key) where T : class { + if (AdditionalData == null || !AdditionalData.TryGetValue(key, out var value)) + return null; + + if (value is T tValue) + return tValue; + if (value is JsonElement jsonElement) + return jsonElement.Deserialize<T>(); + + throw new InvalidCastException($"Value for key '{key}' ({value.GetType()}) cannot be cast to type '{typeof(T)}'. Cannot continue."); + } + } +} \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
index ae893f8..d1cf8be 100644 --- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs +++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -29,13 +29,34 @@ public class RoomMessageEventContent : TimelineEventContent { [JsonPropertyName("url")] public string? Url { get; set; } + [JsonPropertyName("filename")] public string? FileName { get; set; } [JsonPropertyName("info")] public FileInfoStruct? FileInfo { get; set; } - + [JsonIgnore] - public string BodyWithoutReplyFallback => Body.Split('\n').SkipWhile(x => x.StartsWith(">")).SkipWhile(x=>x.Trim().Length == 0).Aggregate((x, y) => $"{x}\n{y}"); + public string BodyWithoutReplyFallback { + get { + var parts = Body + .Split('\n') + .SkipWhile(x => x.StartsWith(">")) + .SkipWhile(x => x.Trim().Length == 0) + .ToList(); + return parts.Count > 0 ? parts.Aggregate((x, y) => $"{x}\n{y}") : Body; + } + } + + [JsonPropertyName("m.mentions")] + public MentionsStruct? Mentions { get; set; } + + public class MentionsStruct { + [JsonPropertyName("user_ids")] + public List<string>? Users { get; set; } + + [JsonPropertyName("room")] + public bool? Room { get; set; } + } public class FileInfoStruct { [JsonPropertyName("mimetype")] @@ -46,10 +67,10 @@ public class RoomMessageEventContent : TimelineEventContent { [JsonPropertyName("thumbnail_url")] public string? ThumbnailUrl { get; set; } - + [JsonPropertyName("w")] public int? Width { get; set; } - + [JsonPropertyName("h")] public int? Height { get; set; } } diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 89e2fdb..d75b19f 100644 --- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -1,5 +1,9 @@ +using System.Diagnostics; +using System.Security.Cryptography; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using ArcaneLibs.Attributes; +using ArcaneLibs.Extensions; namespace LibMatrix.EventTypes.Spec.State.Policy; @@ -28,9 +32,9 @@ public class RoomPolicyRuleEventContent : PolicyRuleEventContent { public const string EventId = "m.policy.rule.room"; } +[DebuggerDisplay("""{GetType().Name.Replace("PolicyRuleEventContent", ""),nq} policy matching {Entity}, Reason: {Reason}""")] public abstract class PolicyRuleEventContent : EventContent { - public PolicyRuleEventContent() => Console.WriteLine($"init policy {GetType().Name}"); - private string? _reason; + // public PolicyRuleEventContent() => Console.WriteLine($"init policy {GetType().Name}"); /// <summary> /// Entity this ban applies to, can use * and ? as globs. @@ -40,24 +44,14 @@ public abstract class PolicyRuleEventContent : EventContent { [FriendlyName(Name = "Entity")] public string? Entity { get; set; } - private bool init; + // private bool init; /// <summary> /// Reason this user is banned /// </summary> [JsonPropertyName("reason")] [FriendlyName(Name = "Reason")] - public virtual string? Reason { - get => - // Console.WriteLine($"Read policy reason: {_reason}"); - _reason; - set => - // Console.WriteLine($"Set policy reason: {value}"); - // if(init) - // Console.WriteLine(string.Join('\n', Environment.StackTrace.Split('\n')[..5])); - // init = true; - _reason = value; - } + public string? Reason { get; set; } /// <summary> /// Suggested action to take @@ -87,6 +81,49 @@ public abstract class PolicyRuleEventContent : EventContent { Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); } } + + [JsonPropertyName("org.matrix.msc4205.hashes")] + [TableHide] + public PolicyHash? Hashes { get; set; } + + public string GetDraupnir2StateKey() => Convert.ToBase64String(SHA256.HashData($"{Entity}{Recommendation}".AsBytes().ToArray())); + + public Regex? GetEntityRegex() => Entity is null ? null : new(Entity.Replace(".", "\\.").Replace("*", ".*").Replace("?", ".")); + + public bool IsGlobRule() => + !string.IsNullOrWhiteSpace(Entity) + && (Entity.Contains('*') || Entity.Contains('?')); + + public bool EntityMatches(string entity) { + if (string.IsNullOrWhiteSpace(entity)) return false; + + if (!string.IsNullOrWhiteSpace(Entity)) { + // Check if entity is equal regardless of glob check + var match = Entity == entity + || (IsGlobRule() && GetEntityRegex()!.IsMatch(entity)); + if (match) return match; + } + + if (Hashes is not null) { + if (!string.IsNullOrWhiteSpace(Hashes.Sha256)) { + var hash = SHA256.HashData(entity.AsBytes().ToArray()); + var match = Convert.ToBase64String(hash) == Hashes.Sha256; + if (match) return match; + } + } + + return false; + } + + public string? GetNormalizedRecommendation() { + if (Recommendation is "m.ban" or "org.matrix.mjolnir.ban") + return PolicyRecommendationTypes.Ban; + + if (Recommendation is "m.takedown" or "org.matrix.msc4204.takedown") + return "m.takedown"; + + return Recommendation; + } } public static class PolicyRecommendationTypes { @@ -99,6 +136,13 @@ public static class PolicyRecommendationTypes { /// Mute this user /// </summary> public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending + + public static string Takedown = "m.takedown"; //unstable prefix: org.matrix.msc4204.takedown +} + +public class PolicyHash { + [JsonPropertyName("sha256")] + public string? Sha256 { get; set; } } // public class PolicySchemaDefinition { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
index 93f13ac..ee3234c 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomCanonicalAliasEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
index c619d0e..37b831a 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
@@ -1,8 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Deserialization, public API")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Deserialization, public API")] public class RoomCreateEventContent : EventContent { public const string EventId = "m.room.create"; @@ -15,17 +18,17 @@ public class RoomCreateEventContent : EventContent { [JsonPropertyName("m.federate")] public bool? Federate { get; set; } - [JsonPropertyName("predecessor")] - public RoomCreatePredecessor? Predecessor { get; set; } + // [JsonPropertyName("predecessor")] + // public RoomCreatePredecessor? Predecessor { get; set; } [JsonPropertyName("type")] public string? Type { get; set; } +} - public class RoomCreatePredecessor { - [JsonPropertyName("room_id")] - public string? RoomId { get; set; } +public class RoomCreatePredecessor { + [JsonPropertyName("room_id")] + public string? RoomId { get; set; } - [JsonPropertyName("event_id")] - public string? EventId { get; set; } - } + [JsonPropertyName("event_id")] + public string? EventId { get; set; } } \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs
index b49abfa..16209f0 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomEncryptionEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs
index a7811bf..1ba5a3f 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs
@@ -1,13 +1,13 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomGuestAccessEventContent : EventContent { public const string EventId = "m.room.guest_access"; [JsonPropertyName("guest_access")] - public string GuestAccess { get; set; } + public required string GuestAccess { get; set; } [JsonIgnore] public bool IsGuestAccessEnabled { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
index 7676dad..8edf4a7 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs
@@ -1,11 +1,17 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomHistoryVisibilityEventContent : EventContent { public const string EventId = "m.room.history_visibility"; [JsonPropertyName("history_visibility")] - public string HistoryVisibility { get; set; } + public required string HistoryVisibility { get; set; } + + public static class HistoryVisibilityTypes { + public const string WorldReadable = "world_readable"; + public const string Invited = "invited"; + public const string Shared = "shared"; + } } \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs
index 349c8a7..03d994d 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomJoinRulesEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs
index b2d5596..b034425 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomMemberEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
index 3ea5730..415c675 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomNameEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs
index b4474e9..7eee605 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] public class RoomPinnedEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs new file mode 100644
index 0000000..80e254f --- /dev/null +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
@@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; + +[MatrixEvent(EventName = EventId)] +public class RoomPolicyServerEventContent : EventContent { + public const string EventId = "org.matrix.msc4284.policy"; + + [JsonPropertyName("via")] + public string? Via { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index eb156b3..22fa3b7 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] @@ -20,7 +20,7 @@ public class RoomPowerLevelEventContent : EventContent { public long? Kick { get; set; } = 50; [JsonPropertyName("notifications")] - public NotificationsPL? NotificationsPl { get; set; } // = null!; + public NotificationsPowerLevels? NotificationsPl { get; set; } [JsonPropertyName("redact")] public long? Redact { get; set; } = 50; @@ -29,10 +29,10 @@ public class RoomPowerLevelEventContent : EventContent { public long? StateDefault { get; set; } = 50; [JsonPropertyName("events")] - public Dictionary<string, long>? Events { get; set; } // = null!; + public Dictionary<string, long>? Events { get; set; } [JsonPropertyName("users")] - public Dictionary<string, long>? Users { get; set; } // = null!; + public Dictionary<string, long>? Users { get; set; } [JsonPropertyName("users_default")] public long? UsersDefault { get; set; } = 0; @@ -42,19 +42,19 @@ public class RoomPowerLevelEventContent : EventContent { [JsonPropertyName("historical")] public long Historical { get; set; } // = 50; - public class NotificationsPL { + public class NotificationsPowerLevels { [JsonPropertyName("room")] public long Room { get; set; } = 50; } public bool IsUserAdmin(string userId) { ArgumentNullException.ThrowIfNull(userId); - return Users.TryGetValue(userId, out var level) && level >= Events.Max(x => x.Value); + return GetUserPowerLevel(userId) >= Events?.Max(x => x.Value); } public bool UserHasTimelinePermission(string userId, string eventType) { ArgumentNullException.ThrowIfNull(userId); - return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0); + return GetUserPowerLevel(userId) >= Events?.GetValueOrDefault(eventType, EventsDefault ?? 0); } public bool UserHasStatePermission(string userId, string eventType, bool log = false) { diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs new file mode 100644
index 0000000..055f22d --- /dev/null +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomRedactionEventContent.cs
@@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; + +[MatrixEvent(EventName = EventId)] +public class RoomRedactionEventContent : EventContent { + public const string EventId = "m.room.redaction"; + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// <summary> + /// Required in room version 11 + /// </summary> + [JsonPropertyName("redacts")] + public string? Redacts { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs
index be83e37..c492250 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs
@@ -1,16 +1,16 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] -public class RoomServerACLEventContent : EventContent { +public class RoomServerAclEventContent : EventContent { public const string EventId = "m.room.server_acl"; [JsonPropertyName("allow")] - public List<string>? Allow { get; set; } // = null!; + public List<string>? Allow { get; set; } [JsonPropertyName("deny")] - public List<string>? Deny { get; set; } // = null!; + public List<string>? Deny { get; set; } [JsonPropertyName("allow_ip_literals")] public bool AllowIpLiterals { get; set; } // = false; diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTombstoneEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTombstoneEventContent.cs new file mode 100644
index 0000000..2c45c41 --- /dev/null +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTombstoneEventContent.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; + +[MatrixEvent(EventName = EventId)] +public class RoomTombstoneEventContent : EventContent { + public const string EventId = "m.room.tombstone"; + + [JsonPropertyName("body")] + public string Body { get; set; } + + [JsonPropertyName("replacement_room")] + public string ReplacementRoom { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs
index 92fa75d..065c976 100644 --- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.RoomInfo; [MatrixEvent(EventName = EventId)] [MatrixEvent(EventName = "org.matrix.msc3765.topic", Legacy = true)] diff --git a/LibMatrix.EventTypes/Spec/State/Space/SpaceChildEventContent.cs b/LibMatrix.EventTypes/Spec/State/Space/SpaceChildEventContent.cs
index d233be4..cd0f1f5 100644 --- a/LibMatrix.EventTypes/Spec/State/Space/SpaceChildEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/Space/SpaceChildEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.Space; [MatrixEvent(EventName = EventId)] public class SpaceChildEventContent : EventContent { diff --git a/LibMatrix.EventTypes/Spec/State/Space/SpaceParentEventContent.cs b/LibMatrix.EventTypes/Spec/State/Space/SpaceParentEventContent.cs
index 2ab79a4..f50797e 100644 --- a/LibMatrix.EventTypes/Spec/State/Space/SpaceParentEventContent.cs +++ b/LibMatrix.EventTypes/Spec/State/Space/SpaceParentEventContent.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.EventTypes.Spec.State; +namespace LibMatrix.EventTypes.Spec.State.Space; [MatrixEvent(EventName = EventId)] public class SpaceParentEventContent : EventContent { diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs new file mode 100644
index 0000000..6f8d44b --- /dev/null +++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -0,0 +1,20 @@ +using LibMatrix.Homeservers; + +namespace LibMatrix.Federation; + +public class AuthenticatedFederationClient : FederationClient { + + public class AuthenticatedFederationConfiguration { + + } + public AuthenticatedFederationClient(string federationEndpoint, AuthenticatedFederationConfiguration config, string? proxy = null) : base(federationEndpoint, proxy) + { + + } + + // public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { + // var response = await GetAsync<UserDeviceListResponse>($"/_matrix/federation/v1/user/devices/{userId}", accessToken); + // return response; + // } + +} \ No newline at end of file diff --git a/LibMatrix.Federation/Extensions/Ed25519Extensions.cs b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs new file mode 100644
index 0000000..69baf58 --- /dev/null +++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
@@ -0,0 +1,8 @@ +using LibMatrix.FederationTest.Utilities; +using Org.BouncyCastle.Crypto.Parameters; + +namespace LibMatrix.Federation.Extensions; + +public static class Ed25519Extensions { + public static string ToUnpaddedBase64(this Ed25519PublicKeyParameters key) => UnpaddedBase64.Encode(key.GetEncoded()); +} \ No newline at end of file diff --git a/LibMatrix.Federation/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj new file mode 100644
index 0000000..78086bb --- /dev/null +++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" /> + <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0-preview.5.25277.114" /> + </ItemGroup> + +</Project> diff --git a/LibMatrix.Federation/Utilities/JsonSigning.cs b/LibMatrix.Federation/Utilities/JsonSigning.cs new file mode 100644
index 0000000..c727cde --- /dev/null +++ b/LibMatrix.Federation/Utilities/JsonSigning.cs
@@ -0,0 +1,108 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using LibMatrix.Extensions; +using LibMatrix.FederationTest.Utilities; +using LibMatrix.Homeservers; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math.EC.Rfc8032; + +namespace LibMatrix.Federation.Utilities; + +public static class JsonSigning { } + +[JsonConverter(typeof(SignedObjectConverterFactory))] +public class SignedObject<T> { + [JsonPropertyName("signatures")] + public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new(); + + [JsonIgnore] + public Dictionary<string, Dictionary<ServerKeysResponse.VersionedKeyId, string>> VerifyKeysById { + get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (ServerKeysResponse.VersionedKeyId)key.Key, key => key.Value)); + set => Signatures = value.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (string)key.Key, key => key.Value)); + } + + [JsonExtensionData] + public required JsonObject Content { get; set; } + + [JsonIgnore] + public T TypedContent { + get => Content.Deserialize<T>() ?? throw new JsonException("Failed to deserialize TypedContent from Content."); + set => Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value)) ?? new JsonObject(); + } +} + +// Content needs to be merged at toplevel +public class SignedObjectConverter<T> : JsonConverter<SignedObject<T>> { + public override SignedObject<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var jsonObject = JsonSerializer.Deserialize<JsonObject>(ref reader, options); + if (jsonObject == null) { + throw new JsonException("Failed to deserialize SignedObject, JSON object is null."); + } + + var signatures = jsonObject["signatures"] ?? throw new JsonException("Failed to find 'signatures' property in JSON object."); + jsonObject.Remove("signatures"); + + var signedObject = new SignedObject<T> { + Content = jsonObject, + Signatures = signatures.Deserialize<Dictionary<string, Dictionary<string, string>>>() + ?? throw new JsonException("Failed to deserialize 'signatures' property into Dictionary<string, Dictionary<string, string>>.") + }; + + return signedObject; + } + + public override void Write(Utf8JsonWriter writer, SignedObject<T> value, JsonSerializerOptions options) { + var targetObj = value.Content.DeepClone(); + targetObj["signatures"] = value.Signatures.ToJsonNode(); + JsonSerializer.Serialize(writer, targetObj, options); + } +} + +internal class SignedObjectConverterFactory : JsonConverterFactory { + public override bool CanConvert(Type typeToConvert) { + if (!typeToConvert.IsGenericType) return false; + return typeToConvert.GetGenericTypeDefinition() == typeof(SignedObject<>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { + var wrappedType = typeToConvert.GetGenericArguments()[0]; + var converter = (JsonConverter)Activator.CreateInstance(typeof(SignedObjectConverter<>).MakeGenericType(wrappedType))!; + return converter; + } +} + +public static class ObjectExtensions { + public static SignedObject<T> Sign<T>(this SignedObject<T> content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + var signResult = Sign(content.Content, serverName, keyName, key); + var signedObject = new SignedObject<T> { + Signatures = content.Signatures, + Content = signResult.Content + }; + + if (!signedObject.Signatures.ContainsKey(serverName)) + signedObject.Signatures[serverName] = new Dictionary<string, string>(); + + signedObject.Signatures[serverName][keyName] = signResult.Signatures[serverName][keyName]; + return signedObject; + } + + public static SignedObject<T> Sign<T>(this T content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + SignedObject<T> signedObject = new() { + Signatures = [], + Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(content)) ?? new JsonObject(), + }; + + var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(signedObject.Content); + var signature = new byte[Ed25519.SignatureSize]; + key.Sign(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signature, 0); + + if (!signedObject.Signatures.ContainsKey(serverName)) + signedObject.Signatures[serverName] = new Dictionary<string, string>(); + + signedObject.Signatures[serverName][keyName] = UnpaddedBase64.Encode(signature); + return signedObject; + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/Utilities/UnpaddedBase64.cs b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs new file mode 100644
index 0000000..06f84b2 --- /dev/null +++ b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs
@@ -0,0 +1,17 @@ +namespace LibMatrix.FederationTest.Utilities; + +public static class UnpaddedBase64 { + public static string Encode(byte[] data) { + return Convert.ToBase64String(data).TrimEnd('='); + } + + public static byte[] Decode(string base64) { + string paddedBase64 = base64; + switch (paddedBase64.Length % 4) { + case 2: paddedBase64 += "=="; break; + case 3: paddedBase64 += "="; break; + } + + return Convert.FromBase64String(paddedBase64); + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs new file mode 100644
index 0000000..fc402b7 --- /dev/null +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -0,0 +1,71 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using Microsoft.Extensions.Primitives; + +namespace LibMatrix.Federation; + +public class XMatrixAuthorizationScheme { + public class XMatrixAuthorizationHeader { + public const string Scheme = "X-Matrix"; + + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("destination")] + public required string Destination { get; set; } + + [JsonPropertyName("key")] + public required string Key { get; set; } + + [JsonPropertyName("sig")] + public required string Signature { get; set; } + + public static XMatrixAuthorizationHeader FromHeaderValue(AuthenticationHeaderValue header) { + if (header.Scheme != Scheme) + throw new LibMatrixException() { + Error = $"Expected authentication scheme of {Scheme}, got {header.Scheme}", + ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED + }; + + if (string.IsNullOrWhiteSpace(header.Parameter)) + throw new LibMatrixException() { + Error = $"Expected authentication header to have a value.", + ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED + }; + + var headerValues = new StringValues(header.Parameter); + foreach (var value in headerValues) { + Console.WriteLine(headerValues.ToJson()); + } + + return new() { + Destination = "", + Key = "", + Origin = "", + Signature = "" + }; + } + + public string ToHeaderValue() => $"{Scheme} origin=\"{Origin}\", destination=\"{Destination}\", key=\"{Key}\", sig=\"{Signature}\""; + } + + public class XMatrixRequestSignature { + [JsonPropertyName("method")] + public required string Method { get; set; } + + [JsonPropertyName("uri")] + public required string Uri { get; set; } + + [JsonPropertyName("origin")] + public required string OriginServerName { get; set; } + + [JsonPropertyName("destination")] + public required string DestinationServerName { get; set; } + + [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public JsonObject? Content { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj b/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj deleted file mode 100644
index 4df0c6b..0000000 --- a/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj +++ /dev/null
@@ -1,13 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <ImplicitUsings>enable</ImplicitUsings> - <Nullable>enable</Nullable> - </PropertyGroup> - - <ItemGroup> - <Folder Include="Classes\" /> - </ItemGroup> - -</Project> diff --git a/LibMatrix.sln b/LibMatrix.sln
index c068216..d6c1c0f 100644 --- a/LibMatrix.sln +++ b/LibMatrix.sln
@@ -1,114 +1,310 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{840309F0-435B-43A7-8471-8C2BE643889D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A6345ECE-4C5E-400F-9130-886E343BF314}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{32D9616B-91BB-4B43-97C6-2C3840C12EA6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{1B1B2197-61FB-416F-B6C8-845F2E5A0442}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "ExampleBots\ModerationBot\ModerationBot.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFE16D8E-EFC5-49F6-9854-DB001309B3B4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{345934FF-CA81-4A4B-B137-9F198102C65F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDataGenerator", "Tests\TestDataGenerator\TestDataGenerator.csproj", "{0B9B34D1-9362-45A9-9C21-816FD6959110}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{4D9B5227-48DC-4A30-9263-AFB51DC01ABB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{01A126FE-9D50-40F2-817B-E55F4065EA76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{13A797D1-7E13-4789-A167-8628B1641AC0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{CD13665B-B964-4AB0-991B-12F067B16DA3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.Build.0 = Release|Any CPU - {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.Build.0 = Release|Any CPU - {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.Build.0 = Release|Any CPU - {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.Build.0 = Release|Any CPU - {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.Build.0 = Release|Any CPU - {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.Build.0 = Release|Any CPU - {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.Build.0 = Release|Any CPU - {345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.Build.0 = Release|Any CPU - {0B9B34D1-9362-45A9-9C21-816FD6959110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B9B34D1-9362-45A9-9C21-816FD6959110}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B9B34D1-9362-45A9-9C21-816FD6959110}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B9B34D1-9362-45A9-9C21-816FD6959110}.Release|Any CPU.Build.0 = Release|Any CPU - {4D9B5227-48DC-4A30-9263-AFB51DC01ABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4D9B5227-48DC-4A30-9263-AFB51DC01ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4D9B5227-48DC-4A30-9263-AFB51DC01ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4D9B5227-48DC-4A30-9263-AFB51DC01ABB}.Release|Any CPU.Build.0 = Release|Any CPU - {13A797D1-7E13-4789-A167-8628B1641AC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13A797D1-7E13-4789-A167-8628B1641AC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13A797D1-7E13-4789-A167-8628B1641AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13A797D1-7E13-4789-A167-8628B1641AC0}.Release|Any CPU.Build.0 = Release|Any CPU - {CD13665B-B964-4AB0-991B-12F067B16DA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD13665B-B964-4AB0-991B-12F067B16DA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD13665B-B964-4AB0-991B-12F067B16DA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD13665B-B964-4AB0-991B-12F067B16DA3}.Release|Any CPU.Build.0 = Release|Any CPU - {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D} - {8F0A820E-F6AE-45A2-970E-7A3759693919} = {840309F0-435B-43A7-8471-8C2BE643889D} - {35DF9A1A-D988-4225-AFA3-06BB8EDEB559} = {A6345ECE-4C5E-400F-9130-886E343BF314} - {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7} = {A6345ECE-4C5E-400F-9130-886E343BF314} - {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B} = {840309F0-435B-43A7-8471-8C2BE643889D} - {345934FF-CA81-4A4B-B137-9F198102C65F} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} - {0B9B34D1-9362-45A9-9C21-816FD6959110} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} - {4D9B5227-48DC-4A30-9263-AFB51DC01ABB} = {A6345ECE-4C5E-400F-9130-886E343BF314} - {13A797D1-7E13-4789-A167-8628B1641AC0} = {01A126FE-9D50-40F2-817B-E55F4065EA76} - {D44DB78D-9BAD-4AB6-A054-839ECA9D68D2} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{32C0F9AC-AF7D-4476-A269-99ACA000EF9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{93D00F03-02FF-4C2A-8917-6863D6E633D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{51F770AF-C0C6-4247-A358-82DF323F473D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{8A8E8B67-9351-471E-A502-AF7FAE596CB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{567DABFB-611E-4779-9F39-1D4A5B8F0247}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{28AFDFEF-7597-4450-B999-87C22C24DCD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{F80D5395-28E3-4C34-9662-A890A215DDA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{90A38896-993A-49F1-903B-8C989D8C9B3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{6AE504F8-734B-456B-8BEA-DE37639CB3A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{AAAA5609-D8C1-401E-BB5C-724EFE3955FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{7E15D295-B938-409C-98F6-A22ABC7F3005}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{FBC6F613-4E0B-4A90-8854-31887A796EEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{D632374A-70A1-4954-8F2D-C0B511A24426}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{91E41EE3-687B-4CFD-94C4-028D09BACFAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{4D411F2D-A97D-4485-A318-ED98B96B6CF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Utilities\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{7F36D200-FE91-4761-B681-BEE091FB953F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{69CCB780-50CC-4E75-B794-932E44ACA80F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator", "Utilities\LibMatrix.TestDataGenerator\LibMatrix.TestDataGenerator.csproj", "{C087F8AC-B438-4980-9A79-1E16D0CFB67A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Federation", "LibMatrix.Federation\LibMatrix.Federation.csproj", "{0E6CF267-14FD-43D7-81CB-EE020FD1D106}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x64.Build.0 = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Debug|x86.Build.0 = Debug|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|Any CPU.Build.0 = Release|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x64.ActiveCfg = Release|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x64.Build.0 = Release|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x86.ActiveCfg = Release|Any CPU + {93D00F03-02FF-4C2A-8917-6863D6E633D9}.Release|x86.Build.0 = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x64.Build.0 = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Debug|x86.Build.0 = Debug|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|Any CPU.Build.0 = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x64.ActiveCfg = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x64.Build.0 = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x86.ActiveCfg = Release|Any CPU + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB}.Release|x86.Build.0 = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x64.ActiveCfg = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x64.Build.0 = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x86.ActiveCfg = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Debug|x86.Build.0 = Debug|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|Any CPU.Build.0 = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x64.ActiveCfg = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x64.Build.0 = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x86.ActiveCfg = Release|Any CPU + {51F770AF-C0C6-4247-A358-82DF323F473D}.Release|x86.Build.0 = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x64.Build.0 = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Debug|x86.Build.0 = Debug|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|Any CPU.Build.0 = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x64.ActiveCfg = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x64.Build.0 = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x86.ActiveCfg = Release|Any CPU + {8A8E8B67-9351-471E-A502-AF7FAE596CB4}.Release|x86.Build.0 = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x64.Build.0 = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Debug|x86.Build.0 = Debug|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|Any CPU.Build.0 = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x64.ActiveCfg = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x64.Build.0 = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x86.ActiveCfg = Release|Any CPU + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB}.Release|x86.Build.0 = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|Any CPU.Build.0 = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x64.ActiveCfg = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x64.Build.0 = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x86.ActiveCfg = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Debug|x86.Build.0 = Debug|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.ActiveCfg = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|Any CPU.Build.0 = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x64.ActiveCfg = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x64.Build.0 = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x86.ActiveCfg = Release|Any CPU + {567DABFB-611E-4779-9F39-1D4A5B8F0247}.Release|x86.Build.0 = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x64.Build.0 = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Debug|x86.Build.0 = Debug|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|Any CPU.Build.0 = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x64.ActiveCfg = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x64.Build.0 = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x86.ActiveCfg = Release|Any CPU + {28AFDFEF-7597-4450-B999-87C22C24DCD8}.Release|x86.Build.0 = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x64.Build.0 = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Debug|x86.Build.0 = Debug|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|Any CPU.Build.0 = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x64.ActiveCfg = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x64.Build.0 = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x86.ActiveCfg = Release|Any CPU + {F80D5395-28E3-4C34-9662-A890A215DDA2}.Release|x86.Build.0 = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x64.Build.0 = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Debug|x86.Build.0 = Debug|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|Any CPU.Build.0 = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x64.ActiveCfg = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x64.Build.0 = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x86.ActiveCfg = Release|Any CPU + {90A38896-993A-49F1-903B-8C989D8C9B3A}.Release|x86.Build.0 = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x64.Build.0 = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Debug|x86.Build.0 = Debug|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x64.ActiveCfg = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x64.Build.0 = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x86.ActiveCfg = Release|Any CPU + {6AE504F8-734B-456B-8BEA-DE37639CB3A7}.Release|x86.Build.0 = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x64.Build.0 = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Debug|x86.Build.0 = Debug|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|Any CPU.Build.0 = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x64.ActiveCfg = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x64.Build.0 = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x86.ActiveCfg = Release|Any CPU + {7E15D295-B938-409C-98F6-A22ABC7F3005}.Release|x86.Build.0 = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x64.ActiveCfg = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x64.Build.0 = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x86.ActiveCfg = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Debug|x86.Build.0 = Debug|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|Any CPU.Build.0 = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x64.ActiveCfg = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x64.Build.0 = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x86.ActiveCfg = Release|Any CPU + {D632374A-70A1-4954-8F2D-C0B511A24426}.Release|x86.Build.0 = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x64.ActiveCfg = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x64.Build.0 = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Debug|x86.Build.0 = Debug|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|Any CPU.Build.0 = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x64.ActiveCfg = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x64.Build.0 = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x86.ActiveCfg = Release|Any CPU + {91E41EE3-687B-4CFD-94C4-028D09BACFAB}.Release|x86.Build.0 = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x64.Build.0 = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Debug|x86.Build.0 = Debug|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|Any CPU.Build.0 = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x64.ActiveCfg = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x64.Build.0 = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x86.ActiveCfg = Release|Any CPU + {4D411F2D-A97D-4485-A318-ED98B96B6CF6}.Release|x86.Build.0 = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x64.Build.0 = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Debug|x86.Build.0 = Debug|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|Any CPU.Build.0 = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x64.ActiveCfg = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x64.Build.0 = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x86.ActiveCfg = Release|Any CPU + {7F36D200-FE91-4761-B681-BEE091FB953F}.Release|x86.Build.0 = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x64.ActiveCfg = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x64.Build.0 = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x86.ActiveCfg = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Debug|x86.Build.0 = Debug|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|Any CPU.Build.0 = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x64.ActiveCfg = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x64.Build.0 = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x86.ActiveCfg = Release|Any CPU + {69CCB780-50CC-4E75-B794-932E44ACA80F}.Release|x86.Build.0 = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x64.Build.0 = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Debug|x86.Build.0 = Debug|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|Any CPU.Build.0 = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x64.ActiveCfg = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x64.Build.0 = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x86.ActiveCfg = Release|Any CPU + {C087F8AC-B438-4980-9A79-1E16D0CFB67A}.Release|x86.Build.0 = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x64.Build.0 = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Debug|x86.Build.0 = Debug|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|Any CPU.Build.0 = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x64.ActiveCfg = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x64.Build.0 = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x86.ActiveCfg = Release|Any CPU + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A}.Release|x86.Build.0 = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x64.ActiveCfg = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x64.Build.0 = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x86.ActiveCfg = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Debug|x86.Build.0 = Debug|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|Any CPU.Build.0 = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x64.ActiveCfg = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x64.Build.0 = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x86.ActiveCfg = Release|Any CPU + {0E6CF267-14FD-43D7-81CB-EE020FD1D106}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {93D00F03-02FF-4C2A-8917-6863D6E633D9} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {14CAF7A1-A5C3-4ED1-8BF3-20FC3447BFFB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {51F770AF-C0C6-4247-A358-82DF323F473D} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {8A8E8B67-9351-471E-A502-AF7FAE596CB4} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {5DE8D878-EEBD-44DF-A8FC-0B4866B807FB} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {567DABFB-611E-4779-9F39-1D4A5B8F0247} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {28AFDFEF-7597-4450-B999-87C22C24DCD8} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {F80D5395-28E3-4C34-9662-A890A215DDA2} = {32C0F9AC-AF7D-4476-A269-99ACA000EF9F} + {7E15D295-B938-409C-98F6-A22ABC7F3005} = {AAAA5609-D8C1-401E-BB5C-724EFE3955FB} + {D632374A-70A1-4954-8F2D-C0B511A24426} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {91E41EE3-687B-4CFD-94C4-028D09BACFAB} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {4D411F2D-A97D-4485-A318-ED98B96B6CF6} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {7F36D200-FE91-4761-B681-BEE091FB953F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {69CCB780-50CC-4E75-B794-932E44ACA80F} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {C087F8AC-B438-4980-9A79-1E16D0CFB67A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + {E04E5F37-8F13-4000-ABA1-DAC0D795EA6A} = {FBC6F613-4E0B-4A90-8854-31887A796EEF} + EndGlobalSection +EndGlobal diff --git a/LibMatrix/Extensions/CanonicalJsonSerializer.cs b/LibMatrix/Extensions/CanonicalJsonSerializer.cs new file mode 100644
index 0000000..ae535aa --- /dev/null +++ b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
@@ -0,0 +1,96 @@ +using System.Collections.Frozen; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; +using ArcaneLibs.Extensions; + +namespace LibMatrix.Extensions; + +public static class CanonicalJsonSerializer { + // TODO: Alphabetise dictionaries + private static JsonSerializerOptions JsonOptions => new() { + WriteIndented = false, + Encoder = UnicodeJsonEncoder.Singleton, + }; + + private static readonly FrozenSet<PropertyInfo> JsonSerializerOptionsProperties = typeof(JsonSerializerOptions) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.SetMethod != null && x.GetMethod != null) + .ToFrozenSet(); + + private static JsonSerializerOptions MergeOptions(JsonSerializerOptions? inputOptions) { + var newOptions = JsonOptions; + if (inputOptions == null) + return newOptions; + + foreach (var property in JsonSerializerOptionsProperties) { + if(property.Name == nameof(JsonSerializerOptions.Encoder)) + continue; + if (property.Name == nameof(JsonSerializerOptions.WriteIndented)) + continue; + + var value = property.GetValue(inputOptions); + // if (value == null) + // continue; + property.SetValue(newOptions, value); + } + + return newOptions; + } + +#region STJ API + + public static String Serialize<TValue>(TValue value, JsonSerializerOptions? options = null) { + var newOptions = MergeOptions(options); + + return JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here... + .SortProperties()! + .CanonicalizeNumbers()! + .ToJsonString(newOptions); + + + // System.Text.Json.JsonSerializer.SerializeToNode(System.Text.Json.JsonSerializer.Deserialize<dynamic>("{\n \"a\": -0,\n \"b\": 1e10\n}")).ToJsonString(); + + } + + public static String Serialize(object value, Type inputType, JsonSerializerOptions? options = null) => JsonSerializer.Serialize(value, inputType, JsonOptions); + // public static String Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => JsonSerializer.Serialize(value, jsonTypeInfo, _options); + // public static String Serialize(Object value, JsonTypeInfo jsonTypeInfo) + + public static byte[] SerializeToUtf8Bytes<T>(T value, JsonSerializerOptions? options = null) { + var newOptions = MergeOptions(null); + return JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here... + .SortProperties()! + .CanonicalizeNumbers()! + .ToJsonString(newOptions).AsBytes().ToArray(); + } + +#endregion + + // ReSharper disable once UnusedType.Local + private static class JsonExtensions { + public static Action<JsonTypeInfo> AlphabetizeProperties(Type type) { + return typeInfo => { + if (typeInfo.Kind != JsonTypeInfoKind.Object || !type.IsAssignableFrom(typeInfo.Type)) + return; + AlphabetizeProperties()(typeInfo); + }; + } + + public static Action<JsonTypeInfo> AlphabetizeProperties() { + return static typeInfo => { + if (typeInfo.Kind == JsonTypeInfoKind.Dictionary) { } + + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + var properties = typeInfo.Properties.OrderBy(p => p.Name, StringComparer.Ordinal).ToList(); + typeInfo.Properties.Clear(); + for (int i = 0; i < properties.Count; i++) { + properties[i].Order = i; + typeInfo.Properties.Add(properties[i]); + } + }; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Extensions/EnumerableExtensions.cs b/LibMatrix/Extensions/EnumerableExtensions.cs
index 42d9491..4dcf26e 100644 --- a/LibMatrix/Extensions/EnumerableExtensions.cs +++ b/LibMatrix/Extensions/EnumerableExtensions.cs
@@ -1,29 +1,86 @@ +using System.Collections.Frozen; +using System.Collections.Immutable; + namespace LibMatrix.Extensions; public static class EnumerableExtensions { public static void MergeStateEventLists(this IList<StateEvent> oldState, IList<StateEvent> newState) { - foreach (var stateEvent in newState) { - var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey); - if (old is null) { - oldState.Add(stateEvent); - continue; + // foreach (var stateEvent in newState) { + // var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey); + // if (old is null) { + // oldState.Add(stateEvent); + // continue; + // } + // + // oldState.Remove(old); + // oldState.Add(stateEvent); + // } + + foreach (var e in newState) { + switch (FindIndex(e)) { + case -1: + oldState.Add(e); + break; + case var index: + oldState[index] = e; + break; + } + } + + int FindIndex(StateEvent needle) { + for (int i = 0; i < oldState.Count; i++) { + var old = oldState[i]; + if (old.Type == needle.Type && old.StateKey == needle.StateKey) + return i; } - oldState.Remove(old); - oldState.Add(stateEvent); + return -1; } } public static void MergeStateEventLists(this IList<StateEventResponse> oldState, IList<StateEventResponse> newState) { - foreach (var stateEvent in newState) { - var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey); - if (old is null) { - oldState.Add(stateEvent); - continue; + foreach (var e in newState) { + switch (FindIndex(e)) { + case -1: + oldState.Add(e); + break; + case var index: + oldState[index] = e; + break; + } + } + + int FindIndex(StateEventResponse needle) { + for (int i = 0; i < oldState.Count; i++) { + var old = oldState[i]; + if (old.Type == needle.Type && old.StateKey == needle.StateKey) + return i; + } + + return -1; + } + } + + public static void MergeStateEventLists(this List<StateEventResponse> oldState, List<StateEventResponse> newState) { + foreach (var e in newState) { + switch (FindIndex(e)) { + case -1: + oldState.Add(e); + break; + case var index: + oldState[index] = e; + break; + } + } + + int FindIndex(StateEventResponse needle) { + for (int i = 0; i < oldState.Count; i++) { + var old = oldState[i]; + if (old.Type == needle.Type && old.StateKey == needle.StateKey) + return i; } - oldState.Remove(old); - oldState.Add(stateEvent); + return -1; } } } \ No newline at end of file diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs
index c4ed743..dfec95b 100644 --- a/LibMatrix/Extensions/JsonElementExtensions.cs +++ b/LibMatrix/Extensions/JsonElementExtensions.cs
@@ -126,6 +126,7 @@ public static class JsonElementExtensions { $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!"); return field.Value.EnumerateObject() + // TODO: use key.Value? .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) .Aggregate(false, (current, key) => current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType) diff --git a/LibMatrix/Extensions/MatrixHttpClient.Multi.cs b/LibMatrix/Extensions/MatrixHttpClient.Multi.cs
index e7a2044..bf6fe63 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Multi.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Multi.cs
@@ -1,16 +1,5 @@ #define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http.Headers; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using ArcaneLibs; -using ArcaneLibs.Extensions; - namespace LibMatrix.Extensions; public static class HttpClientExtensions { diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index 39eb7e5..671566f 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -1,20 +1,21 @@ #define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances -// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging +// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Http.Headers; using System.Reflection; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using ArcaneLibs; using ArcaneLibs.Extensions; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; namespace LibMatrix.Extensions; #if SINGLE_HTTPCLIENT -// TODO: Add URI wrapper for +// TODO: Add URI wrapper for public class MatrixHttpClient { private static readonly HttpClient Client; @@ -50,6 +51,7 @@ public class MatrixHttpClient { internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); #endif + public static bool LogRequests = true; public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new(); public Uri? BaseAddress { get; set; } @@ -59,7 +61,7 @@ public class MatrixHttpClient { typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ?? throw new InvalidOperationException("Failed to create HttpRequestHeaders"); - private JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { + private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { options ??= new JsonSerializerOptions(); options.Converters.Add(new JsonFloatStringConverter()); options.Converters.Add(new JsonDoubleStringConverter()); @@ -68,27 +70,54 @@ public class MatrixHttpClient { return options; } - public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken, int attempt = 0) { + if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); + // if (!request.RequestUri.IsAbsoluteUri) + request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!); + var swWait = Stopwatch.StartNew(); #if SYNC_HTTPCLIENT await _rateLimitSemaphore.WaitAsync(cancellationToken); #endif - Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); - if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); - if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); + if (!request.RequestUri.IsAbsoluteUri) + request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri); + swWait.Stop(); + var swExec = Stopwatch.StartNew(); + foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); - foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value); + foreach (var (key, value) in DefaultRequestHeaders) { + if (request.Headers.Contains(key)) continue; + request.Headers.Add(key, value); + } request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); + if (LogRequests) + Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"])); + HttpResponseMessage? responseMessage; try { responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); } catch (Exception e) { - Console.WriteLine( - $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + // if (attempt >= 5) { + // Console.WriteLine( + // $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + // throw; + // } + // + // if (e is TaskCanceledException or TimeoutException or HttpRequestException) { + // if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) { + // await Task.Delay(Random.Shared.Next(100, 1000), cancellationToken); + // request.ResetSendStatus(); + // return await SendUnhandledAsync(request, cancellationToken, attempt + 1); + // } + // } + // else if (!e.ToString().StartsWith("TypeError: NetworkError")) + // Console.WriteLine( + // $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + throw; } #if SYNC_HTTPCLIENT @@ -97,8 +126,26 @@ public class MatrixHttpClient { } #endif - Console.WriteLine( - $"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})"); + // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)"); + if (LogRequests) + Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [ + "Server", + "Date", + "Transfer-Encoding", + "Connection", + "Vary", + "Content-Length", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Headers", + "Access-Control-Expose-Headers", + "Cache-Control", + "Cross-Origin-Resource-Policy", + "X-Content-Security-Policy", + "Referrer-Policy", + "X-Robots-Tag", + "Content-Security-Policy" + ])); return responseMessage; } @@ -107,6 +154,12 @@ public class MatrixHttpClient { var responseMessage = await SendUnhandledAsync(request, cancellationToken); if (responseMessage.IsSuccessStatusCode) return responseMessage; + //retry on gateway timeout + // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { + // request.ResetSendStatus(); + // return await SendAsync(request, cancellationToken); + // } + //error handling var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); if (content.Length == 0) @@ -114,10 +167,15 @@ public class MatrixHttpClient { ErrorCode = "M_UNKNOWN", Error = "Unknown error, server returned no content" }; - if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); + + // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); + if (!content.TrimStart().StartsWith('{')) { + responseMessage.EnsureSuccessStatusCode(); + throw new InvalidDataException("Encountered invalid data:\n" + content); + } //we have a matrix error - MatrixException? ex = null; + MatrixException? ex; try { ex = JsonSerializer.Deserialize<MatrixException>(content); } @@ -131,7 +189,7 @@ public class MatrixHttpClient { Debug.Assert(ex != null, nameof(ex) + " != null"); ex.RawContent = content; // Console.WriteLine($"Failed to send request: {ex}"); - if (ex?.RetryAfterMs is null) throw ex!; + if (ex.RetryAfterMs is null) throw ex!; //we have a ratelimit error await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); request.ResetSendStatus(); @@ -170,7 +228,7 @@ public class MatrixHttpClient { } // GetStreamAsync - public new async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { + public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { var request = new HttpRequestMessage(HttpMethod.Get, requestUri); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await SendAsync(request, cancellationToken); @@ -209,7 +267,7 @@ public class MatrixHttpClient { await foreach (var resp in result) yield return resp; } - public async Task<bool> CheckSuccessStatus(string url) { + public static async Task<bool> CheckSuccessStatus(string url) { //cors causes failure, try to catch try { var resp = await Client.GetAsync(url); @@ -227,5 +285,17 @@ public class MatrixHttpClient { }; return await SendAsync(request, cancellationToken); } + + public async Task<HttpResponseMessage> DeleteAsync(string url) { + var request = new HttpRequestMessage(HttpMethod.Delete, url); + return await SendAsync(request); + } + + public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) { + var request = new HttpRequestMessage(HttpMethod.Delete, url) { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + return await SendAsync(request); + } } #endif \ No newline at end of file diff --git a/LibMatrix/Extensions/UnicodeJsonEncoder.cs b/LibMatrix/Extensions/UnicodeJsonEncoder.cs new file mode 100644
index 0000000..ae58263 --- /dev/null +++ b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
@@ -0,0 +1,173 @@ +// LibMatrix: File sourced from https://github.com/dotnet/runtime/pull/87147/files under the MIT license. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.Encodings.Web; + +namespace LibMatrix.Extensions; + +internal sealed class UnicodeJsonEncoder : JavaScriptEncoder +{ + internal static readonly UnicodeJsonEncoder Singleton = new UnicodeJsonEncoder(); + + private readonly bool _preferHexEscape; + private readonly bool _preferUppercase; + + public UnicodeJsonEncoder() + : this(preferHexEscape: false, preferUppercase: false) + { + } + + public UnicodeJsonEncoder(bool preferHexEscape, bool preferUppercase) + { + _preferHexEscape = preferHexEscape; + _preferUppercase = preferUppercase; + } + + public override int MaxOutputCharactersPerInputCharacter => 6; // "\uXXXX" for a single char ("\uXXXX\uYYYY" [12 chars] for supplementary scalar value) + + public override unsafe int FindFirstCharacterToEncode(char* text, int textLength) + { + for (int index = 0; index < textLength; ++index) + { + char value = text[index]; + + if (NeedsEncoding(value)) + { + return index; + } + } + + return -1; + } + + public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) + { + bool encode = WillEncode(unicodeScalar); + + if (!encode) + { + Span<char> span = new Span<char>(buffer, bufferLength); + int spanWritten; + bool succeeded = new Rune(unicodeScalar).TryEncodeToUtf16(span, out spanWritten); + numberOfCharactersWritten = spanWritten; + return succeeded; + } + + if (!_preferHexEscape && unicodeScalar <= char.MaxValue && HasTwoCharacterEscape((char)unicodeScalar)) + { + if (bufferLength < 2) + { + numberOfCharactersWritten = 0; + return false; + } + + buffer[0] = '\\'; + buffer[1] = GetTwoCharacterEscapeSuffix((char)unicodeScalar); + numberOfCharactersWritten = 2; + return true; + } + else + { + if (bufferLength < 6) + { + numberOfCharactersWritten = 0; + return false; + } + + buffer[0] = '\\'; + buffer[1] = 'u'; + buffer[2] = '0'; + buffer[3] = '0'; + buffer[4] = ToHexDigit((unicodeScalar & 0xf0) >> 4, _preferUppercase); + buffer[5] = ToHexDigit(unicodeScalar & 0xf, _preferUppercase); + numberOfCharactersWritten = 6; + return true; + } + } + + public override bool WillEncode(int unicodeScalar) + { + if (unicodeScalar > char.MaxValue) + { + return false; + } + + return NeedsEncoding((char)unicodeScalar); + } + + // https://datatracker.ietf.org/doc/html/rfc8259#section-7 + private static bool NeedsEncoding(char value) + { + if (value == '"' || value == '\\') + { + return true; + } + + return value <= '\u001f'; + } + + private static bool HasTwoCharacterEscape(char value) + { + // RFC 8259, Section 7, "char = " BNF + switch (value) + { + case '"': + case '\\': + case '/': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + return true; + default: + return false; + } + } + + private static char GetTwoCharacterEscapeSuffix(char value) + { + // RFC 8259, Section 7, "char = " BNF + switch (value) + { + case '"': + return '"'; + case '\\': + return '\\'; + case '/': + return '/'; + case '\b': + return 'b'; + case '\f': + return 'f'; + case '\n': + return 'n'; + case '\r': + return 'r'; + case '\t': + return 't'; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + + private static char ToHexDigit(int value, bool uppercase) + { + if (value > 0xf) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (value < 10) + { + return (char)(value + '0'); + } + else + { + return (char)(value - 0xa + (uppercase ? 'A' : 'a')); + } + } +} \ No newline at end of file diff --git a/LibMatrix/Filters/SyncFilter.cs b/LibMatrix/Filters/SyncFilter.cs
index 787ffa7..7cb6428 100644 --- a/LibMatrix/Filters/SyncFilter.cs +++ b/LibMatrix/Filters/SyncFilter.cs
@@ -34,6 +34,15 @@ public class SyncFilter { [JsonPropertyName("include_leave")] public bool? IncludeLeave { get; set; } + private static RoomFilter Empty => new() { + Rooms = [], + IncludeLeave = false, + AccountData = StateFilter.Empty, + Ephemeral = StateFilter.Empty, + State = StateFilter.Empty, + Timeline = StateFilter.Empty, + }; + public class StateFilter( bool? containsUrl = null, bool? includeRedundantMembers = null, @@ -65,6 +74,9 @@ public class SyncFilter { [JsonPropertyName("unread_thread_notifications")] public bool? UnreadThreadNotifications { get; set; } = unreadThreadNotifications; + + // ReSharper disable once MemberHidesStaticFromOuterClass + public new static StateFilter Empty => new(limit: 0, senders: [], types: [], rooms: []); } } @@ -83,5 +95,7 @@ public class SyncFilter { [JsonPropertyName("not_senders")] public List<string>? NotSenders { get; set; } = notSenders; + + public static EventFilter Empty => new(limit: 0, senders: [], types: []); } -} \ No newline at end of file +} diff --git a/LibMatrix/Helpers/HomeserverWeightEstimation.cs b/LibMatrix/Helpers/HomeserverWeightEstimation.cs deleted file mode 100644
index 5735af3..0000000 --- a/LibMatrix/Helpers/HomeserverWeightEstimation.cs +++ /dev/null
@@ -1,58 +0,0 @@ -namespace LibMatrix.Helpers; - -public class HomeserverWeightEstimation { - public static Dictionary<string, int> EstimatedSize = new() { - { "matrix.org", 843870 }, - { "anontier.nl", 44809 }, - { "nixos.org", 8195 }, - { "the-apothecary.club", 6983 }, - { "waifuhunter.club", 3953 }, - { "neko.dev", 2666 }, - { "nerdsin.space", 2647 }, - { "feline.support", 2633 }, - { "gitter.im", 2584 }, - { "midov.pl", 2219 }, - { "no.lgbtqia.zone", 2083 }, - { "nheko.im", 1883 }, - { "fachschaften.org", 1849 }, - { "pixelthefox.net", 1478 }, - { "arcticfoxes.net", 981 }, - { "pixie.town", 817 }, - { "privacyguides.org", 809 }, - { "rory.gay", 653 }, - { "artemislena.eu", 599 }, - { "alchemi.dev", 445 }, - { "jameskitt616.one", 390 }, - { "hackint.org", 382 }, - { "pikaviestin.fi", 368 }, - { "matrix.nomagic.uk", 337 }, - { "thearcanebrony.net", 178 }, - { "fairydust.space", 176 }, - { "grin.hu", 176 }, - { "envs.net", 165 }, - { "tastytea.de", 143 }, - { "koneko.chat", 121 }, - { "vscape.tk", 115 }, - { "funklause.de", 112 }, - { "seirdy.one", 107 }, - { "pcg.life", 72 }, - { "draupnir.midnightthoughts.space", 22 }, - { "tchncs.de", 19 }, - { "catgirl.cloud", 16 }, - { "possum.city", 16 }, - { "tu-dresden.de", 9 }, - { "fosscord.com", 9 }, - { "nightshade.fun", 8 }, - { "matrix.eclipse.org", 8 }, - { "masfloss.net", 8 }, - { "e2e.zone", 8 }, - { "hyteck.de", 8 } - }; - - public static Dictionary<string, int> LargeRooms = new() { - { "!ehXvUhWNASUkSLvAGP:matrix.org", 21957 }, - { "!fRRqjOaQcUbKOfCjvc:anontier.nl", 19117 }, - { "!OGEhHVWSdvArJzumhm:matrix.org", 101457 }, - { "!YTvKGNlinIzlkMTVRl:matrix.org", 30164 } - }; -} \ No newline at end of file diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index d897078..6f55739 100644 --- a/LibMatrix/Helpers/MessageBuilder.cs +++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -10,9 +10,9 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr public RoomMessageEventContent Build() => Content; - public MessageBuilder WithBody(string body) { + public MessageBuilder WithBody(string body, string? formattedBody = null) { Content.Body += body; - Content.FormattedBody += body; + Content.FormattedBody += formattedBody ?? body; return this; } @@ -37,6 +37,10 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } + public static string GetColoredBody(string color, string body) { + return $"<font color=\"{color}\">{body}</font>"; + } + public MessageBuilder WithColoredBody(string color, string body) { Content.Body += body; Content.FormattedBody += $"<font color=\"{color}\">{body}</font>"; @@ -91,16 +95,38 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } + public MessageBuilder WithMention(string id, string? displayName = null) { + Content.Body += $"@{displayName ?? id}"; + Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; + if (id == "@room") { + Content.Mentions ??= new(); + Content.Mentions.Room = true; + } + else if (id.StartsWith('@')) { + Content.Mentions ??= new(); + Content.Mentions.Users ??= new(); + Content.Mentions.Users.Add(id); + } + + return this; + } + + public MessageBuilder WithNewline() { + Content.Body += "\n"; + Content.FormattedBody += "<br>"; + return this; + } + public MessageBuilder WithTable(Action<TableBuilder> tableBuilder) { var tb = new TableBuilder(this); - this.WithHtmlTag("table", msb => tableBuilder(tb)); + WithHtmlTag("table", msb => tableBuilder(tb)); return this; } public class TableBuilder(MessageBuilder msb) { public TableBuilder WithTitle(string title, int colspan) { msb.Content.Body += title + "\n"; - msb.Content.FormattedBody += $"<thead><tr><th colspan=\"{colspan}\">{title}</th></tr></thead>"; + msb.Content.FormattedBody += $"<thead><tr><th colspan=\"{colspan}\">{title}</th></tr></thead><br/>"; return this; } diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs new file mode 100644
index 0000000..bef7568 --- /dev/null +++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -0,0 +1,173 @@ +using System.Runtime.Intrinsics.X86; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; + +namespace LibMatrix.Helpers; + +public class RoomBuilder { + public string? Type { get; set; } + public string Version { get; set; } = "11"; + public RoomNameEventContent Name { get; set; } = new(); + public RoomTopicEventContent Topic { get; set; } = new(); + public RoomAvatarEventContent Avatar { get; set; } = new(); + public RoomCanonicalAliasEventContent CanonicalAlias { get; set; } = new(); + public string AliasLocalPart { get; set; } = string.Empty; + public bool IsFederatable { get; set; } = true; + public long OwnPowerLevel { get; set; } = MatrixConstants.MaxSafeJsonInteger; + + public RoomJoinRulesEventContent JoinRules { get; set; } = new() { + JoinRule = RoomJoinRulesEventContent.JoinRules.Public + }; + + public RoomHistoryVisibilityEventContent HistoryVisibility { get; set; } = new() { + HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared + }; + + /// <summary> + /// State events to be sent *before* room access is configured. Keep this small! + /// </summary> + public List<StateEvent> ImportantState { get; set; } = []; + + /// <summary> + /// State events to be sent *after* room access is configured, but before invites are sent. + /// </summary> + public List<StateEvent> InitialState { get; set; } = []; + + /// <summary> + /// Users to invite, with optional reason + /// </summary> + public Dictionary<string, string?> Invites { get; set; } = new(); + + public RoomPowerLevelEventContent PowerLevels { get; init; } = new() { + EventsDefault = 0, + UsersDefault = 0, + Kick = 50, + Invite = 50, + Ban = 50, + Redact = 50, + StateDefault = 50, + NotificationsPl = new() { + Room = 50 + }, + Users = [], + Events = new Dictionary<string, long> { + { RoomAvatarEventContent.EventId, 50 }, + { RoomCanonicalAliasEventContent.EventId, 50 }, + { RoomEncryptionEventContent.EventId, 100 }, + { RoomHistoryVisibilityEventContent.EventId, 100 }, + { RoomNameEventContent.EventId, 50 }, + { RoomPowerLevelEventContent.EventId, 100 }, + { RoomServerAclEventContent.EventId, 100 }, + { RoomTombstoneEventContent.EventId, 100 }, + { RoomPolicyServerEventContent.EventId, 100 } + } + }; + + public async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) { + var crq = new CreateRoomRequest() { + PowerLevelContentOverride = new() { + EventsDefault = 1000000, + UsersDefault = 1000000, + Kick = 1000000, + Invite = 1000000, + Ban = 1000000, + Redact = 1000000, + StateDefault = 1000000, + NotificationsPl = new() { + Room = 1000000 + }, + Users = new Dictionary<string, long>() { + { homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger } + }, + Events = new Dictionary<string, long> { + { RoomAvatarEventContent.EventId, 1000000 }, + { RoomCanonicalAliasEventContent.EventId, 1000000 }, + { RoomEncryptionEventContent.EventId, 1000000 }, + { RoomHistoryVisibilityEventContent.EventId, 1000000 }, + { RoomNameEventContent.EventId, 1000000 }, + { RoomPowerLevelEventContent.EventId, 1000000 }, + { RoomServerAclEventContent.EventId, 1000000 }, + { RoomTombstoneEventContent.EventId, 1000000 }, + { RoomPolicyServerEventContent.EventId, 1000000 } + } + }, + Visibility = "private", + RoomVersion = Version + }; + + if (!string.IsNullOrWhiteSpace(Type)) + crq.CreationContent.Add("type", Type); + + if (!IsFederatable) + crq.CreationContent.Add("m.federate", false); + + var room = await homeserver.CreateRoom(crq); + + await SetBasicRoomInfoAsync(room); + await SetStatesAsync(room, ImportantState); + await SetAccessAsync(room); + await SetStatesAsync(room, InitialState); + await SendInvites(room); + + return room; + } + + private async Task SendInvites(GenericRoom room) { + if (Invites.Count == 0) return; + + var inviteTasks = Invites.Select(async kvp => { + try { + await room.InviteUserAsync(kvp.Key, kvp.Value); + } + catch (MatrixException e) { + Console.Error.WriteLine("Failed to invite {0} to {1}: {2}", kvp.Key, room.RoomId, e.Message); + } + }); + + await Task.WhenAll(inviteTasks); + } + + private async Task SetStatesAsync(GenericRoom room, List<StateEvent> state) { + foreach (var ev in state) { + await (string.IsNullOrWhiteSpace(ev.StateKey) + ? room.SendStateEventAsync(ev.Type, ev.RawContent) + : room.SendStateEventAsync(ev.Type, ev.StateKey, ev.RawContent)); + } + } + + private async Task SetBasicRoomInfoAsync(GenericRoom room) { + if (!string.IsNullOrWhiteSpace(Name.Name)) + await room.SendStateEventAsync(RoomNameEventContent.EventId, Name); + + if (!string.IsNullOrWhiteSpace(Topic.Topic)) + await room.SendStateEventAsync(RoomTopicEventContent.EventId, Topic); + + if (!string.IsNullOrWhiteSpace(Avatar.Url)) + await room.SendStateEventAsync(RoomAvatarEventContent.EventId, Avatar); + + if (!string.IsNullOrWhiteSpace(AliasLocalPart)) + CanonicalAlias.Alias = $"#{AliasLocalPart}:{room.Homeserver.ServerName}"; + + if (!string.IsNullOrWhiteSpace(CanonicalAlias.Alias)) { + await room.Homeserver.SetRoomAliasAsync(CanonicalAlias.Alias!, room.RoomId); + await room.SendStateEventAsync(RoomCanonicalAliasEventContent.EventId, CanonicalAlias); + } + } + + private async Task SetAccessAsync(GenericRoom room) { + PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel; + await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, PowerLevels); + + if (!string.IsNullOrWhiteSpace(HistoryVisibility.HistoryVisibility)) + await room.SendStateEventAsync(RoomHistoryVisibilityEventContent.EventId, HistoryVisibility); + + if (!string.IsNullOrWhiteSpace(JoinRules.JoinRuleValue)) + await room.SendStateEventAsync(RoomJoinRulesEventContent.EventId, JoinRules); + } +} + +public class MatrixConstants { + public const long MaxSafeJsonInteger = 9007199254740991L; // 2^53 - 1 +} \ No newline at end of file diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 1833bd0..c8e2928 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -1,22 +1,56 @@ using System.Diagnostics; using System.Net.Http.Json; +using System.Reflection; +using System.Text.Json; +using ArcaneLibs.Collections; +using System.Text.Json.Nodes; using ArcaneLibs.Extensions; using LibMatrix.Filters; +using LibMatrix.Helpers.SyncProcessors; using LibMatrix.Homeservers; +using LibMatrix.Interfaces.Services; using LibMatrix.Responses; using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; -public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) { +public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null, IStorageProvider? storageProvider = null) { + private readonly Func<SyncResponse?, Task<SyncResponse?>> _msc4222EmulationSyncProcessor = new Msc4222EmulationSyncProcessor(homeserver, logger).EmulateMsc4222; + private SyncFilter? _filter; private string? _namedFilterName; - private bool _filterIsDirty = false; - private string? _filterId = null; + private bool _filterIsDirty; + private string? _filterId; public string? Since { get; set; } public int Timeout { get; set; } = 30000; - public string? SetPresence { get; set; } = "online"; + public string? SetPresence { get; set; } + + /// <summary> + /// Disabling this uses a technically slower code path, useful for checking whether delay comes from waiting for server or deserialising responses + /// </summary> + public bool UseInternalStreamingSync { get; set; } = true; + + public bool UseMsc4222StateAfter { + get; + set { + field = value; + if (value) { + AsyncSyncPreprocessors.Add(_msc4222EmulationSyncProcessor); + logger?.LogInformation($"Added MSC4222 emulation sync processor"); + } + else { + AsyncSyncPreprocessors.Remove(_msc4222EmulationSyncProcessor); + logger?.LogInformation($"Removed MSC4222 emulation sync processor"); + } + } + } = false; + + public List<Func<SyncResponse?, SyncResponse?>> SyncPreprocessors { get; } = [ + SimpleSyncProcessors.FillRoomIds + ]; + + public List<Func<SyncResponse?, Task<SyncResponse?>>> AsyncSyncPreprocessors { get; } = []; public string? FilterId { get => _filterId; @@ -42,16 +76,26 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg _filter = value; _filterIsDirty = true; _filterId = null; + _namedFilterName = null; } } + /// <summary> + /// Always include all rooms, and their full state according to passed filter + /// </summary> public bool FullState { get; set; } public bool IsInitialSync { get; set; } = true; public TimeSpan MinimumDelay { get; set; } = new(0); - private async Task updateFilterAsync() { + public async Task<int> GetUnoptimisedStoreCount() { + if (storageProvider is null) return -1; + var keys = await storageProvider.GetAllKeysAsync(); + return keys.Count(static x => !x.StartsWith("old/")) - 1; + } + + private async Task UpdateFilterAsync() { if (!string.IsNullOrWhiteSpace(NamedFilterName)) { _filterId = await homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(NamedFilterName); if (_filterId is null) @@ -61,9 +105,11 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg else if (Filter is not null) _filterId = (await homeserver.UploadFilterAsync(Filter)).FilterId; else _filterId = null; + + _filterIsDirty = false; } - public async Task<SyncResponse?> SyncAsync(CancellationToken? cancellationToken = null) { + public async Task<SyncResponse?> SyncAsync(CancellationToken? cancellationToken = null, bool noDelay = false) { if (homeserver is null) { Console.WriteLine("Null passed as homeserver for SyncHelper!"); throw new ArgumentNullException(nameof(homeserver), "Null passed as homeserver for SyncHelper!"); @@ -74,26 +120,86 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg throw new ArgumentNullException(nameof(homeserver.ClientHttpClient), "Null passed as homeserver for SyncHelper!"); } + if (storageProvider is null) { + var res = await SyncAsyncInternal(cancellationToken, noDelay); + if (res is null) return null; + if (UseMsc4222StateAfter) res.Msc4222Method = SyncResponse.Msc4222SyncType.Server; + + foreach (var preprocessor in SyncPreprocessors) { + res = preprocessor(res); + } + + foreach (var preprocessor in AsyncSyncPreprocessors) { + res = await preprocessor(res); + } + + return res; + } + + var key = Since ?? "init"; + if (await storageProvider.ObjectExistsAsync(key)) { + var cached = await storageProvider.LoadObjectAsync<SyncResponse>(key); + // We explicitly check that NextBatch doesn't match since to prevent infinite loops... + if (cached is not null && cached.NextBatch != Since) { + logger?.LogInformation("SyncHelper: Using cached sync response for {}", key); + return cached; + } + } + + var sync = await SyncAsyncInternal(cancellationToken, noDelay); + if (sync is null) return null; + // Ditto here. + if (sync.NextBatch != Since) await storageProvider.SaveObjectAsync(key, sync); + + if (UseMsc4222StateAfter) sync.Msc4222Method = SyncResponse.Msc4222SyncType.Server; + + foreach (var preprocessor in SyncPreprocessors) { + sync = preprocessor(sync); + } + + foreach (var preprocessor in AsyncSyncPreprocessors) { + sync = await preprocessor(sync); + } + + return sync; + } + + private async Task<SyncResponse?> SyncAsyncInternal(CancellationToken? cancellationToken = null, bool noDelay = false) { var sw = Stopwatch.StartNew(); - if (_filterIsDirty) await updateFilterAsync(); + if (_filterIsDirty) await UpdateFilterAsync(); - var url = $"/_matrix/client/v3/sync?timeout={Timeout}&set_presence={SetPresence}&full_state={(FullState ? "true" : "false")}"; + var url = $"/_matrix/client/v3/sync?timeout={Timeout}"; + if (!string.IsNullOrWhiteSpace(SetPresence)) url += $"&set_presence={SetPresence}"; if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}"; if (_filterId is not null) url += $"&filter={_filterId}"; + if (FullState) url += "&full_state=true"; + if (UseMsc4222StateAfter) url += "&org.matrix.msc4222.use_state_after=true&use_state_after=true"; // We use both unstable and stable names for compatibility - logger?.LogInformation("SyncHelper: Calling: {}", url); + // logger?.LogInformation("SyncHelper: Calling: {}", url); try { - var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None); - if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request"); - logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.Content.Headers.ContentLength ?? -1, sw.Elapsed); - var deserializeSw = Stopwatch.StartNew(); - var resp = await httpResp.Content.ReadFromJsonAsync<SyncResponse>(cancellationToken: cancellationToken ?? CancellationToken.None, - jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse); - logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.Content.Headers.ContentLength ?? -1, deserializeSw.Elapsed, sw.Elapsed); + SyncResponse? resp; + if (UseInternalStreamingSync) { + resp = await homeserver.ClientHttpClient.GetFromJsonAsync<SyncResponse>(url, cancellationToken: cancellationToken ?? CancellationToken.None); + logger?.LogInformation("Got sync response: ~{} bytes, {} elapsed", resp.ToJson(false, true, true).Length, sw.Elapsed); + } + else { + var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None); + if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request"); + var receivedTime = sw.Elapsed; + var deserializeSw = Stopwatch.StartNew(); + resp = await httpResp.Content.ReadFromJsonAsync(cancellationToken: cancellationToken ?? CancellationToken.None, + jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse); + logger?.LogInformation("Deserialized sync response: {} bytes, {} response time, {} deserialize time, {} total", httpResp.GetContentLength(), receivedTime, + deserializeSw.Elapsed, sw.Elapsed); + } + var timeToWait = MinimumDelay.Subtract(sw.Elapsed); - if (timeToWait.TotalMilliseconds > 0) + if (!noDelay && timeToWait.TotalMilliseconds > 0) { + logger?.LogWarning("SyncAsyncInternal: Waiting {delay}", timeToWait); await Task.Delay(timeToWait); + } + return resp; } catch (TaskCanceledException) { @@ -103,6 +209,8 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg catch (Exception e) { Console.WriteLine(e); logger?.LogError(e, "Failed to sync!\n{}", e.ToString()); + await Task.WhenAll(ExceptionHandlers.Select(x => x.Invoke(e)).ToList()); + if (e is MatrixException { ErrorCode: MatrixException.ErrorCodes.M_UNKNOWN_TOKEN }) throw; } return null; @@ -110,10 +218,17 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg public async IAsyncEnumerable<SyncResponse> EnumerateSyncAsync(CancellationToken? cancellationToken = null) { while (!cancellationToken?.IsCancellationRequested ?? true) { - var sync = await SyncAsync(cancellationToken); + var sw = Stopwatch.StartNew(); + var sync = await SyncAsync(cancellationToken, noDelay: true); if (sync is null) continue; if (!string.IsNullOrWhiteSpace(sync.NextBatch)) Since = sync.NextBatch; yield return sync; + + var timeToWait = MinimumDelay.Subtract(sw.Elapsed); + if (timeToWait.TotalMilliseconds > 0) { + logger?.LogWarning("EnumerateSyncAsync: Waiting {delay}", timeToWait); + await Task.Delay(timeToWait); + } } } @@ -183,7 +298,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg if (syncResponse.Rooms is { Join.Count: > 0 }) foreach (var updatedRoom in syncResponse.Rooms.Join) { if (updatedRoom.Value.Timeline is null) continue; - foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) { + foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events ?? []) { stateEventResponse.RoomId = updatedRoom.Key; var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList(); await Task.WhenAll(tasks); @@ -210,7 +325,12 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg /// Event fired when an account data event is received /// </summary> public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); - + + /// <summary> + /// Event fired when an exception is thrown + /// </summary> + public List<Func<Exception, Task>> ExceptionHandlers { get; } = new(); + private void Log(string message) { if (logger is null) Console.WriteLine(message); else logger.LogInformation(message); diff --git a/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs new file mode 100644
index 0000000..e34b5cf --- /dev/null +++ b/LibMatrix/Helpers/SyncProcessors/Msc4222EmulationSyncProcessor.cs
@@ -0,0 +1,210 @@ +using System.Diagnostics; +using System.Timers; +using ArcaneLibs.Extensions; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Helpers.SyncProcessors; + +public class Msc4222EmulationSyncProcessor(AuthenticatedHomeserverGeneric homeserver, ILogger? logger) { + private static bool StateEventsMatch(StateEventResponse a, StateEventResponse b) { + return a.Type == b.Type && a.StateKey == b.StateKey; + } + + private static bool StateEventIsNewer(StateEventResponse a, StateEventResponse b) { + return StateEventsMatch(a, b) && a.OriginServerTs < b.OriginServerTs; + } + + public async Task<SyncResponse?> EmulateMsc4222(SyncResponse? resp) { + var sw = Stopwatch.StartNew(); + if (resp is null or { Rooms: null }) return resp; + + if ( + resp.Rooms.Join?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true + || resp.Rooms.Leave?.Any(x => x.Value.StateAfter is { Events.Count: > 0 }) == true + ) { + logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug, + "Msc4222EmulationSyncProcessor.EmulateMsc4222 determined that no emulation is needed in {elapsed}", sw.Elapsed); + return resp; + } + + resp = await EmulateMsc4222Internal(resp, sw); + + return SimpleSyncProcessors.FillRoomIds(resp); + } + + private async Task<SyncResponse?> EmulateMsc4222Internal(SyncResponse? resp, Stopwatch sw) { + var modified = false; + List<Task<bool>> tasks = []; + if (resp.Rooms is { Join.Count: > 0 }) { + tasks.AddRange(resp.Rooms.Join.Select(ProcessJoinedRooms).ToList()); + } + + if (resp.Rooms is { Leave.Count: > 0 }) { + tasks.AddRange(resp.Rooms.Leave.Select(ProcessLeftRooms).ToList()); + } + + var tasksEnum = tasks.ToAsyncEnumerable(); + await foreach (var wasModified in tasksEnum) { + if (wasModified) { + modified = true; + } + } + + logger?.Log(sw.ElapsedMilliseconds > 100 ? LogLevel.Warning : LogLevel.Debug, + "Msc4222EmulationSyncProcessor.EmulateMsc4222 processed {joinCount}/{leaveCount} rooms in {elapsed} (modified: {modified})", + resp.Rooms?.Join?.Count ?? 0, resp.Rooms?.Leave?.Count ?? 0, sw.Elapsed, modified); + + if (modified) + resp.Msc4222Method = SyncResponse.Msc4222SyncType.Emulated; + + return resp; + } + + private async Task<bool> ProcessJoinedRooms(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> roomData) { + var (roomId, data) = roomData; + var room = homeserver.GetRoom(roomId); + + if (data.StateAfter is { Events.Count: > 0 }) { + return false; + } + + data.StateAfter = new() { Events = [] }; + + data.StateAfter = new() { + Events = [] + }; + + var oldState = new List<StateEventResponse>(); + if (data.State is { Events.Count: > 0 }) { + oldState.ReplaceBy(data.State.Events, StateEventIsNewer); + } + + if (data.Timeline is { Limited: true }) { + if (data.Timeline.Events != null) + oldState.ReplaceBy(data.Timeline.Events, StateEventIsNewer); + + try { + var timeline = await homeserver.GetRoom(roomId).GetMessagesAsync(limit: 250); + if (timeline is { State.Count: > 0 }) { + oldState.ReplaceBy(timeline.State, StateEventIsNewer); + } + + if (timeline is { Chunk.Count: > 0 }) { + oldState.ReplaceBy(timeline.Chunk.Where(x => x.StateKey != null), StateEventIsNewer); + } + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e); + } + } + + oldState = oldState.DistinctBy(x => (x.Type, x.StateKey)).ToList(); + + // Different order: we need oldState here to reduce the set + try { + data.StateAfter.Events = (await room.GetFullStateAsListAsync()) + // .Where(x=> oldState.Any(y => StateEventsMatch(x, y))) + // .Join(oldState, x => (x.Type, x.StateKey), y => (y.Type, y.StateKey), (x, y) => x) + .IntersectBy(oldState.Select(s => (s.Type, s.StateKey)), s => (s.Type, s.StateKey)) + .ToList(); + + data.State = null; + return true; + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e); + } + + var tasks = oldState + .Select(async oldEvt => { + try { + return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!); + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}", + oldEvt.Type, oldEvt.StateKey, roomId, e); + return oldEvt; + } + }); + + var tasksEnum = tasks.ToAsyncEnumerable(); + await foreach (var evt in tasksEnum) { + data.StateAfter.Events.Add(evt); + } + + data.State = null; + + return true; + } + + private async Task<bool> ProcessLeftRooms(KeyValuePair<string, SyncResponse.RoomsDataStructure.LeftRoomDataStructure> roomData) { + var (roomId, data) = roomData; + var room = homeserver.GetRoom(roomId); + + if (data.StateAfter is { Events.Count: > 0 }) { + return false; + } + + data.StateAfter = new() { + Events = [] + }; + + try { + data.StateAfter.Events = await room.GetFullStateAsListAsync(); + data.State = null; + return true; + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get full state for room {roomId}, state may be incomplete!\n{exception}", roomId, e); + } + + var oldState = new List<StateEventResponse>(); + if (data.State is { Events.Count: > 0 }) { + oldState.ReplaceBy(data.State.Events, StateEventIsNewer); + } + + if (data.Timeline is { Limited: true }) { + if (data.Timeline.Events != null) + oldState.ReplaceBy(data.Timeline.Events, StateEventIsNewer); + + try { + var timeline = await homeserver.GetRoom(roomId).GetMessagesAsync(limit: 250); + if (timeline is { State.Count: > 0 }) { + oldState.ReplaceBy(timeline.State, StateEventIsNewer); + } + + if (timeline is { Chunk.Count: > 0 }) { + oldState.ReplaceBy(timeline.Chunk.Where(x => x.StateKey != null), StateEventIsNewer); + } + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get timeline for room {roomId}, state may be incomplete!\n{exception}", roomId, e); + } + } + + oldState = oldState.DistinctBy(x => (x.Type, x.StateKey)).ToList(); + + var tasks = oldState + .Select(async oldEvt => { + try { + return await room.GetStateEventAsync(oldEvt.Type, oldEvt.StateKey!); + } + catch (Exception e) { + logger?.LogWarning("Msc4222Emulation: Failed to get state event {type}/{stateKey} for room {roomId}, state may be incomplete!\n{exception}", + oldEvt.Type, oldEvt.StateKey, roomId, e); + return oldEvt; + } + }); + + var tasksEnum = tasks.ToAsyncEnumerable(); + await foreach (var evt in tasksEnum) { + data.StateAfter.Events.Add(evt); + } + + data.State = null; + + return true; + } +} \ No newline at end of file diff --git a/LibMatrix/Helpers/SyncProcessors/SimpleSyncProcessors.cs b/LibMatrix/Helpers/SyncProcessors/SimpleSyncProcessors.cs new file mode 100644
index 0000000..5981cb5 --- /dev/null +++ b/LibMatrix/Helpers/SyncProcessors/SimpleSyncProcessors.cs
@@ -0,0 +1,47 @@ +using System.Diagnostics; +using LibMatrix.Responses; + +namespace LibMatrix.Helpers.SyncProcessors; + +public class SimpleSyncProcessors { + public static SyncResponse? FillRoomIds(SyncResponse? resp) { + var sw = Stopwatch.StartNew(); + if (resp is not { Rooms: not null }) return resp; + if (resp.Rooms.Join is { Count: > 0 }) + Parallel.ForEach(resp.Rooms.Join, (roomEntry) => { + var (id, data) = roomEntry; + if (data.AccountData is { Events.Count: > 0 }) + Parallel.ForEach(data.AccountData.Events, evt => evt.RoomId = id); + if (data.Ephemeral is { Events.Count: > 0 }) + Parallel.ForEach(data.Ephemeral.Events, evt => evt.RoomId = id); + if (data.Timeline is { Events.Count: > 0 }) + Parallel.ForEach(data.Timeline.Events, evt => evt.RoomId = id); + if (data.State is { Events.Count: > 0 }) + Parallel.ForEach(data.State.Events, evt => evt.RoomId = id); + if (data.StateAfter is { Events.Count: > 0 }) + Parallel.ForEach(data.StateAfter.Events, evt => evt.RoomId = id); + }); + if (resp.Rooms.Leave is { Count: > 0 }) + Parallel.ForEach(resp.Rooms.Leave, (roomEntry) => { + var (id, data) = roomEntry; + if (data.AccountData is { Events.Count: > 0 }) + Parallel.ForEach(data.AccountData.Events, evt => evt.RoomId = id); + if (data.Timeline is { Events.Count: > 0 }) + Parallel.ForEach(data.Timeline.Events, evt => evt.RoomId = id); + if (data.State is { Events.Count: > 0 }) + Parallel.ForEach(data.State.Events, evt => evt.RoomId = id); + if (data.StateAfter is { Events.Count: > 0 }) + Parallel.ForEach(data.StateAfter.Events, evt => evt.RoomId = id); + }); + if (resp.Rooms.Invite is { Count: > 0 }) + Parallel.ForEach(resp.Rooms.Invite, (roomEntry) => { + var (id, data) = roomEntry; + if (data.InviteState is { Events.Count: > 0 }) + Parallel.ForEach(data.InviteState.Events, evt => evt.RoomId = id); + }); + + Console.WriteLine($"SimpleSyncProcessors.FillRoomIds took {sw.Elapsed}"); + + return resp; + } +} \ No newline at end of file diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs
index 72d600d..f111c79 100644 --- a/LibMatrix/Helpers/SyncStateResolver.cs +++ b/LibMatrix/Helpers/SyncStateResolver.cs
@@ -1,12 +1,20 @@ +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text.Json; +using System.Threading.Tasks.Dataflow; +using ArcaneLibs.Extensions; using LibMatrix.Extensions; using LibMatrix.Filters; using LibMatrix.Homeservers; +using LibMatrix.Interfaces.Services; using LibMatrix.Responses; using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; -public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) { +public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null, IStorageProvider? storageProvider = null) { public string? Since { get; set; } public int Timeout { get; set; } = 30000; public string? SetPresence { get; set; } = "online"; @@ -15,7 +23,20 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge public SyncResponse? MergedState { get; set; } - private SyncHelper _syncHelper = new(homeserver, logger); + private SyncHelper _syncHelper = new(homeserver, logger, storageProvider); + + private async Task<SyncResponse?> LoadSyncResponse(string key) { + if (storageProvider is null) ArgumentNullException.ThrowIfNull(storageProvider); + var stream = await storageProvider.LoadStreamAsync(key); + return JsonSerializer.Deserialize<SyncResponse>(stream!, SyncResponseSerializerContext.Default.SyncResponse); + } + + private async Task SaveSyncResponse(string key, SyncResponse value) { + ArgumentNullException.ThrowIfNull(storageProvider); + var ms = new MemoryStream(); + await JsonSerializer.SerializeAsync(ms, value, SyncResponseSerializerContext.Default.SyncResponse); + await storageProvider.SaveStreamAsync(key, ms); + } public async Task<(SyncResponse next, SyncResponse merged)> ContinueAsync(CancellationToken? cancellationToken = null) { // copy properties @@ -24,149 +45,615 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge _syncHelper.SetPresence = SetPresence; _syncHelper.Filter = Filter; _syncHelper.FullState = FullState; - // run sync - var sync = await _syncHelper.SyncAsync(cancellationToken); + + var sync = await _syncHelper.SyncAsync(cancellationToken, noDelay: false); if (sync is null) return await ContinueAsync(cancellationToken); + if (MergedState is null) MergedState = sync; - else MergedState = MergeSyncs(MergedState, sync); + else MergedState = await MergeSyncs(MergedState, sync); Since = sync.NextBatch; + return (sync, MergedState); } - private SyncResponse MergeSyncs(SyncResponse oldState, SyncResponse newState) { - oldState.NextBatch = newState.NextBatch ?? oldState.NextBatch; + // private async IAsyncEnumerable<List<SyncResponse>> MergeP() { + + // } + + private async Task<SyncResponse?> OptimiseFrom(string start, Action<int, int>? progressCallback = null) { + var a = GetSerializedUnoptimisedResponses(start); + SyncResponse merged = null!; + int iters = 0; + var sw = Stopwatch.StartNew(); + await foreach (var (key, resp) in a) { + if (resp is null) continue; + iters++; + // if (key == "init") _merged = resp; + // else _merged = await MergeSyncs(_merged, resp); + // Console.WriteLine($"{key} @ {resp.GetDerivedSyncTime()} -> {resp.NextBatch}"); + } - oldState.AccountData ??= new EventList(); - oldState.AccountData.Events ??= new List<StateEventResponse>(); - if (newState.AccountData?.Events is not null) - oldState.AccountData.Events.MergeStateEventLists(newState.AccountData?.Events ?? new List<StateEventResponse>()); + Console.WriteLine($"OptimiseFrom {start} finished in {sw.Elapsed.TotalMilliseconds}ms with {iters} iterations"); - oldState.Presence ??= new SyncResponse.PresenceDataStructure(); - if (newState.Presence?.Events is not null) - oldState.Presence.Events.MergeStateEventLists(newState.Presence?.Events ?? new List<StateEventResponse>()); + return merged; + } - oldState.DeviceOneTimeKeysCount ??= new Dictionary<string, int>(); - if (newState.DeviceOneTimeKeysCount is not null) - foreach (var (key, value) in newState.DeviceOneTimeKeysCount) - oldState.DeviceOneTimeKeysCount[key] = value; + private async Task<List<string>> GetSerializedUnoptimisedKeysParallel(string start = "init") { + Dictionary<string, string> pairs = []; + var unoptimisedKeys = (await storageProvider.GetAllKeysAsync()).Where(static x => !x.Contains('/')).ToFrozenSet(); + await Parallel.ForEachAsync(unoptimisedKeys, async (key, _) => { + var data = await storageProvider.LoadObjectAsync<SyncResponse>(key, SyncResponseSerializerContext.Default.SyncResponse); + if (data is null) return; + lock (pairs) + pairs.Add(key, data.NextBatch); + }); + + var serializedKeys = new List<string>(); + var currentKey = start; + while (pairs.TryGetValue(currentKey, out var nextKey)) { + serializedKeys.Add(currentKey); + currentKey = nextKey; + } - oldState.Rooms ??= new SyncResponse.RoomsDataStructure(); - if (newState.Rooms is not null) - oldState.Rooms = MergeRoomsDataStructure(oldState.Rooms, newState.Rooms); + return serializedKeys; + } - oldState.ToDevice ??= new EventList(); - oldState.ToDevice.Events ??= new List<StateEventResponse>(); - if (newState.ToDevice?.Events is not null) - oldState.ToDevice.Events.MergeStateEventLists(newState.ToDevice?.Events ?? new List<StateEventResponse>()); + private async Task<SyncResponse> MergeRecursive(string[] keys, int depth = 0) { + if (keys.Length > 10) { + var newKeys = keys.Chunk((keys.Length / 2) + 1).ToArray(); + var (left, right) = (MergeRecursive(newKeys[0], depth + 1), MergeRecursive(newKeys[1], depth + 1)); + await Task.WhenAll(left, right); + return await MergeSyncs(await left, await right); + } - oldState.DeviceLists ??= new SyncResponse.DeviceListsDataStructure(); - if (newState.DeviceLists?.Changed is not null) - foreach (var s in oldState.DeviceLists.Changed!) - oldState.DeviceLists.Changed.Add(s); - if (newState.DeviceLists?.Left is not null) - foreach (var s in oldState.DeviceLists.Left!) - oldState.DeviceLists.Left.Add(s); + // Console.WriteLine("Hit max depth: " + depth); + SyncResponse merged = await LoadSyncResponse(keys[0]); + foreach (var key in keys[1..]) { + merged = await MergeSyncs(merged, await LoadSyncResponse(key)); + } - return oldState; + return merged; } -#region Merge rooms + public async Task OptimiseStore(Action<int, int>? progressCallback = null) { + if (storageProvider is null) return; + if (!await storageProvider.ObjectExistsAsync("init")) return; + // + // { + // var a = GetSerializedUnoptimisedResponses(); + // SyncResponse _merged = null!; + // await foreach (var (key, resp) in a) { + // if (resp is null) continue; + // // if (key == "init") _merged = resp; + // // else _merged = await MergeSyncs(_merged, resp); + // // Console.WriteLine($"{key} @ {resp.GetDerivedSyncTime()} -> {resp.NextBatch}"); + // } + // Environment.Exit(0); + // } + + { + // List<string> serialisedKeys = new(4000000); + // await foreach (var res in GetSerializedUnoptimisedResponses()) { + // if (res.resp is null) continue; + // serialisedKeys.Add(res.key); + // if (serialisedKeys.Count % 1000 == 0) _ = Console.Out.WriteAsync($"{serialisedKeys.Count}\r"); + // } + + List<string> serialisedKeys = await GetSerializedUnoptimisedKeysParallel(); + + await MergeRecursive(serialisedKeys.ToArray()); + + // var chunkSize = serialisedKeys.Count / Environment.ProcessorCount; + // var chunks = serialisedKeys.Chunk(chunkSize+1).Select(x => (x.First(), x.Length)).ToList(); + // Console.WriteLine($"Got {chunks.Count} chunks:"); + // foreach (var chunk in chunks) { + // Console.WriteLine($"Chunk {chunk.Item1} with length {chunk.Length}"); + // } + // + // var mergeTasks = chunks.Select(async chunk => { + // var (startKey, length) = chunk; + // string currentKey = startKey; + // SyncResponse merged = await storageProvider.LoadObjectAsync<SyncResponse>(currentKey, SyncResponseSerializerContext.Default.SyncResponse); + // for (int i = 0; i < length; i++) { + // if (i % 1000 == 0) Console.Write($"{i}... \r"); + // var newData = await storageProvider.LoadObjectAsync<SyncResponse>(currentKey, SyncResponseSerializerContext.Default.SyncResponse); + // merged = await MergeSyncs(merged, newData); + // currentKey = merged.NextBatch; + // } + // + // return merged; + // }).ToList(); + // + // var mergedResults = await Task.WhenAll(mergeTasks); + // SyncResponse _merged = mergedResults[0]; + // foreach (var key in mergedResults[1..]) { + // _merged = await MergeSyncs(_merged, key); + // } + } + + Environment.Exit(0); + + return; + + var totalSw = Stopwatch.StartNew(); + Console.Write("Optimising sync store..."); + var initLoadTask = LoadSyncResponse("init"); + var keys = (await storageProvider.GetAllKeysAsync()).Where(static x => !x.StartsWith("old/")).ToFrozenSet(); + var count = keys.Count - 1; + int total = count; + Console.WriteLine($"Found {count} entries to optimise in {totalSw.Elapsed}."); + + var merged = await initLoadTask; + if (merged is null) return; + if (!keys.Contains(merged.NextBatch)) { + Console.WriteLine("Next response after initial sync is not present, not checkpointing!"); + return; + } + + // if (keys.Count > 100_000) { + // // batch data by core count + // + // return; + // } + + // We back up old entries + var oldPath = $"old/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + await storageProvider.MoveObjectAsync("init", $"{oldPath}/init"); + + var moveTasks = new List<Task>(); + + Dictionary<string, Dictionary<string, TimeSpan>> traces = []; + string[] loopTrace = new string[4]; + while (keys.Contains(merged.NextBatch)) { + loopTrace[0] = $"Merging {merged.NextBatch}, {--count} remaining"; + var sw = Stopwatch.StartNew(); + var swt = Stopwatch.StartNew(); + var next = await LoadSyncResponse(merged.NextBatch); + loopTrace[1] = $"Load {sw.GetElapsedAndRestart().TotalMilliseconds}ms"; + if (next is null || merged.NextBatch == next.NextBatch) break; + + // back up old entry + moveTasks.Add(storageProvider.MoveObjectAsync(merged.NextBatch, $"{oldPath}/{merged.NextBatch}")); + + if (moveTasks.Count >= 250) + moveTasks.RemoveAll(t => t.IsCompleted); + + if (moveTasks.Count >= 500) { + Console.Write("Reached 500 moveTasks... "); + moveTasks.RemoveAll(t => t.IsCompleted); + Console.WriteLine($"{moveTasks.Count} remaining"); + } + + var trace = new Dictionary<string, TimeSpan>(); + traces[merged.NextBatch] = trace; + merged = await MergeSyncs(merged, next, trace); + loopTrace[2] = $"Merge {sw.GetElapsedAndRestart().TotalMilliseconds}ms"; + loopTrace[3] = $"Total {swt.Elapsed.TotalMilliseconds}ms"; + + if (swt.ElapsedMilliseconds >= 25) + Console.WriteLine(string.Join("... ", loopTrace)); + + if (count % 50 == 0) + progressCallback?.Invoke(count, total); +#if WRITE_TRACE + var traceString = string.Join("\n", traces.Select(x => $"{x.Key}\t{x.Value.ToJson(indent: false, ignoreNull: true)}")); + var ms = new MemoryStream(Encoding.UTF8.GetBytes(traceString)); + var traceSaveTask = storageProvider.SaveStreamAsync($"traces/{oldPath}", ms); + var slowtraceString = string.Join("\n", + traces + .Where(x=>x.Value.Max(y=>y.Value.TotalMilliseconds) >= 100) + .OrderBy(x=>x.Value.Max(y=>y.Value)) + .Select(x => $"{x.Key}\t{x.Value.Where(y => y.Value.TotalMilliseconds >= 100).ToDictionary().ToJson(indent: false, ignoreNull: true)}")); + var slowms = new MemoryStream(Encoding.UTF8.GetBytes(slowtraceString)); + var slowTraceSaveTask = storageProvider.SaveStreamAsync($"traces/{oldPath}-slow", slowms); + var slow1straceString = string.Join("\n", + traces + .Where(x=>x.Value.Max(y=>y.Value.TotalMilliseconds) >= 1000) + .OrderBy(x=>x.Value.Max(y=>y.Value)) + .Select(x => $"{x.Key}\t{x.Value.Where(y => y.Value.TotalMilliseconds >= 1000).ToDictionary().ToJson(indent: false, ignoreNull: true)}")); + var slow1sms = new MemoryStream(Encoding.UTF8.GetBytes(slow1straceString)); + var slow1sTraceSaveTask = storageProvider.SaveStreamAsync($"traces/{oldPath}-slow-1s", slow1sms); + + await Task.WhenAll(traceSaveTask, slowTraceSaveTask, slow1sTraceSaveTask); +#endif + } - private SyncResponse.RoomsDataStructure MergeRoomsDataStructure(SyncResponse.RoomsDataStructure oldState, SyncResponse.RoomsDataStructure newState) { - oldState.Join ??= new Dictionary<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>(); - foreach (var (key, value) in newState.Join ?? new Dictionary<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure>()) - if (!oldState.Join.ContainsKey(key)) oldState.Join[key] = value; - else oldState.Join[key] = MergeJoinedRoomDataStructure(oldState.Join[key], value); + await SaveSyncResponse("init", merged); + await Task.WhenAll(moveTasks); - oldState.Invite ??= new Dictionary<string, SyncResponse.RoomsDataStructure.InvitedRoomDataStructure>(); - foreach (var (key, value) in newState.Invite ?? new Dictionary<string, SyncResponse.RoomsDataStructure.InvitedRoomDataStructure>()) - if (!oldState.Invite.ContainsKey(key)) oldState.Invite[key] = value; - else oldState.Invite[key] = MergeInvitedRoomDataStructure(oldState.Invite[key], value); + Console.WriteLine($"Optimised store in {totalSw.Elapsed.TotalMilliseconds}ms"); + } + + /// <summary> + /// Remove all but initial sync and last checkpoint + /// </summary> + public async Task RemoveOldSnapshots() { + if (storageProvider is null) return; + var sw = Stopwatch.StartNew(); + + var map = await GetCheckpointMap(); + if (map is null) return; + if (map.Count < 3) return; + + var toRemove = map.Keys.Skip(1).Take(map.Count - 2).ToList(); + Console.Write("Cleaning up old snapshots: "); + foreach (var key in toRemove) { + var path = $"old/{key}/init"; + if (await storageProvider.ObjectExistsAsync(path)) { + Console.Write($"{key}... "); + await storageProvider.DeleteObjectAsync(path); + } + } + + Console.WriteLine("Done!"); + Console.WriteLine($"Removed {toRemove.Count} old snapshots in {sw.Elapsed.TotalMilliseconds}ms"); + } + + public async Task UnrollOptimisedStore() { + if (storageProvider is null) return; + Console.WriteLine("WARNING: Unrolling sync store!"); + } + + public async Task SquashOptimisedStore(int targetCountPerCheckpoint) { + Console.Write($"Balancing optimised store to {targetCountPerCheckpoint} per checkpoint..."); + var checkpoints = await GetCheckpointMap(); + if (checkpoints is null) return; + + Console.WriteLine( + $" Stats: {checkpoints.Count} checkpoints with [{checkpoints.Min(x => x.Value.Count)} < ~{checkpoints.Average(x => x.Value.Count)} < {checkpoints.Max(x => x.Value.Count)}] entries"); + Console.WriteLine($"Found {checkpoints?.Count ?? 0} checkpoints."); + } + + public async Task dev() { + int i = 0; + var sw = Stopwatch.StartNew(); + var hist = GetSerialisedHistory(); + await foreach (var (key, resp) in hist) { + if (resp is null) continue; + // Console.WriteLine($"[{++i}] {key} -> {resp.NextBatch} ({resp.GetDerivedSyncTime()})"); + i++; + } + + Console.WriteLine($"Iterated {i} syncResponses in {sw.Elapsed}"); + Environment.Exit(0); + } + + private async IAsyncEnumerable<(string key, SyncResponse? resp)> GetSerialisedHistory() { + if (storageProvider is null) yield break; + var map = await GetCheckpointMap(); + var currentRange = map.First(); + var nextKey = $"old/{map.First().Key}/init"; + var next = storageProvider.LoadObjectAsync<SyncResponse>(nextKey); + while (true) { + var data = await next; + if (data is null) break; + yield return (nextKey, data); + if (currentRange.Value.Contains(data.NextBatch)) { + nextKey = $"old/{currentRange.Key}/{data.NextBatch}"; + } + else if (map.Any(x => x.Value.Contains(data.NextBatch))) { + currentRange = map.First(x => x.Value.Contains(data.NextBatch)); + nextKey = $"old/{currentRange.Key}/{data.NextBatch}"; + } + else if (await storageProvider.ObjectExistsAsync(data.NextBatch)) { + nextKey = data.NextBatch; + } + else break; + + next = storageProvider.LoadObjectAsync<SyncResponse>(nextKey); + } + } + + private async IAsyncEnumerable<(string key, SyncResponse? resp)> GetSerializedUnoptimisedResponses(string since = "init") { + if (storageProvider is null) yield break; + var nextKey = since; + var next = storageProvider.LoadObjectAsync<SyncResponse>(nextKey); + while (true) { + var data = await next; + + if (data is null) break; + yield return (nextKey, data); + if (await storageProvider.ObjectExistsAsync(data.NextBatch)) { + nextKey = data.NextBatch; + } + else break; + + next = storageProvider.LoadObjectAsync<SyncResponse>(nextKey); + } + } + + public async Task<SyncResponse?> GetMergedUpTo(DateTime time) { + if (storageProvider is null) return null; + var unixTime = new DateTimeOffset(time.ToUniversalTime()).ToUnixTimeMilliseconds(); + var map = await GetCheckpointMap(); + if (map is null) return new(); + var stream = GetSerialisedHistory().GetAsyncEnumerator(); + SyncResponse? merged = await stream.MoveNextAsync() ? stream.Current.resp : null; + + if (merged.GetDerivedSyncTime() > unixTime) { + Console.WriteLine("Initial sync is already past the target time!"); + Console.WriteLine($"CURRENT: {merged.GetDerivedSyncTime()} (UTC: {DateTimeOffset.FromUnixTimeMilliseconds(merged.GetDerivedSyncTime())})"); + Console.WriteLine($" TARGET: {unixTime} ({time.Kind}: {time}, UTC: {time.ToUniversalTime()})"); + return null; + } - oldState.Leave ??= new Dictionary<string, SyncResponse.RoomsDataStructure.LeftRoomDataStructure>(); - foreach (var (key, value) in newState.Leave ?? new Dictionary<string, SyncResponse.RoomsDataStructure.LeftRoomDataStructure>()) { - if (!oldState.Leave.ContainsKey(key)) oldState.Leave[key] = value; - else oldState.Leave[key] = MergeLeftRoomDataStructure(oldState.Leave[key], value); - if (oldState.Invite.ContainsKey(key)) oldState.Invite.Remove(key); - if (oldState.Join.ContainsKey(key)) oldState.Join.Remove(key); + while (await stream.MoveNextAsync()) { + var (key, resp) = stream.Current; + if (resp is null) continue; + if (resp.GetDerivedSyncTime() > unixTime) break; + merged = await MergeSyncs(merged, resp); } + return merged; + } + + private async Task<ImmutableSortedDictionary<ulong, FrozenSet<string>>> GetCheckpointMap() { + if (storageProvider is null) return null; + var keys = (await storageProvider.GetAllKeysAsync()).ToFrozenSet(); + var map = new Dictionary<ulong, List<string>>(); + foreach (var key in keys) { + if (!key.StartsWith("old/")) continue; + var parts = key.Split('/'); + if (parts.Length < 3) continue; + if (!ulong.TryParse(parts[1], out var checkpoint)) continue; + if (!map.ContainsKey(checkpoint)) map[checkpoint] = new(); + map[checkpoint].Add(parts[2]); + } + + return map.OrderBy(static x => x.Key).ToImmutableSortedDictionary(static x => x.Key, x => x.Value.ToFrozenSet()); + } + + private async Task<SyncResponse> MergeSyncs(SyncResponse oldSync, SyncResponse newSync, Dictionary<string, TimeSpan>? trace = null) { + // var sw = Stopwatch.StartNew(); + oldSync.NextBatch = newSync.NextBatch; + + void Trace(string key, TimeSpan span) { + if (trace is not null) { + lock (trace) + trace.Add(key, span); + } + } + + var accountDataTask = Task.Run(() => { + var sw = Stopwatch.StartNew(); + oldSync.AccountData = MergeEventList(oldSync.AccountData, newSync.AccountData); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: AccountData took {sw.ElapsedMilliseconds}ms"); + Trace("AccountData", sw.GetElapsedAndRestart()); + }); + + var presenceTask = Task.Run(() => { + var sw = Stopwatch.StartNew(); + oldSync.Presence = MergeEventListBy(oldSync.Presence, newSync.Presence, + static (oldState, newState) => oldState.Sender == newState.Sender && oldState.Type == newState.Type); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: Presence took {sw.ElapsedMilliseconds}ms"); + Trace("Presence", sw.GetElapsedAndRestart()); + }); + + { + var sw = Stopwatch.StartNew(); + // TODO: can this be cleaned up? + oldSync.DeviceOneTimeKeysCount ??= new(); + if (newSync.DeviceOneTimeKeysCount is not null) + foreach (var (key, value) in newSync.DeviceOneTimeKeysCount) + oldSync.DeviceOneTimeKeysCount[key] = value; + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: DeviceOneTimeKeysCount took {sw.ElapsedMilliseconds}ms"); + Trace("DeviceOneTimeKeysCount", sw.GetElapsedAndRestart()); + } + + var roomsTask = Task.Run(() => { + var sw = Stopwatch.StartNew(); + if (newSync.Rooms is not null) + oldSync.Rooms = MergeRoomsDataStructure(oldSync.Rooms, newSync.Rooms, Trace); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: Rooms took {sw.ElapsedMilliseconds}ms"); + Trace("Rooms", sw.GetElapsedAndRestart()); + }); + + var toDeviceTask = Task.Run(() => { + var sw = Stopwatch.StartNew(); + // oldSync.ToDevice = MergeEventList(oldSync.ToDevice, newSync.ToDevice); + oldSync.ToDevice = AppendEventList(oldSync.ToDevice, newSync.ToDevice); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: ToDevice took {sw.ElapsedMilliseconds}ms"); + Trace("ToDevice", sw.GetElapsedAndRestart()); + }); + + var deviceListsTask = Task.Run(() => { + var sw = Stopwatch.StartNew(); + oldSync.DeviceLists ??= new SyncResponse.DeviceListsDataStructure(); + oldSync.DeviceLists.Changed ??= []; + oldSync.DeviceLists.Left ??= []; + if (newSync.DeviceLists?.Changed is not null) + foreach (var s in newSync.DeviceLists.Changed!) { + oldSync.DeviceLists.Left.Remove(s); + oldSync.DeviceLists.Changed.Add(s); + } + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: DeviceLists.Changed took {sw.ElapsedMilliseconds}ms"); + Trace("DeviceLists.Changed", sw.GetElapsedAndRestart()); + + if (newSync.DeviceLists?.Left is not null) + foreach (var s in newSync.DeviceLists.Left!) { + oldSync.DeviceLists.Changed.Remove(s); + oldSync.DeviceLists.Left.Add(s); + } + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: DeviceLists.Left took {sw.ElapsedMilliseconds}ms"); + Trace("DeviceLists.Left", sw.GetElapsedAndRestart()); + }); + + await Task.WhenAll(accountDataTask, presenceTask, roomsTask, toDeviceTask, deviceListsTask); + + return oldSync; + } + +#region Merge rooms + + private SyncResponse.RoomsDataStructure MergeRoomsDataStructure(SyncResponse.RoomsDataStructure? oldState, SyncResponse.RoomsDataStructure newState, + Action<string, TimeSpan> trace) { + var sw = Stopwatch.StartNew(); + if (oldState is null) return newState; + + if (newState.Join is { Count: > 0 }) + if (oldState.Join is null) + oldState.Join = newState.Join; + else + foreach (var (key, value) in newState.Join) + if (!oldState.Join.TryAdd(key, value)) + oldState.Join[key] = MergeJoinedRoomDataStructure(oldState.Join[key], value, key, trace); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeRoomsDataStructure.Join took {sw.ElapsedMilliseconds}ms"); + trace("MergeRoomsDataStructure.Join", sw.GetElapsedAndRestart()); + + if (newState.Invite is { Count: > 0 }) + if (oldState.Invite is null) + oldState.Invite = newState.Invite; + else + foreach (var (key, value) in newState.Invite) + if (!oldState.Invite.TryAdd(key, value)) + oldState.Invite[key] = MergeInvitedRoomDataStructure(oldState.Invite[key], value, key, trace); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeRoomsDataStructure.Invite took {sw.ElapsedMilliseconds}ms"); + trace("MergeRoomsDataStructure.Invite", sw.GetElapsedAndRestart()); + + if (newState.Leave is { Count: > 0 }) + if (oldState.Leave is null) + oldState.Leave = newState.Leave; + else + foreach (var (key, value) in newState.Leave) { + if (!oldState.Leave.TryAdd(key, value)) + oldState.Leave[key] = MergeLeftRoomDataStructure(oldState.Leave[key], value, key, trace); + if (oldState.Invite?.ContainsKey(key) ?? false) oldState.Invite.Remove(key); + if (oldState.Join?.ContainsKey(key) ?? false) oldState.Join.Remove(key); + } + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeRoomsDataStructure.Leave took {sw.ElapsedMilliseconds}ms"); + trace("MergeRoomsDataStructure.Leave", sw.GetElapsedAndRestart()); + return oldState; } - private SyncResponse.RoomsDataStructure.LeftRoomDataStructure MergeLeftRoomDataStructure(SyncResponse.RoomsDataStructure.LeftRoomDataStructure oldData, - SyncResponse.RoomsDataStructure.LeftRoomDataStructure newData) { - oldData.AccountData ??= new EventList(); - oldData.AccountData.Events ??= new List<StateEventResponse>(); - oldData.Timeline ??= new SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.TimelineDataStructure(); - oldData.Timeline.Events ??= new List<StateEventResponse>(); - oldData.State ??= new EventList(); - oldData.State.Events ??= new List<StateEventResponse>(); + private static SyncResponse.RoomsDataStructure.LeftRoomDataStructure MergeLeftRoomDataStructure(SyncResponse.RoomsDataStructure.LeftRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.LeftRoomDataStructure newData, string roomId, Action<string, TimeSpan> trace) { + var sw = Stopwatch.StartNew(); - if (newData.AccountData?.Events is not null) - oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new List<StateEventResponse>()); + oldData.AccountData = MergeEventList(oldData.AccountData, newData.AccountData); + trace($"LeftRoomDataStructure.AccountData/{roomId}", sw.GetElapsedAndRestart()); - if (newData.Timeline?.Events is not null) - oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new List<StateEventResponse>()); + oldData.Timeline = AppendEventList(oldData.Timeline, newData.Timeline) as SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.TimelineDataStructure + ?? throw new InvalidOperationException("Merged room timeline was not TimelineDataStructure"); oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited; oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch; + trace($"LeftRoomDataStructure.Timeline/{roomId}", sw.GetElapsedAndRestart()); - if (newData.State?.Events is not null) - oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new List<StateEventResponse>()); + oldData.State = MergeEventList(oldData.State, newData.State); + trace($"LeftRoomDataStructure.State/{roomId}", sw.GetElapsedAndRestart()); return oldData; } - private SyncResponse.RoomsDataStructure.InvitedRoomDataStructure MergeInvitedRoomDataStructure(SyncResponse.RoomsDataStructure.InvitedRoomDataStructure oldData, - SyncResponse.RoomsDataStructure.InvitedRoomDataStructure newData) { - oldData.InviteState ??= new EventList(); - oldData.InviteState.Events ??= new List<StateEventResponse>(); - if (newData.InviteState?.Events is not null) - oldData.InviteState.Events.MergeStateEventLists(newData.InviteState?.Events ?? new List<StateEventResponse>()); + private static SyncResponse.RoomsDataStructure.InvitedRoomDataStructure MergeInvitedRoomDataStructure(SyncResponse.RoomsDataStructure.InvitedRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.InvitedRoomDataStructure newData, string roomId, Action<string, TimeSpan> trace) { + var sw = Stopwatch.StartNew(); + oldData.InviteState = MergeEventList(oldData.InviteState, newData.InviteState); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeInvitedRoomDataStructure.InviteState took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"InvitedRoomDataStructure.InviteState/{roomId}", sw.GetElapsedAndRestart()); return oldData; } - private SyncResponse.RoomsDataStructure.JoinedRoomDataStructure MergeJoinedRoomDataStructure(SyncResponse.RoomsDataStructure.JoinedRoomDataStructure oldData, - SyncResponse.RoomsDataStructure.JoinedRoomDataStructure newData) { - oldData.AccountData ??= new EventList(); - oldData.AccountData.Events ??= new List<StateEventResponse>(); - oldData.Timeline ??= new SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.TimelineDataStructure(); - oldData.Timeline.Events ??= new List<StateEventResponse>(); - oldData.State ??= new EventList(); - oldData.State.Events ??= new List<StateEventResponse>(); - oldData.Ephemeral ??= new EventList(); - oldData.Ephemeral.Events ??= new List<StateEventResponse>(); + private static SyncResponse.RoomsDataStructure.JoinedRoomDataStructure MergeJoinedRoomDataStructure(SyncResponse.RoomsDataStructure.JoinedRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.JoinedRoomDataStructure newData, string roomId, Action<string, TimeSpan> trace) { + var sw = Stopwatch.StartNew(); + + oldData.AccountData = MergeEventList(oldData.AccountData, newData.AccountData); - if (newData.AccountData?.Events is not null) - oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new List<StateEventResponse>()); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.AccountData took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoomDataStructure.AccountData/{roomId}", sw.GetElapsedAndRestart()); - if (newData.Timeline?.Events is not null) - oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new List<StateEventResponse>()); + oldData.Timeline = AppendEventList(oldData.Timeline, newData.Timeline) as SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.TimelineDataStructure + ?? throw new InvalidOperationException("Merged room timeline was not TimelineDataStructure"); oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited; oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch; - if (newData.State?.Events is not null) - oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new List<StateEventResponse>()); + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.Timeline took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoomDataStructure.Timeline/{roomId}", sw.GetElapsedAndRestart()); - if (newData.Ephemeral?.Events is not null) - oldData.Ephemeral.Events.MergeStateEventLists(newData.Ephemeral?.Events ?? new List<StateEventResponse>()); + oldData.State = MergeEventList(oldData.State, newData.State); + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.State took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoomDataStructure.State/{roomId}", sw.GetElapsedAndRestart()); + + oldData.Ephemeral = MergeEventList(oldData.Ephemeral, newData.Ephemeral); + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.Ephemeral took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoomDataStructure.Ephemeral/{roomId}", sw.GetElapsedAndRestart()); oldData.UnreadNotifications ??= new SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.UnreadNotificationsDataStructure(); oldData.UnreadNotifications.HighlightCount = newData.UnreadNotifications?.HighlightCount ?? oldData.UnreadNotifications.HighlightCount; oldData.UnreadNotifications.NotificationCount = newData.UnreadNotifications?.NotificationCount ?? oldData.UnreadNotifications.NotificationCount; - oldData.Summary ??= new SyncResponse.RoomsDataStructure.JoinedRoomDataStructure.SummaryDataStructure { - Heroes = newData.Summary?.Heroes ?? oldData.Summary.Heroes, - JoinedMemberCount = newData.Summary?.JoinedMemberCount ?? oldData.Summary.JoinedMemberCount, - InvitedMemberCount = newData.Summary?.InvitedMemberCount ?? oldData.Summary.InvitedMemberCount - }; - oldData.Summary.Heroes = newData.Summary?.Heroes ?? oldData.Summary.Heroes; - oldData.Summary.JoinedMemberCount = newData.Summary?.JoinedMemberCount ?? oldData.Summary.JoinedMemberCount; - oldData.Summary.InvitedMemberCount = newData.Summary?.InvitedMemberCount ?? oldData.Summary.InvitedMemberCount; + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.UnreadNotifications took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoom$DataStructure.UnreadNotifications/{roomId}", sw.GetElapsedAndRestart()); + + if (oldData.Summary is null) + oldData.Summary = newData.Summary; + else { + oldData.Summary.Heroes = newData.Summary?.Heroes ?? oldData.Summary.Heroes; + oldData.Summary.JoinedMemberCount = newData.Summary?.JoinedMemberCount ?? oldData.Summary.JoinedMemberCount; + oldData.Summary.InvitedMemberCount = newData.Summary?.InvitedMemberCount ?? oldData.Summary.InvitedMemberCount; + } + + if (sw.ElapsedMilliseconds > 100) Console.WriteLine($"WARN: MergeJoinedRoomDataStructure.Summary took {sw.ElapsedMilliseconds}ms for {roomId}"); + trace($"JoinedRoomDataStructure.Summary/{roomId}", sw.GetElapsedAndRestart()); return oldData; } #endregion + + private static EventList? MergeEventList(EventList? oldState, EventList? newState) { + if (newState is null) return oldState; + if (oldState is null) { + return newState; + } + + if (newState.Events is null) return oldState; + if (oldState.Events is null) { + oldState.Events = newState.Events; + return oldState; + } + + // oldState.Events.MergeStateEventLists(newState.Events); + oldState = MergeEventListBy(oldState, newState, static (oldEvt, newEvt) => oldEvt.Type == newEvt.Type && oldEvt.StateKey == newEvt.StateKey); + return oldState; + } + + private static EventList? MergeEventListBy(EventList? oldState, EventList? newState, Func<StateEventResponse, StateEventResponse, bool> comparer) { + if (newState is null) return oldState; + if (oldState is null) { + return newState; + } + + if (newState.Events is null) return oldState; + if (oldState.Events is null) { + oldState.Events = newState.Events; + return oldState; + } + + oldState.Events.ReplaceBy(newState.Events, comparer); + return oldState; + } + + private static EventList? AppendEventList(EventList? oldState, EventList? newState) { + if (newState is null) return oldState; + if (oldState is null) { + return newState; + } + + if (newState.Events is null) return oldState; + if (oldState.Events is null) { + oldState.Events = newState.Events; + return oldState; + } + + oldState.Events.AddRange(newState.Events); + return oldState; + } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 6be49b9..5fd3311 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -5,8 +5,8 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Extensions; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Filters; using LibMatrix.Helpers; using LibMatrix.Homeservers.Extensions.NamedCaches; @@ -14,7 +14,6 @@ using LibMatrix.Responses; using LibMatrix.RoomTypes; using LibMatrix.Services; using LibMatrix.Utilities; -using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Homeservers; @@ -41,14 +40,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { public string UserId => WhoAmI.UserId; public string UserLocalpart => UserId.Split(":")[0][1..]; public string ServerName => UserId.Split(":", 2)[1]; + public string BaseUrl => ClientHttpClient.BaseAddress!.ToString().TrimEnd('/'); [JsonIgnore] public string AccessToken { get; set; } - public HsNamedCaches NamedCaches { get; set; } = null!; + public HsNamedCaches NamedCaches { get; set; } public GenericRoom GetRoom(string roomId) { - if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId)); return new GenericRoom(this, roomId); } @@ -171,8 +170,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { try { return await GetAccountDataAsync<T>(key); } - catch (Exception e) { - return default; + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return default; + throw; } } @@ -186,6 +186,16 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { #endregion +#region MSC 4133 + + public async Task UpdateProfilePropertyAsync(string name, object? value) { + var caps = await GetCapabilitiesAsync(); + if (caps is null) throw new Exception("Failed to get capabilities"); + } + +#endregion + + [Obsolete("This method assumes no support for MSC 4069 and MSC 4133")] public async Task UpdateProfileAsync(UserProfileResponse? newProfile, bool preserveCustomRoomProfile = true) { if (newProfile is null) return; Console.WriteLine($"Updating profile for {WhoAmI.UserId} to {newProfile.ToJson(ignoreNull: true)} (preserving room profiles: {preserveCustomRoomProfile})"); @@ -377,11 +387,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { /// <returns>All account data.</returns> /// <exception cref="Exception"></exception> public async Task<EventList?> EnumerateAccountData() { - var syncHelper = new SyncHelper(this); - syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData); + var syncHelper = new SyncHelper(this) { + FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData) + }; var resp = await syncHelper.SyncAsync(); - if (resp is null) throw new Exception("Sync failed"); - return resp.AccountData; + return resp?.AccountData ?? throw new Exception("Sync failed"); } private Dictionary<string, string>? _namedFilterCache; @@ -410,22 +420,22 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { #region Authenticated Media // TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig + private bool? _serverSupportsAuthMedia; - private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) { - if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri)); - var parts = mxcUri[6..].Split('/'); - if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri)); - return (parts[0], parts[1]); - } + public async Task<string> GetMediaUrlAsync(MxcUri mxcUri, string? filename = null, int? timeout = null) { + if (_serverSupportsAuthMedia == true) return mxcUri.ToDownloadUri(BaseUrl, filename, timeout); + if (_serverSupportsAuthMedia == false) return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout); - public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) { - var (serverName, mediaId) = ParseMxcUri(mxcUri); try { - var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}"; - if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}"; - if (timeout is not null) uri += $"?timeout_ms={timeout}"; - var res = await ClientHttpClient.GetAsync(uri); - return await res.Content.ReadAsStreamAsync(); + // Console.WriteLine($"Trying authenticated media URL: {uri}"); + var res = await ClientHttpClient.SendAsync(new() { + Method = HttpMethod.Head, + RequestUri = (new Uri(mxcUri.ToDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute)) + }); + if (res.IsSuccessStatusCode) { + _serverSupportsAuthMedia = true; + return mxcUri.ToDownloadUri(BaseUrl, filename, timeout); + } } catch (MatrixException e) { if (e is not { ErrorCode: "M_UNKNOWN" }) throw; @@ -433,11 +443,15 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { //fallback to legacy media try { - var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}"; - if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}"; - if (timeout is not null) uri += $"?timeout_ms={timeout}"; - var res = await ClientHttpClient.GetAsync(uri); - return await res.Content.ReadAsStreamAsync(); + // Console.WriteLine($"Trying legacy media URL: {uri}"); + var res = await ClientHttpClient.SendAsync(new() { + Method = HttpMethod.Head, + RequestUri = new(mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute) + }); + if (res.IsSuccessStatusCode) { + _serverSupportsAuthMedia = false; + return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout); + } } catch (MatrixException e) { if (e is not { ErrorCode: "M_UNKNOWN" }) throw; @@ -445,19 +459,25 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { throw new LibMatrixException() { ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, - Error = "Failed to download media" + Error = "Failed to get media URL" }; - // return default; } - public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) { - var (serverName, mediaId) = ParseMxcUri(mxcUri); + public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) { + var uri = await GetMediaUrlAsync(mxcUri, filename, timeout); + var res = await ClientHttpClient.GetAsync(uri); + return await res.Content.ReadAsStreamAsync(); + } + + public async Task<Stream> GetThumbnailStreamAsync(MxcUri mxcUri, int width, int height, string? method = null, int? timeout = null) { + var (serverName, mediaId) = mxcUri; + try { var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}"); uri = uri.AddQuery("width", width.ToString()); uri = uri.AddQuery("height", height.ToString()); if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method); - if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()); + if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()!); var res = await ClientHttpClient.GetAsync(uri.ToString()); return await res.Content.ReadAsStreamAsync(); @@ -468,11 +488,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { //fallback to legacy media try { - var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}"); + var uri = new Uri($"/_matrix/media/v3/thumbnail/{serverName}/{mediaId}"); uri = uri.AddQuery("width", width.ToString()); uri = uri.AddQuery("height", height.ToString()); if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method); - if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()); + if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()!); var res = await ClientHttpClient.GetAsync(uri.ToString()); return await res.Content.ReadAsStreamAsync(); @@ -496,16 +516,16 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { catch (MatrixException e) { if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw; } - + //fallback to legacy media try { - var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}"); + var res = await ClientHttpClient.GetAsync($"/_matrix/media/v3/preview_url?url={HttpUtility.UrlEncode(url)}"); return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>(); } catch (MatrixException e) { if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw; } - + throw new LibMatrixException() { ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED, Error = "Failed to download URL preview" @@ -513,4 +533,86 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { } #endregion + + public Task ReportRoomAsync(string roomId, string reason) => + ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report", new { + reason + }); + + public async Task ReportRoomEventAsync(string roomId, string eventId, string reason, int score = 0, bool ignoreSender = false) { + await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report/{eventId}", new { + reason, + score + }); + + if (ignoreSender) { + var eventContent = await GetRoom(roomId).GetEventAsync(eventId); + var sender = eventContent.Sender; + await IgnoreUserAsync(sender); + } + } + + public async Task ReportUserAsync(string userId, string reason, bool ignore = false) { + await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/users/{userId}/report", new { + reason + }); + + if (ignore) { + await IgnoreUserAsync(userId); + } + } + + public async Task<IgnoredUserListEventContent> GetIgnoredUserListAsync() { + return await GetAccountDataOrNullAsync<IgnoredUserListEventContent>(IgnoredUserListEventContent.EventId) ?? new(); + } + + public async Task IgnoreUserAsync(string userId, IgnoredUserListEventContent.IgnoredUserContent? content = null) { + content ??= new(); + + var ignoredUserList = await GetIgnoredUserListAsync(); + ignoredUserList.IgnoredUsers.TryAdd(userId, content); + await SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoredUserList); + } + + private class CapabilitiesResponse { + [JsonPropertyName("capabilities")] + public Dictionary<string, object>? Capabilities { get; set; } + } + +#region Room Directory/aliases + + public async Task SetRoomAliasAsync(string roomAlias, string roomId) { + var resp = await ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/directory/room/{HttpUtility.UrlEncode(roomAlias)}", new RoomIdResponse() { + RoomId = roomId + }); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + } + } + + public async Task DeleteRoomAliasAsync(string roomAlias) { + var resp = await ClientHttpClient.DeleteAsync("/_matrix/client/v3/directory/room/" + HttpUtility.UrlEncode(roomAlias)); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + } + } + + public async Task<RoomAliasesResponse> GetLocalRoomAliasesAsync(string roomId) { + var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{HttpUtility.UrlEncode(roomId)}/aliases"); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}"); + } + + return await resp.Content.ReadFromJsonAsync<RoomAliasesResponse>() ?? throw new Exception("Failed to get room aliases?"); + } + + public class RoomAliasesResponse { + [JsonPropertyName("aliases")] + public required List<string> Aliases { get; set; } + } + +#endregion } \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs new file mode 100644
index 0000000..1cc8ca2 --- /dev/null +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs
@@ -0,0 +1,16 @@ +using LibMatrix.Homeservers.ImplementationDetails.Synapse; +using LibMatrix.Responses; +using LibMatrix.Services; + +namespace LibMatrix.Homeservers; + +public class AuthenticatedHomeserverHSE : AuthenticatedHomeserverGeneric { + public AuthenticatedHomeserverHSE(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy, string accessToken) : base(serverName, + wellKnownUris, proxy, accessToken) { } + + public Task<Dictionary<string, LoginResponse>> GetExternalProfilesAsync() => + ClientHttpClient.GetFromJsonAsync<Dictionary<string, LoginResponse>>("/_hse/client/v1/external_profiles"); + + public Task SetExternalProfile(string sessionName, LoginResponse session) => + ClientHttpClient.PutAsJsonAsync($"/_hse/client/v1/external_profiles/{sessionName}", session); +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
index 83ebf20..1c0a656 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
@@ -1,7 +1,4 @@ -using ArcaneLibs.Extensions; -using LibMatrix.Filters; using LibMatrix.Homeservers.ImplementationDetails.Synapse; -using LibMatrix.Responses.Admin; using LibMatrix.Services; namespace LibMatrix.Homeservers; diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
index 622eef6..1f62637 100644 --- a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs +++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
@@ -3,35 +3,72 @@ namespace LibMatrix.Homeservers.Extensions.NamedCaches; public class NamedCache<T>(AuthenticatedHomeserverGeneric hs, string name) where T : class { private Dictionary<string, T>? _cache = new(); private DateTime _expiry = DateTime.MinValue; - + private SemaphoreSlim _lock = new(1, 1); + + public TimeSpan ExpiryTime { get; set; } = TimeSpan.FromMinutes(5); + public DateTime GetCurrentExpiryTime() => _expiry; + + /// <summary> + /// Update the cached map with the latest data from the homeserver. + /// </summary> + /// <returns>The updated data</returns> public async Task<Dictionary<string, T>> ReadCacheMapAsync() { - _cache = await hs.GetAccountDataOrNullAsync<Dictionary<string, T>>(name); + try { + _cache = await hs.GetAccountDataAsync<Dictionary<string, T>>(name); + } + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) + _cache = []; + else throw; + } return _cache ?? new(); } - - public async Task<Dictionary<string,T>> ReadCacheMapCachedAsync() { + + public async Task<Dictionary<string, T>> ReadCacheMapCachedAsync() { + await _lock.WaitAsync(); if (_expiry < DateTime.Now || _cache == null) { _cache = await ReadCacheMapAsync(); - _expiry = DateTime.Now.AddMinutes(5); + _expiry = DateTime.Now.Add(ExpiryTime); } + _lock.Release(); + return _cache; } - - public virtual async Task<T?> GetValueAsync(string key) { - return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key); + + public virtual async Task<T?> GetValueAsync(string key, bool useCache = true) { + return (await (useCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync())).GetValueOrDefault(key); } - - public virtual async Task<T> SetValueAsync(string key, T value) { - var cache = await ReadCacheMapCachedAsync(); + + public virtual async Task<T> SetValueAsync(string key, T value, bool unsafeUseCache = false) { + if (!unsafeUseCache) + await _lock.WaitAsync(); + var cache = await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync()); cache[key] = value; await hs.SetAccountDataAsync(name, cache); + if (!unsafeUseCache) + _lock.Release(); + return value; } - - public virtual async Task<T> GetOrSetValueAsync(string key, Func<Task<T>> value) { - return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key) ?? await SetValueAsync(key, await value()); + + public virtual async Task<T> RemoveValueAsync(string key, bool unsafeUseCache = false) { + if (!unsafeUseCache) + await _lock.WaitAsync(); + var cache = await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync()); + var removedValue = cache[key]; + cache.Remove(key); + await hs.SetAccountDataAsync(name, cache); + + if (!unsafeUseCache) + _lock.Release(); + + return removedValue; + } + + public virtual async Task<T> GetOrSetValueAsync(string key, Func<Task<T>> value, bool unsafeUseCache = false) { + return (await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync())).GetValueOrDefault(key) ?? await SetValueAsync(key, await value()); } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
index 76533a4..e3c5943 100644 --- a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs +++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
@@ -1,3 +1,5 @@ +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; using LibMatrix.Filters; using LibMatrix.Utilities; @@ -16,7 +18,13 @@ public class NamedFilterCache(AuthenticatedHomeserverGeneric hs) : NamedCache<st public async Task<string> GetOrSetValueAsync(string key, SyncFilter? filter = null) { var existingValue = await GetValueAsync(key); if (existingValue != null) { - return existingValue; + try { + var existingFilter = await hs.GetFilterAsync(existingValue); + return existingValue; + } + catch { + // ignored + } } if (filter is null) { diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index 22653e4..a2cb12d 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using LibMatrix.Extensions; using LibMatrix.Services; -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualBasic.CompilerServices; namespace LibMatrix.Homeservers; @@ -14,10 +14,74 @@ public class FederationClient { if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint); } - public MatrixHttpClient HttpClient { get; set; } = null!; - public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } = null!; + public MatrixHttpClient HttpClient { get; set; } + public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } public async Task<ServerVersionResponse> GetServerVersionAsync() => await HttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version"); + public async Task<ServerKeysResponse> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<ServerKeysResponse>("/_matrix/key/v2/server"); +} + +public class ServerKeysResponse { + [JsonPropertyName("server_name")] + public string ServerName { get; set; } + + [JsonPropertyName("valid_until_ts")] + public ulong ValidUntilTs { get; set; } + + [JsonIgnore] + public DateTime ValidUntil { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime; + set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [JsonPropertyName("verify_keys")] + public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById { + get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + [JsonPropertyName("old_verify_keys")] + public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById { + get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + public class VersionedKeyId { + public required string Algorithm { get; set; } + public required string KeyId { get; set; } + + public static implicit operator VersionedKeyId(string key) { + var parts = key.Split(':', 2); + if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key)); + return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] }; + } + + public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}"; + public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId); + public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId); + } + + public class CurrentVerifyKey { + [JsonPropertyName("key")] + public string Key { get; set; } + } + + public class ExpiredVerifyKey : CurrentVerifyKey { + [JsonPropertyName("expired_ts")] + public ulong ExpiredTs { get; set; } + + [JsonIgnore] + public DateTime Expired { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime; + set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + } } public class ServerVersionResponse { diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs new file mode 100644
index 0000000..c34ad7c --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs
@@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; + +public class SynapseAdminLocalEventReportQueryFilter { + public string UserIdContains { get; set; } = ""; + public string NameContains { get; set; } = ""; + public string CanonicalAliasContains { get; set; } = ""; + public string VersionContains { get; set; } = ""; + public string CreatorContains { get; set; } = ""; + public string EncryptionContains { get; set; } = ""; + public string JoinRulesContains { get; set; } = ""; + public string GuestAccessContains { get; set; } = ""; + public string HistoryVisibilityContains { get; set; } = ""; + + public bool Federatable { get; set; } = true; + public bool Public { get; set; } = true; + + public int JoinedMembersGreaterThan { get; set; } + public int JoinedMembersLessThan { get; set; } = int.MaxValue; + + public int JoinedLocalMembersGreaterThan { get; set; } + public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue; + public int StateEventsGreaterThan { get; set; } + public int StateEventsLessThan { get; set; } = int.MaxValue; + + public bool CheckFederation { get; set; } + public bool CheckPublic { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Filters/LocalRoomQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs
index b3bd4c0..b8929a0 100644 --- a/LibMatrix/Filters/LocalRoomQueryFilter.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs
@@ -1,6 +1,6 @@ -namespace LibMatrix.Filters; +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; -public class LocalRoomQueryFilter { +public class SynapseAdminLocalRoomQueryFilter { public string RoomIdContains { get; set; } = ""; public string NameContains { get; set; } = ""; public string CanonicalAliasContains { get; set; } = ""; diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs new file mode 100644
index 0000000..5a4acf7 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
@@ -0,0 +1,5 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; + +public class SynapseAdminLocalUserQueryFilter { + +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs deleted file mode 100644
index f4c927a..0000000 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs +++ /dev/null
@@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; - -public class AdminRoomDeleteRequest { - [JsonPropertyName("new_room_user_id")] - public string? NewRoomUserId { get; set; } - - [JsonPropertyName("room_name")] - public string? RoomName { get; set; } - - [JsonPropertyName("block")] - public bool Block { get; set; } - - [JsonPropertyName("purge")] - public bool Purge { get; set; } - - [JsonPropertyName("message")] - public string? Message { get; set; } - - [JsonPropertyName("force_purge")] - public bool ForcePurge { get; set; } -} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs new file mode 100644
index 0000000..197fd5d --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs
@@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRegistrationTokenUpdateRequest { + [JsonPropertyName("uses_allowed")] + public int? UsesAllowed { get; set; } + + [JsonPropertyName("expiry_time")] + public long? ExpiryTime { get; set; } + + [JsonIgnore] + public DateTime? ExpiresAt { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime : null; + set => ExpiryTime = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + + [JsonIgnore] + public TimeSpan? ExpiresAfter { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime - DateTimeOffset.Now : null; + set => ExpiryTime = value.HasValue ? (DateTimeOffset.Now + value.Value).ToUnixTimeMilliseconds() : null; + } +} + +public class SynapseAdminRegistrationTokenCreateRequest : SynapseAdminRegistrationTokenUpdateRequest { + [JsonPropertyName("token")] + public string? Token { get; set; } + + [JsonPropertyName("length")] + public int? Length { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs new file mode 100644
index 0000000..c2b2fac --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs
@@ -0,0 +1,60 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; + +public class SynapseAdminRoomDeleteRequest { + [JsonPropertyName("new_room_user_id")] + public string? NewRoomUserId { get; set; } + + [JsonPropertyName("room_name")] + public string? RoomName { get; set; } + + [JsonPropertyName("block")] + public bool Block { get; set; } + + [JsonPropertyName("purge")] + public bool Purge { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("force_purge")] + public bool ForcePurge { get; set; } +} + +public class SynapseAdminRoomDeleteResponse { + [JsonPropertyName("delete_id")] + public string DeleteId { get; set; } = null!; +} + +public class SynapseAdminRoomDeleteStatusList { + [JsonPropertyName("results")] + public List<SynapseAdminRoomDeleteStatus> Results { get; set; } +} + +public class SynapseAdminRoomDeleteStatus { + public const string Scheduled = "scheduled"; + public const string Active = "active"; + public const string Complete = "complete"; + public const string Failed = "failed"; + + [JsonPropertyName("status")] + public string Status { get; set; } = null!; + + [JsonPropertyName("shutdown_room")] + public RoomShutdownInfo ShutdownRoom { get; set; } + + public class RoomShutdownInfo { + [JsonPropertyName("kicked_users")] + public List<string>? KickedUsers { get; set; } + + [JsonPropertyName("failed_to_kick_users")] + public List<string>? FailedToKickUsers { get; set; } + + [JsonPropertyName("local_aliases")] + public List<string>? LocalAliasses { get; set; } + + [JsonPropertyName("new_room_id")] + public string? NewRoomId { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs new file mode 100644
index 0000000..2394b98 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs
@@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminBackgroundUpdateStatusResponse { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("current_updates")] + public Dictionary<string, BackgroundUpdateInfo> CurrentUpdates { get; set; } + + public class BackgroundUpdateInfo { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("total_item_count")] + public int TotalItemCount { get; set; } + + [JsonPropertyName("total_duration_ms")] + public double TotalDurationMs { get; set; } + + [JsonPropertyName("average_items_per_ms")] + public double AverageItemsPerMs { get; set; } + + [JsonIgnore] + public TimeSpan TotalDuration => TimeSpan.FromMilliseconds(TotalDurationMs); + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs new file mode 100644
index 0000000..646a4b5 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs
@@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminDestinationListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("destinations")] + public List<SynapseAdminDestinationListResultDestination> Destinations { get; set; } = new(); + + public class SynapseAdminDestinationListResultDestination { + [JsonPropertyName("destination")] + public string Destination { get; set; } + + [JsonPropertyName("retry_last_ts")] + public long RetryLastTs { get; set; } + + [JsonPropertyName("retry_interval")] + public long RetryInterval { get; set; } + + [JsonPropertyName("failure_ts")] + public long? FailureTs { get; set; } + + [JsonPropertyName("last_successful_stream_ordering")] + public long? LastSuccessfulStreamOrdering { get; set; } + + [JsonIgnore] + public DateTime? FailureTsDateTime { + get => FailureTs.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(FailureTs.Value).DateTime : null; + set => FailureTs = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + + [JsonIgnore] + public DateTime? RetryLastTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(RetryLastTs).DateTime; + set => RetryLastTs = new DateTimeOffset(value.Value).ToUnixTimeMilliseconds(); + } + + [JsonIgnore] + public TimeSpan RetryIntervalTimeSpan { + get => TimeSpan.FromMilliseconds(RetryInterval); + set => RetryInterval = (long)value.TotalMilliseconds; + } + } +} + +public class SynapseAdminDestinationRoomListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("rooms")] + public List<SynapseAdminDestinationRoomListResultRoom> Rooms { get; set; } = new(); + + public class SynapseAdminDestinationRoomListResultRoom { + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("stream_ordering")] + public int StreamOrdering { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs new file mode 100644
index 0000000..10fc039 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs
@@ -0,0 +1,169 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs; +using ArcaneLibs.Attributes; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes; +using LibMatrix.Extensions; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminEventReportListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("event_reports")] + public List<SynapseAdminEventReportListResultReport> Reports { get; set; } = new(); + + public class SynapseAdminEventReportListResultReport { + [JsonPropertyName("event_id")] + public string EventId { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("score")] + public int? Score { get; set; } + + [JsonPropertyName("received_ts")] + public long ReceivedTs { get; set; } + + [JsonPropertyName("canonical_alias")] + public string? CanonicalAlias { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonIgnore] + public DateTime ReceivedTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(ReceivedTs).DateTime; + set => ReceivedTs = new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + } + + public class SynapseAdminEventReportListResultReportWithDetails : SynapseAdminEventReportListResultReport { + [JsonPropertyName("event_json")] + public SynapseEventJson EventJson { get; set; } + + public class SynapseEventJson { + [JsonPropertyName("auth_events")] + public List<string> AuthEvents { get; set; } + + [JsonPropertyName("content")] + public JsonObject? RawContent { get; set; } + + [JsonPropertyName("depth")] + public int Depth { get; set; } + + [JsonPropertyName("hashes")] + public Dictionary<string, string> Hashes { get; set; } + + [JsonPropertyName("origin")] + public string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public long OriginServerTs { get; set; } + + [JsonPropertyName("prev_events")] + public List<string> PrevEvents { get; set; } + + [JsonPropertyName("prev_state")] + public List<object> PrevState { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("signatures")] + public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("unsigned")] + public JsonObject? Unsigned { get; set; } + + // Extra... copied from StateEventResponse + + [JsonIgnore] + public Type MappedType => StateEvent.GetStateEventType(Type); + + [JsonIgnore] + public bool IsLegacyType => MappedType.GetCustomAttributes<MatrixEventAttribute>().FirstOrDefault(x => x.EventName == Type)?.Legacy ?? false; + + [JsonIgnore] + public string FriendlyTypeName => MappedType.GetFriendlyNameOrNull() ?? Type; + + [JsonIgnore] + public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type; + + private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() { + Converters = { + new JsonFloatStringConverter(), + new JsonDoubleStringConverter(), + new JsonDecimalStringConverter() + } + }; + + [JsonIgnore] + [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] + public EventContent? TypedContent { + get { + ClassCollector<EventContent>.ResolveFromAllAccessibleAssemblies(); + // if (Type == "m.receipt") { + // return null; + // } + try { + var mappedType = StateEvent.GetStateEventType(Type); + if (mappedType == typeof(UnknownEventContent)) + Console.WriteLine($"Warning: unknown event type '{Type}'"); + var deserialisedContent = (EventContent)RawContent.Deserialize(mappedType, TypedContentSerializerOptions)!; + return deserialisedContent; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null")); + } + + return null; + } + set { + if (value is null) + RawContent?.Clear(); + else + RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value, value.GetType(), + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + } + } + + //debug + [JsonIgnore] + public string InternalSelfTypeName { + get { + var res = GetType().Name switch { + "StateEvent`1" => "StateEvent", + _ => GetType().Name + }; + return res; + } + } + + [JsonIgnore] + public string InternalContentTypeName => TypedContent?.GetType().Name ?? "null"; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs new file mode 100644
index 0000000..fa92ef9 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs
@@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRegistrationTokenListResult { + [JsonPropertyName("registration_tokens")] + public List<SynapseAdminRegistrationTokenListResultToken> RegistrationTokens { get; set; } = new(); + + public class SynapseAdminRegistrationTokenListResultToken { + [JsonPropertyName("token")] + public string Token { get; set; } + + [JsonPropertyName("uses_allowed")] + public int? UsesAllowed { get; set; } + + [JsonPropertyName("pending")] + public int Pending { get; set; } + + [JsonPropertyName("completed")] + public int Completed { get; set; } + + [JsonPropertyName("expiry_time")] + public long? ExpiryTime { get; set; } + + [JsonIgnore] + public DateTime? ExpiryTimeDateTime { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime : null; + set => ExpiryTime = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
index 7ab96ac..d84c89b 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
@@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace LibMatrix.Responses.Admin; +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; -public class AdminRoomListingResult { +public class SynapseAdminRoomListResult { [JsonPropertyName("offset")] public int Offset { get; set; } @@ -16,9 +16,9 @@ public class AdminRoomListingResult { public int? PrevBatch { get; set; } [JsonPropertyName("rooms")] - public List<AdminRoomListingResultRoom> Rooms { get; set; } = new(); + public List<SynapseAdminRoomListResultRoom> Rooms { get; set; } = new(); - public class AdminRoomListingResultRoom { + public class SynapseAdminRoomListResultRoom { [JsonPropertyName("room_id")] public required string RoomId { get; set; } diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs new file mode 100644
index 0000000..97e85ad --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs
@@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomMediaListResult { + [JsonPropertyName("local")] + public List<string> Local { get; set; } = new(); + + [JsonPropertyName("remote")] + public List<string> Remote { get; set; } = new(); +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs new file mode 100644
index 0000000..cb2ec08 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs
@@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomMemberListResult { + [JsonPropertyName("members")] + public List<string> Members { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs new file mode 100644
index 0000000..ae36d4e --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs
@@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomStateResult { + [JsonPropertyName("state")] + public required List<StateEventResponse> Events { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs new file mode 100644
index 0000000..3f5f865 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs
@@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserRedactIdResponse { + [JsonPropertyName("redact_id")] + public string RedactionId { get; set; } +} + +public class SynapseAdminRedactStatusResponse { + /// <summary> + /// One of "scheduled", "active", "completed", "failed" + /// </summary> + [JsonPropertyName("status")] + public string Status { get; set; } + + /// <summary> + /// Key: Event ID, Value: Error message + /// </summary> + [JsonPropertyName("failed_redactions")] + public Dictionary<string, string> FailedRedactions { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs new file mode 100644
index 0000000..36a5596 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs
@@ -0,0 +1,250 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } +} + +// [JsonConverter(typeof(SynapseCollectionJsonConverter<>))] +public class SynapseCollectionResult<T>(string chunkKey = "chunk", string prevTokenKey = "prev_token", string nextTokenKey = "next_token", string totalKey = "total") { + public int? Total { get; set; } + public string? PrevToken { get; set; } + public string? NextToken { get; set; } + public List<T> Chunk { get; set; } = []; + + // TODO: figure out how to provide an IAsyncEnumerable<T> for this + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-utf8jsonreader#read-from-a-stream-using-utf8jsonreader + + // public async IAsyncEnumerable<T> FromJsonAsync(Stream stream) { + // + // } + + public SynapseCollectionResult<T> FromJson(Stream stream, Action<T> action) { + byte[] buffer = new byte[4096]; + _ = stream.Read(buffer); + var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default); + + try { + FromJsonInternal(stream, ref buffer, ref reader, action); + } + catch (JsonException e) { + Console.WriteLine($"Caught a JsonException: {e}"); + int hexdumpWidth = 64; + Console.WriteLine($"Check hexdump line {reader.BytesConsumed / hexdumpWidth} index {reader.BytesConsumed % hexdumpWidth}"); + buffer.HexDump(64); + } + finally { } + + return this; + } + + private void FromJsonInternal(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader, Action<T> action) { + while (!reader.IsFinalBlock) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + if (reader.TokenType == JsonTokenType.PropertyName) { + var propName = reader.GetString(); + Console.WriteLine($"SynapseCollectionResult: encountered property name: {propName}"); + + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + Console.WriteLine($"{reader.BytesConsumed}/{stream.Position} {reader.TokenType}"); + + if (propName == totalKey && reader.TokenType == JsonTokenType.Number) { + Total = reader.GetInt32(); + } + else if (propName == prevTokenKey && reader.TokenType == JsonTokenType.String) { + PrevToken = reader.GetString(); + } + else if (propName == nextTokenKey && reader.TokenType == JsonTokenType.String) { + NextToken = reader.GetString(); + } + else if (propName == chunkKey) { + if (reader.TokenType == JsonTokenType.StartArray) { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { + // if (reader.TokenType == JsonTokenType.EndArray) { + // break; + // } + // Console.WriteLine($"Encountered token in chunk: {reader.TokenType}"); + // var _buf = reader.ValueSequence.ToArray(); + // try { + // var item = JsonSerializer.Deserialize<T>(_buf); + // action(item); + // Chunk.Add(item); + // } + // catch(JsonException e) { + // Console.WriteLine($"Caught a JsonException: {e}"); + // int hexdumpWidth = 64; + // + // // Console.WriteLine($"Check hexdump line {reader.BytesConsumed / hexdumpWidth} index {reader.BytesConsumed % hexdumpWidth}"); + // Console.WriteLine($"Buffer length: {_buf.Length}"); + // _buf.HexDump(64); + // throw; + // } + var item = ReadItem(stream, ref buffer, ref reader); + action(item); + Chunk.Add(item); + } + } + } + } + } + } + + private T ReadItem(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + // handle nullable types + if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>)) { + if (reader.TokenType == JsonTokenType.Null) { + return default(T); + } + } + + // if(typeof(T) == typeof(string)) { + // return (T)(object)reader.GetString(); + // } + // else if(typeof(T) == typeof(int)) { + // return (T)(object)reader.GetInt32(); + // } + // else { + // var _buf = reader.ValueSequence.ToArray(); + // return JsonSerializer.Deserialize<T>(_buf); + // } + + // default branch uses "object?" cast to avoid compiler error + // add more branches here as nessesary + // reader.Read(); + var call = typeof(T) switch { + Type t when t == typeof(string) => reader.GetString(), + _ => ReadObject<T>(stream, ref buffer, ref reader) + }; + + object ReadObject<T>(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new JsonException(); + } + + List<byte> objBuffer = [(byte)'{', ..reader.ValueSequence.ToArray()]; + var currentDepth = reader.CurrentDepth; + while (reader.CurrentDepth >= currentDepth) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == currentDepth) { + break; + } + + objBuffer.AddRange(reader.ValueSpan); + } + + return JsonSerializer.Deserialize<T>(objBuffer.ToArray()); + } + + return (T)call; + + // return JsonSerializer.Deserialize<T>(ref reader); + } + + private static void GetMoreBytesFromStream(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + int bytesRead; + if (reader.BytesConsumed < buffer.Length) { + ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed); + + if (leftover.Length == buffer.Length) { + Array.Resize(ref buffer, buffer.Length * 2); + Console.WriteLine($"Increased buffer size to {buffer.Length}"); + } + + leftover.CopyTo(buffer); + bytesRead = stream.Read(buffer.AsSpan(leftover.Length)); + } + else { + bytesRead = stream.Read(buffer); + } + + // Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}"); + reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState); + } +} + +public partial class SynapseCollectionJsonConverter<T> : JsonConverter<SynapseCollectionResult<T>> { + public override SynapseCollectionResult<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType != JsonTokenType.StartObject) { + throw new JsonException(); + } + + var result = new SynapseCollectionResult<T>(); + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new JsonException(); + } + + var propName = reader.GetString(); + reader.Read(); + if (propName == "total") { + result.Total = reader.GetInt32(); + } + else if (propName == "prev_token") { + result.PrevToken = reader.GetString(); + } + else if (propName == "next_token") { + result.NextToken = reader.GetString(); + } + else if (propName == "chunk") { + if (reader.TokenType != JsonTokenType.StartArray) { + throw new JsonException(); + } + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndArray) { + break; + } + + var item = JsonSerializer.Deserialize<T>(ref reader, options); + result.Chunk.Add(item); + } + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, SynapseCollectionResult<T> value, JsonSerializerOptions options) { + writer.WriteStartObject(); + if (value.Total is not null) + writer.WriteNumber("total", value.Total ?? 0); + if (value.PrevToken is not null) + writer.WriteString("prev_token", value.PrevToken); + if (value.NextToken is not null) + writer.WriteString("next_token", value.NextToken); + + writer.WriteStartArray("chunk"); + foreach (var item in value.Chunk) { + JsonSerializer.Serialize(writer, item, options); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs new file mode 100644
index 0000000..5530cc3 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs
@@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserMediaResult { + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } + + [JsonPropertyName("media")] + public List<MediaInfo> Media { get; set; } = new(); + + public class MediaInfo { + [JsonPropertyName("created_ts")] + public long CreatedTimestamp { get; set; } + + [JsonPropertyName("last_access_ts")] + public long? LastAccessTimestamp { get; set; } + + [JsonPropertyName("media_id")] + public string MediaId { get; set; } + + [JsonPropertyName("media_length")] + public int MediaLength { get; set; } + + [JsonPropertyName("media_type")] + public string MediaType { get; set; } + + [JsonPropertyName("quarantined_by")] + public string? QuarantinedBy { get; set; } + + [JsonPropertyName("safe_from_quarantine")] + public bool SafeFromQuarantine { get; set; } + + [JsonPropertyName("upload_name")] + public string UploadName { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs new file mode 100644
index 0000000..3132906 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs
@@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserListResult { + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } + + [JsonPropertyName("users")] + public List<SynapseAdminUserListResultUser> Users { get; set; } = new(); + + public class SynapseAdminUserListResultUser { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("is_guest")] + public bool? IsGuest { get; set; } + + [JsonPropertyName("admin")] + public bool? Admin { get; set; } + + [JsonPropertyName("user_type")] + public string? UserType { get; set; } + + [JsonPropertyName("deactivated")] + public bool Deactivated { get; set; } + + [JsonPropertyName("erased")] + public bool Erased { get; set; } + + [JsonPropertyName("shadow_banned")] + public bool ShadowBanned { get; set; } + + [JsonPropertyName("displayname")] + public string? DisplayName { get; set; } + + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("creation_ts")] + public long CreationTs { get; set; } + + [JsonPropertyName("last_seen_ts")] + public long? LastSeenTs { get; set; } + + [JsonPropertyName("locked")] + public bool Locked { get; set; } + + // Requires enabling MSC3866 + [JsonPropertyName("approved")] + public bool? Approved { get; set; } + + [JsonIgnore] + public DateTime CreationTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(CreationTs).DateTime; + set => CreationTs = new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [JsonIgnore] + public DateTime? LastSeenTsDateTime { + get => LastSeenTs.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(LastSeenTs.Value).DateTime : null; + set => LastSeenTs = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
index ac94a7a..cee3d8d 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -1,90 +1,163 @@ +// #define LOG_SKIP + +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using ArcaneLibs.Extensions; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; +using LibMatrix.Responses; using LibMatrix.Filters; -using LibMatrix.Responses.Admin; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; +using LibMatrix.Responses; namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedHomeserver) { - public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", - string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) { - AdminRoomListingResult? res = null; + private SynapseAdminUserCleanupExecutor UserCleanupExecutor { get; } = new(authenticatedHomeserver); + // https://github.com/element-hq/synapse/tree/develop/docs/admin_api + // https://github.com/element-hq/synapse/tree/develop/docs/usage/administration/admin_api + +#region Rooms + + public async IAsyncEnumerable<SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom> SearchRoomsAsync(int limit = int.MaxValue, int chunkLimit = 250, + string orderBy = "name", string dir = "f", string? searchTerm = null, SynapseAdminLocalRoomQueryFilter? localFilter = null) { + SynapseAdminRoomListResult? res = null; var i = 0; int? totalRooms = null; do { - var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 250)}&dir={dir}&order_by={orderBy}"; + var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, chunkLimit)}&dir={dir}&order_by={orderBy}"; if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); - res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<AdminRoomListingResult>(url); + res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomListResult>(url); totalRooms ??= res.TotalRooms; - Console.WriteLine(res.ToJson(false)); + // Console.WriteLine(res.ToJson(false)); foreach (var room in res.Rooms) { if (localFilter is not null) { - if (!room.RoomId.Contains(localFilter.RoomIdContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.RoomIdContains) && !room.RoomId.Contains(localFilter.RoomIdContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomid."); +#endif continue; } - if (!room.Name?.Contains(localFilter.NameContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.NameContains) && room.Name?.Contains(localFilter.NameContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomname."); +#endif continue; } - if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.CanonicalAliasContains) && + room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule alias."); +#endif continue; } - if (!room.Version.Contains(localFilter.VersionContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.VersionContains) && !room.Version.Contains(localFilter.VersionContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule version."); +#endif continue; } - if (!room.Creator.Contains(localFilter.CreatorContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.CreatorContains) && !room.Creator.Contains(localFilter.CreatorContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule creator."); +#endif continue; } - if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.EncryptionContains) && + room.Encryption?.Contains(localFilter.EncryptionContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule encryption."); +#endif continue; } - if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.JoinRulesContains) && + room.JoinRules?.Contains(localFilter.JoinRulesContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joinrules."); +#endif continue; } - if (!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.GuestAccessContains) && + room.GuestAccess?.Contains(localFilter.GuestAccessContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule guestaccess."); +#endif continue; } - if (!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.HistoryVisibilityContains) && + room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule history visibility."); +#endif continue; } if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule federation."); +#endif continue; } if (localFilter.CheckPublic && room.Public != localFilter.Public) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule public."); +#endif + continue; + } + + if (room.StateEvents < localFilter.StateEventsGreaterThan || room.StateEvents > localFilter.StateEventsLessThan) { + totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members."); +#endif continue; } if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined members: {localFilter.JoinedMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedMembersLessThan}."); +#endif continue; } if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members: {localFilter.JoinedLocalMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedLocalMembersLessThan}."); +#endif continue; } } @@ -104,4 +177,386 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH } } while (i < Math.Min(limit, totalRooms ?? limit)); } + +#endregion + +#region Users + + public async IAsyncEnumerable<SynapseAdminUserListResult.SynapseAdminUserListResultUser> SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250, + string orderBy = "name", string dir = "f", + SynapseAdminLocalUserQueryFilter? localFilter = null) { + // TODO: implement filters + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v3/users", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + if (!string.IsNullOrWhiteSpace(orderBy)) url = url.AddQuery("order_by", orderBy); + if (!string.IsNullOrWhiteSpace(dir)) url = url.AddQuery("dir", dir); + + Console.WriteLine($"--- ADMIN Querying User List with URL: {url} ---"); + // TODO: implement URI methods in http client + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminUserListResult>(url.ToString()); + foreach (var user in res.Users) { + limit--; + yield return user; + } + + if (string.IsNullOrWhiteSpace(res.NextToken)) break; + from = res.NextToken; + } + } + + public async Task<LoginResponse> LoginUserAsync(string userId, TimeSpan expireAfter) { + var url = new Uri($"/_synapse/admin/v1/users/{userId.UrlEncode()}/login", UriKind.Relative); + url.AddQuery("valid_until_ms", DateTimeOffset.UtcNow.Add(expireAfter).ToUnixTimeMilliseconds().ToString()); + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync<JsonObject>(url.ToString(), new()); + var loginResp = await resp.Content.ReadFromJsonAsync<LoginResponse>(); + loginResp.UserId = userId; // Synapse only returns the access token + return loginResp; + } + + public async Task<AuthenticatedHomeserverSynapse> GetHomeserverForUserAsync(string userId, TimeSpan expireAfter) { + var loginResp = await LoginUserAsync(userId, expireAfter); + var homeserver = new AuthenticatedHomeserverSynapse( + authenticatedHomeserver.ServerName, authenticatedHomeserver.WellKnownUris, authenticatedHomeserver.Proxy, loginResp.AccessToken + ); + await homeserver.Initialise(); + return homeserver; + } + +#endregion + +#region Reports + + public async IAsyncEnumerable<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReport> GetEventReportsAsync(int limit = int.MaxValue, int chunkLimit = 250, + string dir = "f", SynapseAdminLocalEventReportQueryFilter? filter = null) { + // TODO: implement filters + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v1/event_reports", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Reports with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminEventReportListResult>(url.ToString()); + foreach (var report in res.Reports) { + limit--; + yield return report; + } + + if (string.IsNullOrWhiteSpace(res.NextToken)) break; + from = res.NextToken; + } + } + + public async Task<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails> GetEventReportDetailsAsync(string reportId) { + var url = new Uri($"/_synapse/admin/v1/event_reports/{reportId.UrlEncode()}", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient + .GetFromJsonAsync<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails>(url.ToString()); + } + + // Utility function to get details straight away + public async IAsyncEnumerable<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails> GetEventReportsWithDetailsAsync(int limit = int.MaxValue, + int chunkLimit = 250, string dir = "f", SynapseAdminLocalEventReportQueryFilter? filter = null) { + Queue<Task<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails>> tasks = []; + await foreach (var report in GetEventReportsAsync(limit, chunkLimit, dir, filter)) { + tasks.Enqueue(GetEventReportDetailsAsync(report.Id)); + while (tasks.Peek().IsCompleted) yield return await tasks.Dequeue(); // early return if possible + } + + while (tasks.Count > 0) yield return await tasks.Dequeue(); + } + + public async Task DeleteEventReportAsync(string reportId) { + var url = new Uri($"/_synapse/admin/v1/event_reports/{reportId.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.DeleteAsync(url.ToString()); + } + +#endregion + +#region Background Updates + + public async Task<bool> GetBackgroundUpdatesEnabledAsync() { + var url = new Uri("/_synapse/admin/v1/background_updates/enabled", UriKind.Relative); + // The return type is technically wrong, but includes the field we want. + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminBackgroundUpdateStatusResponse>(url.ToString()); + return resp.Enabled; + } + + public async Task<bool> SetBackgroundUpdatesEnabledAsync(bool enabled) { + var url = new Uri("/_synapse/admin/v1/background_updates/enabled", UriKind.Relative); + // The used types are technically wrong, but include the field we want. + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync<JsonObject>(url.ToString(), new JsonObject { + ["enabled"] = enabled + }); + var json = await resp.Content.ReadFromJsonAsync<SynapseAdminBackgroundUpdateStatusResponse>(); + return json!.Enabled; + } + + public async Task<SynapseAdminBackgroundUpdateStatusResponse> GetBackgroundUpdatesStatusAsync() { + var url = new Uri("/_synapse/admin/v1/background_updates/status", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminBackgroundUpdateStatusResponse>(url.ToString()); + } + + /// <summary> + /// Run a background job + /// </summary> + /// <param name="jobName">One of "populate_stats_process_rooms" or "regenerate_directory"</param> + public async Task RunBackgroundJobsAsync(string jobName) { + var url = new Uri("/_synapse/admin/v1/background_updates/run", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), new JsonObject() { + ["job_name"] = jobName + }); + } + +#endregion + +#region Federation + + public async IAsyncEnumerable<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination> GetFederationDestinationsAsync(int limit = int.MaxValue, + int chunkLimit = 250) { + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v1/federation/destinations", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Federation Destinations with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminDestinationListResult>(url.ToString()); + foreach (var dest in res.Destinations) { + limit--; + yield return dest; + } + } + } + + public async Task<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination> GetFederationDestinationDetailsAsync(string destination) { + var url = new Uri($"/_synapse/admin/v1/federation/destinations/{destination}", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination>(url.ToString()); + } + + public async IAsyncEnumerable<SynapseAdminDestinationRoomListResult.SynapseAdminDestinationRoomListResultRoom> GetFederationDestinationRoomsAsync(string destination, + int limit = int.MaxValue, int chunkLimit = 250) { + string? from = null; + while (limit > 0) { + var url = new Uri($"/_synapse/admin/v1/federation/destinations/{destination}/rooms", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Federation Destination Rooms with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminDestinationRoomListResult>(url.ToString()); + foreach (var room in res.Rooms) { + limit--; + yield return room; + } + } + } + + public async Task ResetFederationConnectionTimeoutAsync(string destination) { + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/federation/destinations/{destination}/reset_connection", new JsonObject()); + } + +#endregion + +#region Registration Tokens + + // does not support pagination + public async Task<List<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>> GetRegistrationTokensAsync() { + var url = new Uri("/_synapse/admin/v1/registration_tokens", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRegistrationTokenListResult>(url.ToString()); + return resp.RegistrationTokens; + } + + public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> GetRegistrationTokenAsync(string token) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + var resp = + await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>(url.ToString()); + return resp; + } + + public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> CreateRegistrationTokenAsync( + SynapseAdminRegistrationTokenCreateRequest request) { + var url = new Uri("/_synapse/admin/v1/", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), request); + var token = await resp.Content.ReadFromJsonAsync<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>(); + return token!; + } + + public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> UpdateRegistrationTokenAsync(string token, + SynapseAdminRegistrationTokenUpdateRequest request) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync(url.ToString(), request); + return await resp.Content.ReadFromJsonAsync<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>(); + } + + public async Task DeleteRegistrationTokenAsync(string token) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.DeleteAsync(url.ToString()); + } + +#endregion + +#region Account Validity + + // Does anyone even use this? + // README: https://github.com/matrix-org/synapse/issues/15271 + // -> Don't implement unless requested, if not for this feature almost never being used. + +#endregion + +#region Experimental Features + + public async Task<Dictionary<string, bool>> GetExperimentalFeaturesAsync(string userId) { + var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<JsonObject>(url.ToString()); + return resp["features"]!.GetValue<Dictionary<string, bool>>(); + } + + public async Task SetExperimentalFeaturesAsync(string userId, Dictionary<string, bool> features) { + var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync<JsonObject>(url.ToString(), new JsonObject { + ["features"] = JsonSerializer.Deserialize<JsonObject>(features.ToJson()) + }); + } + +#endregion + +#region Media + + public async Task<SynapseAdminRoomMediaListResult> GetRoomMediaAsync(string roomId) { + var url = new Uri($"/_synapse/admin/v1/room/{roomId.UrlEncode()}/media", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomMediaListResult>(url.ToString()); + } + + // This is in the user admin API section + // public async IAsyncEnumerable<SynapseAdminRoomMediaListResult> + +#endregion + + public async Task<SynapseAdminUserRedactIdResponse?> DeleteAllMessages(string mxid, List<string>? rooms = null, string? reason = null, int? limit = 100000, + bool waitForCompletion = true) { + rooms ??= []; + + Dictionary<string, object> payload = new(); + if (rooms.Count > 0) payload["rooms"] = rooms; + if (!string.IsNullOrEmpty(reason)) payload["reason"] = reason; + if (limit.HasValue) payload["limit"] = limit.Value; + + var redactIdResp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/user/{mxid}/redact", payload); + var redactId = await redactIdResp.Content.ReadFromJsonAsync<SynapseAdminUserRedactIdResponse>(); + + if (waitForCompletion) { + while (true) { + var status = await GetRedactStatus(redactId!.RedactionId); + if (status?.Status != "pending") break; + await Task.Delay(1000); + } + } + + return redactId; + } + + public async Task<SynapseAdminRedactStatusResponse?> GetRedactStatus(string redactId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRedactStatusResponse>( + $"/_synapse/admin/v1/user/redact_status/{redactId}"); + } + + public async Task DeactivateUserAsync(string mxid, bool erase = false, bool eraseMessages = false, bool extraCleanup = false) { + if (eraseMessages) { + await DeleteAllMessages(mxid); + } + + if (extraCleanup) { + await UserCleanupExecutor.CleanupUser(mxid); + } + + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/deactivate", new { erase }); + } + + public async Task ResetPasswordAsync(string mxid, string newPassword, bool logoutDevices = false) { + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/reset_password/{mxid}", + new { new_password = newPassword, logout_devices = logoutDevices }); + } + + public async Task<SynapseAdminUserMediaResult> GetUserMediaAsync(string mxid, int? limit = 100, string? from = null, string? orderBy = null, string? dir = null) { + var url = $"/_synapse/admin/v1/users/{mxid}/media"; + if (limit.HasValue) url += $"?limit={limit}"; + if (!string.IsNullOrEmpty(from)) url += $"&from={from}"; + if (!string.IsNullOrEmpty(orderBy)) url += $"&order_by={orderBy}"; + if (!string.IsNullOrEmpty(dir)) url += $"&dir={dir}"; + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminUserMediaResult>(url); + } + + public async IAsyncEnumerable<SynapseAdminUserMediaResult.MediaInfo> GetUserMediaEnumerableAsync(string mxid, int chunkSize = 100, string? orderBy = null, string? dir = null) { + SynapseAdminUserMediaResult? res = null; + do { + res = await GetUserMediaAsync(mxid, chunkSize, res?.NextToken, orderBy, dir); + foreach (var media in res.Media) { + yield return media; + } + } while (!string.IsNullOrEmpty(res.NextToken)); + } + + public async Task BlockRoom(string roomId, bool block = true) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/block", new { + block + }); + } + + public async Task<SynapseAdminRoomDeleteResponse> DeleteRoom(string roomId, SynapseAdminRoomDeleteRequest request, bool waitForCompletion = true) { + var resp = await authenticatedHomeserver.ClientHttpClient.DeleteAsJsonAsync($"/_synapse/admin/v2/rooms/{roomId.UrlEncode()}", request); + var deleteResp = await resp.Content.ReadFromJsonAsync<SynapseAdminRoomDeleteResponse>(); + + if (waitForCompletion) { + while (true) { + var status = await GetRoomDeleteStatus(deleteResp!.DeleteId); + if (status?.Status != "pending") break; + await Task.Delay(1000); + } + } + + return deleteResp!; + } + + public async Task<SynapseAdminRoomDeleteStatus> GetRoomDeleteStatusByRoomId(string roomId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomDeleteStatus>( + $"/_synapse/admin/v2/rooms/{roomId.UrlEncode()}/delete_status"); + } + + public async Task<SynapseAdminRoomDeleteStatus> GetRoomDeleteStatus(string deleteId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomDeleteStatus>( + $"/_synapse/admin/v2/rooms/delete_status/{deleteId}"); + } + + public async Task<SynapseAdminRoomMemberListResult> GetRoomMembersAsync(string roomId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomMemberListResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/members"); + } + + public async Task<SynapseAdminRoomStateResult> GetRoomStateAsync(string roomId, string? type = null) { + return string.IsNullOrWhiteSpace(type) + ? await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomStateResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/state") + : await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomStateResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/state?type={type}"); + } + + public async Task QuarantineMediaByRoomId(string roomId) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/room/{roomId.UrlEncode()}/media/quarantine", new { }); + } + + public async Task QuarantineMediaByUserId(string mxid) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/user/{mxid}/media/quarantine", new { }); + } + + public async Task QuarantineMediaById(string serverName, string mediaId) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/media/quarantine/{serverName}/{mediaId}", new { }); + } + + public async Task QuarantineMediaById(MxcUri mxcUri) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/media/quarantine/{mxcUri.ServerName}/{mxcUri.MediaId}", new { }); + } + + public async Task DeleteMediaById(string serverName, string mediaId) { + await authenticatedHomeserver.ClientHttpClient.DeleteAsync($"/_synapse/admin/v1/media/{serverName}/{mediaId}"); + } + + public async Task DeleteMediaById(MxcUri mxcUri) { + await authenticatedHomeserver.ClientHttpClient.DeleteAsync($"/_synapse/admin/v1/media/{mxcUri.ServerName}/{mxcUri.MediaId}"); + } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs new file mode 100644
index 0000000..6edf40c --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs
@@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; + +public class SynapseAdminUserCleanupExecutor(AuthenticatedHomeserverSynapse homeserver) { + /* + Remove mappings of SSO IDs + Delete media uploaded by user (included avatar images) + Delete sent and received messages + Remove the user's creation (registration) timestamp + Remove rate limit overrides + Remove from monthly active users + Remove user's consent information (consent version and timestamp) + */ + public async Task CleanupUser(string mxid) { + // change the user's password to a random one + var newPassword = Guid.NewGuid().ToString(); + await homeserver.Admin.ResetPasswordAsync(mxid, newPassword, true); + await homeserver.Admin.DeleteAllMessages(mxid); + + } + private async Task RunUserTasks(string mxid) { + var auth = await homeserver.Admin.LoginUserAsync(mxid, TimeSpan.FromDays(1)); + var userHs = new AuthenticatedHomeserverSynapse(homeserver.ServerName, homeserver.WellKnownUris, null, auth.AccessToken); + await userHs.Initialise(); + + + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index f9e3d04..f0b35f9 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -1,35 +1,36 @@ using System.Net.Http.Json; +using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; using ArcaneLibs.Extensions; using LibMatrix.Extensions; using LibMatrix.Responses; using LibMatrix.Services; -using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Homeservers; public class RemoteHomeserver { - public RemoteHomeserver(string baseUrl, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy) { + public RemoteHomeserver(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy) { if (string.IsNullOrWhiteSpace(proxy)) proxy = null; - BaseUrl = baseUrl; + ServerNameOrUrl = serverName; WellKnownUris = wellKnownUris; + Proxy = proxy; ClientHttpClient = new MatrixHttpClient { - BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")), + BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {serverName}!")), // Timeout = TimeSpan.FromSeconds(300) // TODO: Re-implement this }; - if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); + if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", serverName); if (!string.IsNullOrWhiteSpace(wellKnownUris.Server)) FederationClient = new FederationClient(WellKnownUris.Server!, proxy); Auth = new(this); } private Dictionary<string, object> _profileCache { get; set; } = new(); - public string BaseUrl { get; } + public string ServerNameOrUrl { get; } + public string? Proxy { get; } [JsonIgnore] public MatrixHttpClient ClientHttpClient { get; set; } @@ -51,16 +52,18 @@ public class RemoteHomeserver { var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/profile/{HttpUtility.UrlEncode(mxid)}"); var data = await resp.Content.ReadFromJsonAsync<UserProfileResponse>(); if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); - _profileCache[mxid] = data; + _profileCache[mxid] = data ?? throw new InvalidOperationException($"Could not get profile for {mxid}"); return data; } + + // TODO: Do we need to support retrieving individual profile properties? Is there any use for that besides just getting the full profile? public async Task<ClientVersionsResponse> GetClientVersionsAsync() { var resp = await ClientHttpClient.GetAsync($"/_matrix/client/versions"); var data = await resp.Content.ReadFromJsonAsync<ClientVersionsResponse>(); if (!resp.IsSuccessStatusCode) Console.WriteLine("ClientVersions: " + data); - return data; + return data ?? throw new InvalidOperationException("ClientVersionsResponse is null"); } public async Task<AliasResult> ResolveRoomAliasAsync(string alias) { @@ -68,7 +71,16 @@ public class RemoteHomeserver { var data = await resp.Content.ReadFromJsonAsync<AliasResult>(); //var text = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) Console.WriteLine("ResolveAlias: " + data.ToJson()); - return data; + return data ?? throw new InvalidOperationException($"Could not resolve alias {alias}"); + } + + public Task<PublicRoomDirectoryResult> GetPublicRoomsAsync(int limit = 100, string? server = null, string? since = null) => + ClientHttpClient.GetFromJsonAsync<PublicRoomDirectoryResult>(buildUriWithParams("/_matrix/client/v3/publicRooms", (nameof(limit), true, limit), + (nameof(server), !string.IsNullOrWhiteSpace(server), server), (nameof(since), !string.IsNullOrWhiteSpace(since), since))); + + // TODO: move this somewhere else + private string buildUriWithParams(string url, params (string name, bool include, object? value)[] values) { + return url + "?" + string.Join("&", values.Where(x => x.include)); } #region Authentication @@ -80,12 +92,11 @@ public class RemoteHomeserver { type = "m.id.user", user = username }, - password = password, + password, initial_device_display_name = deviceName }); var data = await resp.Content.ReadFromJsonAsync<LoginResponse>(); - if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToJson()); - return data; + return data ?? throw new InvalidOperationException("LoginResponse is null"); } public async Task<LoginResponse> RegisterAsync(string username, string password, string? deviceName = null) { @@ -101,26 +112,64 @@ public class RemoteHomeserver { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var data = await resp.Content.ReadFromJsonAsync<LoginResponse>(); - if (!resp.IsSuccessStatusCode) Console.WriteLine("Register: " + data.ToJson()); - return data; + return data ?? throw new InvalidOperationException("LoginResponse is null"); } #endregion [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)] - public string? ResolveMediaUri(string? mxcUri) { - if (mxcUri is null) return null; - if (mxcUri.StartsWith("https://")) return mxcUri; - return $"{ClientHttpClient.BaseAddress}/_matrix/media/v3/download/{mxcUri.Replace("mxc://", "")}".Replace("//_matrix", "/_matrix"); - } + public virtual string? ResolveMediaUri(string? mxcUri) => null; public UserInteractiveAuthClient Auth; } +public class PublicRoomDirectoryResult { + [JsonPropertyName("chunk")] + public List<PublicRoomListItem> Chunk { get; set; } + + [JsonPropertyName("next_batch")] + public string? NextBatch { get; set; } + + [JsonPropertyName("prev_batch")] + public string? PrevBatch { get; set; } + + [JsonPropertyName("total_room_count_estimate")] + public int TotalRoomCountEstimate { get; set; } + + public class PublicRoomListItem { + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("canonical_alias")] + public string? CanonicalAlias { get; set; } + + [JsonPropertyName("guest_can_join")] + public bool GuestCanJoin { get; set; } + + [JsonPropertyName("join_rule")] + public string JoinRule { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("num_joined_members")] + public int NumJoinedMembers { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("topic")] + public string? Topic { get; set; } + + [JsonPropertyName("world_readable")] + public bool WorldReadable { get; set; } + } +} + public class AliasResult { [JsonPropertyName("room_id")] - public string RoomId { get; set; } = null!; + public string RoomId { get; set; } [JsonPropertyName("servers")] - public List<string> Servers { get; set; } = null!; + public List<string> Servers { get; set; } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/UserInteractiveAuthClient.cs b/LibMatrix/Homeservers/UserInteractiveAuthClient.cs
index 8be2cb9..8de01d2 100644 --- a/LibMatrix/Homeservers/UserInteractiveAuthClient.cs +++ b/LibMatrix/Homeservers/UserInteractiveAuthClient.cs
@@ -1,7 +1,5 @@ -using System.Net.Http.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using ArcaneLibs.Extensions; using LibMatrix.Responses; namespace LibMatrix.Homeservers; @@ -51,27 +49,27 @@ public class UserInteractiveAuthClient { internal class RegisterFlowsResponse { [JsonPropertyName("session")] - public string Session { get; set; } = null!; + public string Session { get; set; } [JsonPropertyName("flows")] - public List<RegisterFlow> Flows { get; set; } = null!; + public List<RegisterFlow> Flows { get; set; } [JsonPropertyName("params")] - public JsonObject Params { get; set; } = null!; + public JsonObject Params { get; set; } public class RegisterFlow { [JsonPropertyName("stages")] - public List<string> Stages { get; set; } = null!; + public List<string> Stages { get; set; } } } internal class LoginFlowsResponse { [JsonPropertyName("flows")] - public List<LoginFlow> Flows { get; set; } = null!; + public List<LoginFlow> Flows { get; set; } public class LoginFlow { [JsonPropertyName("type")] - public string Type { get; set; } = null!; + public string Type { get; set; } } } diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs
index 165e7df..f7e5488 100644 --- a/LibMatrix/Interfaces/Services/IStorageProvider.cs +++ b/LibMatrix/Interfaces/Services/IStorageProvider.cs
@@ -1,3 +1,7 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using LibMatrix.Responses; + namespace LibMatrix.Interfaces.Services; public interface IStorageProvider { @@ -17,6 +21,13 @@ public interface IStorageProvider { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!"); throw new NotImplementedException(); } + + public Task SaveObjectAsync<T>(string key, T value, JsonTypeInfo<T> jsonTypeInfo) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObjectAsync<T>(key, value, typeInfo), using default implementation w/ MemoryStream!"); + var ms = new MemoryStream(); + JsonSerializer.Serialize(ms, value, jsonTypeInfo); + return SaveStreamAsync(key, ms); + } // load public Task<T?> LoadObjectAsync<T>(string key) { @@ -24,6 +35,12 @@ public interface IStorageProvider { throw new NotImplementedException(); } + public async Task<T?> LoadObjectAsync<T>(string key, JsonTypeInfo<T> jsonTypeInfo) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, typeInfo), using default implementation!"); + await using var stream = await LoadStreamAsync(key); + return JsonSerializer.Deserialize(stream!, jsonTypeInfo); + } + // check if exists public Task<bool> ObjectExistsAsync(string key) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!"); @@ -31,7 +48,7 @@ public interface IStorageProvider { } // get all keys - public Task<List<string>> GetAllKeysAsync() { + public Task<IEnumerable<string>> GetAllKeysAsync() { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!"); throw new NotImplementedException(); } @@ -53,4 +70,18 @@ public interface IStorageProvider { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!"); throw new NotImplementedException(); } + + // copy + public async Task CopyObjectAsync(string sourceKey, string destKey) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement CopyObject(sourceKey, destKey), using load + save!"); + var data = await LoadObjectAsync<object>(sourceKey); + await SaveObjectAsync(destKey, data); + } + + // move + public async Task MoveObjectAsync(string sourceKey, string destKey) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement MoveObject(sourceKey, destKey), using copy + delete!"); + await CopyObjectAsync(sourceKey, destKey); + await DeleteObjectAsync(sourceKey); + } } \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index d0511ea..62bb48f 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj
@@ -1,33 +1,26 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>preview</LangVersion> <Optimize>true</Optimize> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- Required for UnicodeJsonEncoder... --> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1"/> + <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/> </ItemGroup> <ItemGroup> - <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/> - <!-- This is dangerous, but eases development since locking the version will drift out of sync without noticing, - which causes build errors due to missing functions. - Using the NuGet version in development is annoying due to delays between pushing and being able to consume. - If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git - instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. --> - <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/> - <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/> + <!-- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250313-104848" Condition="'$(Configuration)' == 'Release'" />--> + <!-- <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/>--> + <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/> </ItemGroup> - <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild"> - <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/> - </Target> - </Project> diff --git a/LibMatrix/LibMatrixException.cs b/LibMatrix/LibMatrixException.cs
index 5854826..27cfc2a 100644 --- a/LibMatrix/LibMatrixException.cs +++ b/LibMatrix/LibMatrixException.cs
@@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; +// ReSharper disable MemberCanBePrivate.Global namespace LibMatrix; @@ -20,6 +22,7 @@ public class LibMatrixException : Exception { _ => $"Unknown error: {GetAsObject().ToJson(ignoreNull: true)}" }}\nError: {Error}"; + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Follows spec naming")] public static class ErrorCodes { public const string M_NOT_FOUND = "M_NOT_FOUND"; public const string M_UNSUPPORTED = "M_UNSUPPORTED"; diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs
index eb207da..519f99e 100644 --- a/LibMatrix/MatrixException.cs +++ b/LibMatrix/MatrixException.cs
@@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; +// ReSharper disable MemberCanBePrivate.Global namespace LibMatrix; @@ -17,51 +19,56 @@ public class MatrixException : Exception { [JsonPropertyName("retry_after_ms")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? RetryAfterMs { get; set; } - + public string RawContent { get; set; } public object GetAsObject() => new { errcode = ErrorCode, error = Error, soft_logout = SoftLogout, retry_after_ms = RetryAfterMs }; public string GetAsJson() => GetAsObject().ToJson(ignoreNull: true); public override string Message => - $"{ErrorCode}: {ErrorCode switch { - // common - "M_FORBIDDEN" => $"You do not have permission to perform this action: {Error}", - "M_UNKNOWN_TOKEN" => $"The access token specified was not recognised: {Error}{(SoftLogout == true ? " (soft logout)" : "")}", - "M_MISSING_TOKEN" => $"No access token was specified: {Error}", - "M_BAD_JSON" => $"Request contained valid JSON, but it was malformed in some way: {Error}", - "M_NOT_JSON" => $"Request did not contain valid JSON: {Error}", - "M_NOT_FOUND" => $"The requested resource was not found: {Error}", - "M_LIMIT_EXCEEDED" => $"Too many requests have been sent in a short period of time. Wait a while then try again: {Error}", - "M_UNRECOGNISED" => $"The server did not recognise the request: {Error}", - "M_UNKOWN" => $"The server encountered an unexpected error: {Error}", - // endpoint specific - "M_UNAUTHORIZED" => $"The request did not contain valid authentication information for the target of the request: {Error}", - "M_USER_DEACTIVATED" => $"The user ID associated with the request has been deactivated: {Error}", - "M_USER_IN_USE" => $"The user ID associated with the request is already in use: {Error}", - "M_INVALID_USERNAME" => $"The requested user ID is not valid: {Error}", - "M_ROOM_IN_USE" => $"The room alias requested is already taken: {Error}", - "M_INVALID_ROOM_STATE" => $"The room associated with the request is not in a valid state to perform the request: {Error}", - "M_THREEPID_IN_USE" => $"The threepid requested is already associated with a user ID on this server: {Error}", - "M_THREEPID_NOT_FOUND" => $"The threepid requested is not associated with any user ID: {Error}", - "M_THREEPID_AUTH_FAILED" => $"The provided threepid and/or token was invalid: {Error}", - "M_THREEPID_DENIED" => $"The homeserver does not permit the third party identifier in question: {Error}", - "M_SERVER_NOT_TRUSTED" => $"The homeserver does not trust the identity server: {Error}", - "M_UNSUPPORTED_ROOM_VERSION" => $"The room version is not supported: {Error}", - "M_INCOMPATIBLE_ROOM_VERSION" => $"The room version is incompatible: {Error}", - "M_BAD_STATE" => $"The request was invalid because the state was invalid: {Error}", - "M_GUEST_ACCESS_FORBIDDEN" => $"Guest access is forbidden: {Error}", - "M_CAPTCHA_NEEDED" => $"Captcha needed: {Error}", - "M_CAPTCHA_INVALID" => $"Captcha invalid: {Error}", - "M_MISSING_PARAM" => $"Missing parameter: {Error}", - "M_INVALID_PARAM" => $"Invalid parameter: {Error}", - "M_TOO_LARGE" => $"The request or entity was too large: {Error}", - "M_EXCLUSIVE" => $"The resource being requested is reserved by an application service, or the application service making the request has not created the resource: {Error}", - "M_RESOURCE_LIMIT_EXCEEDED" => $"Exceeded resource limit: {Error}", - "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" => $"Cannot leave server notice room: {Error}", - _ => $"Unknown error: {new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true)}" - }}"; + $"{ErrorCode}: " + + (!string.IsNullOrWhiteSpace(Error) + ? Error + : ErrorCode switch { + // common + "M_FORBIDDEN" => $"You do not have permission to perform this action: {Error}", + "M_UNKNOWN_TOKEN" => $"The access token specified was not recognised: {Error}{(SoftLogout == true ? " (soft logout)" : "")}", + "M_MISSING_TOKEN" => $"No access token was specified: {Error}", + "M_BAD_JSON" => $"Request contained valid JSON, but it was malformed in some way: {Error}", + "M_NOT_JSON" => $"Request did not contain valid JSON: {Error}", + "M_NOT_FOUND" => $"The requested resource was not found: {Error}", + "M_LIMIT_EXCEEDED" => $"Too many requests have been sent in a short period of time. Wait a while then try again: {Error}", + "M_UNRECOGNISED" => $"The server did not recognise the request: {Error}", + "M_UNKOWN" => $"The server encountered an unexpected error: {Error}", + // endpoint specific + "M_UNAUTHORIZED" => $"The request did not contain valid authentication information for the target of the request: {Error}", + "M_USER_DEACTIVATED" => $"The user ID associated with the request has been deactivated: {Error}", + "M_USER_IN_USE" => $"The user ID associated with the request is already in use: {Error}", + "M_INVALID_USERNAME" => $"The requested user ID is not valid: {Error}", + "M_ROOM_IN_USE" => $"The room alias requested is already taken: {Error}", + "M_INVALID_ROOM_STATE" => $"The room associated with the request is not in a valid state to perform the request: {Error}", + "M_THREEPID_IN_USE" => $"The threepid requested is already associated with a user ID on this server: {Error}", + "M_THREEPID_NOT_FOUND" => $"The threepid requested is not associated with any user ID: {Error}", + "M_THREEPID_AUTH_FAILED" => $"The provided threepid and/or token was invalid: {Error}", + "M_THREEPID_DENIED" => $"The homeserver does not permit the third party identifier in question: {Error}", + "M_SERVER_NOT_TRUSTED" => $"The homeserver does not trust the identity server: {Error}", + "M_UNSUPPORTED_ROOM_VERSION" => $"The room version is not supported: {Error}", + "M_INCOMPATIBLE_ROOM_VERSION" => $"The room version is incompatible: {Error}", + "M_BAD_STATE" => $"The request was invalid because the state was invalid: {Error}", + "M_GUEST_ACCESS_FORBIDDEN" => $"Guest access is forbidden: {Error}", + "M_CAPTCHA_NEEDED" => $"Captcha needed: {Error}", + "M_CAPTCHA_INVALID" => $"Captcha invalid: {Error}", + "M_MISSING_PARAM" => $"Missing parameter: {Error}", + "M_INVALID_PARAM" => $"Invalid parameter: {Error}", + "M_TOO_LARGE" => $"The request or entity was too large: {Error}", + "M_EXCLUSIVE" => + $"The resource being requested is reserved by an application service, or the application service making the request has not created the resource: {Error}", + "M_RESOURCE_LIMIT_EXCEEDED" => $"Exceeded resource limit: {Error}", + "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" => $"Cannot leave server notice room: {Error}", + _ => $"Unknown error: {new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true)}" + }); + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Follows spec naming")] public static class ErrorCodes { public const string M_FORBIDDEN = "M_FORBIDDEN"; public const string M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"; diff --git a/LibMatrix/MxcUri.cs b/LibMatrix/MxcUri.cs new file mode 100644
index 0000000..875ae53 --- /dev/null +++ b/LibMatrix/MxcUri.cs
@@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; + +namespace LibMatrix; + +public class MxcUri { + public required string ServerName { get; set; } + public required string MediaId { get; set; } + + public static MxcUri Parse([StringSyntax("Uri")] string mxcUri) { + if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri)); + var parts = mxcUri[6..].Split('/'); + if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri)); + return new MxcUri { + ServerName = parts[0], + MediaId = parts[1] + }; + } + + public static implicit operator MxcUri(string mxcUri) => Parse(mxcUri); + public static implicit operator string(MxcUri mxcUri) => $"mxc://{mxcUri.ServerName}/{mxcUri.MediaId}"; + public static implicit operator (string, string)(MxcUri mxcUri) => (mxcUri.ServerName, mxcUri.MediaId); + public static implicit operator MxcUri((string serverName, string mediaId) mxcUri) => (mxcUri.serverName, mxcUri.mediaId); + // public override string ToString() => $"mxc://{ServerName}/{MediaId}"; + + public string ToDownloadUri(string? baseUrl = null, string? filename = null, int? timeout = null) { + var uri = $"{baseUrl}/_matrix/client/v1/media/download/{ServerName}/{MediaId}"; + if (filename is not null) uri += $"/{filename}"; + if (timeout is not null) uri += $"?timeout={timeout}"; + return uri; + } + + public string ToLegacyDownloadUri(string? baseUrl = null, string? filename = null, int? timeout = null) { + var uri = $"{baseUrl}/_matrix/media/v3/download/{ServerName}/{MediaId}"; + if (filename is not null) uri += $"/{filename}"; + if (timeout is not null) uri += $"?timeout_ms={timeout}"; + return uri; + } + + public void Deconstruct(out string serverName, out string mediaId) { + serverName = ServerName; + mediaId = MediaId; + } +} \ No newline at end of file diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
index 6f47183..6933622 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -3,7 +3,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using LibMatrix.EventTypes; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers; namespace LibMatrix.Responses; @@ -26,7 +26,7 @@ public class CreateRoomRequest { //we dont want to use this, we want more control // [JsonPropertyName("preset")] - // public string Preset { get; set; } = null!; + // public string Preset { get; set; } [JsonPropertyName("initial_state")] public List<StateEvent>? InitialState { get; set; } @@ -35,10 +35,11 @@ public class CreateRoomRequest { /// One of: ["public", "private"] /// </summary> [JsonPropertyName("visibility")] + // ReSharper disable once UnusedAutoPropertyAccessor.Global public string? Visibility { get; set; } [JsonPropertyName("power_level_content_override")] - public RoomPowerLevelEventContent? PowerLevelContentOverride { get; set; } = null!; + public RoomPowerLevelEventContent? PowerLevelContentOverride { get; set; } [JsonPropertyName("creation_content")] public JsonObject CreationContent { get; set; } = new(); @@ -46,15 +47,17 @@ public class CreateRoomRequest { [JsonPropertyName("invite")] public List<string>? Invite { get; set; } + public string? RoomVersion { get; set; } + /// <summary> /// For use only when you can't use the CreationContent property /// </summary> - public StateEvent this[string eventType, string eventKey = ""] { + public StateEvent? this[string eventType, string eventKey = ""] { get { - var stateEvent = InitialState.FirstOrDefault(x => x.Type == eventType && x.StateKey == eventKey); + var stateEvent = InitialState?.FirstOrDefault(x => x.Type == eventType && x.StateKey == eventKey); if (stateEvent == null) - InitialState.Add(stateEvent = new StateEvent { + InitialState?.Add(stateEvent = new StateEvent { Type = eventType, StateKey = eventKey, TypedContent = (EventContent)Activator.CreateInstance( @@ -77,7 +80,7 @@ public class CreateRoomRequest { public Dictionary<string, string> Validate() { Dictionary<string, string> errors = new(); - if (!Regex.IsMatch(RoomAliasName, @"[a-zA-Z0-9_\-]+$")) + if (!string.IsNullOrWhiteSpace(RoomAliasName) && !Regex.IsMatch(RoomAliasName, @"[a-zA-Z0-9_\-]+$")) errors.Add("room_alias_name", "Room alias name must only contain letters, numbers, underscores, and hyphens."); @@ -97,7 +100,7 @@ public class CreateRoomRequest { Invite = 25, StateDefault = 10, Redact = 50, - NotificationsPl = new RoomPowerLevelEventContent.NotificationsPL { + NotificationsPl = new RoomPowerLevelEventContent.NotificationsPowerLevels { Room = 10 }, Events = new Dictionary<string, long> { @@ -137,7 +140,7 @@ public class CreateRoomRequest { Invite = 25, StateDefault = 10, Redact = 50, - NotificationsPl = new RoomPowerLevelEventContent.NotificationsPL { + NotificationsPl = new RoomPowerLevelEventContent.NotificationsPowerLevels { Room = 10 }, Events = new Dictionary<string, long> { diff --git a/LibMatrix/Responses/DeviceKeysUploadRequest.cs b/LibMatrix/Responses/DeviceKeysUploadRequest.cs new file mode 100644
index 0000000..c93c4c6 --- /dev/null +++ b/LibMatrix/Responses/DeviceKeysUploadRequest.cs
@@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class DeviceKeysUploadRequest { + [JsonPropertyName("device_keys")] + public DeviceKeysSchema DeviceKeys { get; set; } + + + [JsonPropertyName("one_time_keys")] + public Dictionary<string, OneTimeKey> OneTimeKeys { get; set; } + + public class DeviceKeysSchema { + [JsonPropertyName("algorithms")] + public List<string> Algorithms { get; set; } + } + public class OneTimeKey { + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("signatures")] + public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/Responses/EventIdResponse.cs
index 4d715a4..9e23210 100644 --- a/LibMatrix/EventIdResponse.cs +++ b/LibMatrix/Responses/EventIdResponse.cs
@@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace LibMatrix; +namespace LibMatrix.Responses; public class EventIdResponse { [JsonPropertyName("event_id")] - public string EventId { get; set; } + public required string EventId { get; set; } } \ No newline at end of file diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
index 28fb245..1944276 100644 --- a/LibMatrix/Responses/LoginResponse.cs +++ b/LibMatrix/Responses/LoginResponse.cs
@@ -1,30 +1,24 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using LibMatrix.Homeservers; namespace LibMatrix.Responses; public class LoginResponse { [JsonPropertyName("access_token")] - public string AccessToken { get; set; } = null!; + public string AccessToken { get; set; } [JsonPropertyName("device_id")] - public string DeviceId { get; set; } = null!; - - private string? _homeserver; + public string DeviceId { get; set; } [JsonPropertyName("home_server")] + [field: AllowNull, MaybeNull] public string Homeserver { - get => _homeserver ?? UserId.Split(':', 2).Last(); - protected init => _homeserver = value; + get => field ?? UserId.Split(':', 2).Last(); + set; } [JsonPropertyName("user_id")] - public string UserId { get; set; } = null!; - - // public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) { - // var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver); - // await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(Homeserver, AccessToken, proxy); - // } + public string UserId { get; set; } } public class LoginRequest { diff --git a/LibMatrix/MessagesResponse.cs b/LibMatrix/Responses/MessagesResponse.cs
index 526da74..4912add 100644 --- a/LibMatrix/MessagesResponse.cs +++ b/LibMatrix/Responses/MessagesResponse.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix; +namespace LibMatrix.Responses; public class MessagesResponse { [JsonPropertyName("start")] diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs
index e4addb6..d79e820 100644 --- a/LibMatrix/Responses/SyncResponse.cs +++ b/LibMatrix/Responses/SyncResponse.cs
@@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LibMatrix.EventTypes.Spec.State.RoomInfo; namespace LibMatrix.Responses; @@ -8,16 +9,16 @@ internal partial class SyncResponseSerializerContext : JsonSerializerContext { } public class SyncResponse { [JsonPropertyName("next_batch")] - public string NextBatch { get; set; } = null!; + public string NextBatch { get; set; } [JsonPropertyName("account_data")] public EventList? AccountData { get; set; } [JsonPropertyName("presence")] - public PresenceDataStructure? Presence { get; set; } + public EventList? Presence { get; set; } [JsonPropertyName("device_one_time_keys_count")] - public Dictionary<string, int>? DeviceOneTimeKeysCount { get; set; } = null!; + public Dictionary<string, int>? DeviceOneTimeKeysCount { get; set; } [JsonPropertyName("rooms")] public RoomsDataStructure? Rooms { get; set; } @@ -28,6 +29,9 @@ public class SyncResponse { [JsonPropertyName("device_lists")] public DeviceListsDataStructure? DeviceLists { get; set; } + [JsonPropertyName("gay.rory.libmatrix.msc4222_sync_type")] + public Msc4222SyncType Msc4222Method { get; set; } = Msc4222SyncType.None; + public class DeviceListsDataStructure { [JsonPropertyName("changed")] public List<string>? Changed { get; set; } @@ -39,7 +43,7 @@ public class SyncResponse { // supporting classes public class PresenceDataStructure { [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } = new(); + public List<StateEventResponse>? Events { get; set; } } public class RoomsDataStructure { @@ -61,6 +65,22 @@ public class SyncResponse { [JsonPropertyName("state")] public EventList? State { get; set; } + + [JsonPropertyName("state_after")] + public EventList? StateAfter { get; set; } + + [Obsolete("This property is only used for de/serialisation")] + [JsonPropertyName("org.matrix.msc4222.state_after")] + public EventList? StateAfterUnstable { + get => StateAfter; + set => StateAfter = value; + } + + public override string ToString() { + var lastEvent = Timeline?.Events?.LastOrDefault(x => x.Type == "m.room.member"); + var membership = (lastEvent?.TypedContent as RoomMemberEventContent); + return $"LeftRoomDataStructure: {lastEvent?.Sender} {membership?.Membership} ({membership?.Reason})"; + } } public class JoinedRoomDataStructure { @@ -70,6 +90,16 @@ public class SyncResponse { [JsonPropertyName("state")] public EventList? State { get; set; } + [JsonPropertyName("state_after")] + public EventList? StateAfter { get; set; } + + [Obsolete("This property is only used for de/serialisation")] + [JsonPropertyName("org.matrix.msc4222.state_after")] + public EventList? StateAfterUnstable { + get => StateAfter; + set => StateAfter = value; + } + [JsonPropertyName("account_data")] public EventList? AccountData { get; set; } @@ -82,7 +112,7 @@ public class SyncResponse { [JsonPropertyName("summary")] public SummaryDataStructure? Summary { get; set; } - public class TimelineDataStructure { + public class TimelineDataStructure : EventList { public TimelineDataStructure() { } public TimelineDataStructure(List<StateEventResponse>? events, bool? limited) { @@ -90,8 +120,8 @@ public class SyncResponse { Limited = limited; } - [JsonPropertyName("events")] - public List<StateEventResponse>? Events { get; set; } + // [JsonPropertyName("events")] + // public List<StateEventResponse>? Events { get; set; } [JsonPropertyName("prev_batch")] public string? PrevBatch { get; set; } @@ -125,4 +155,22 @@ public class SyncResponse { public EventList? InviteState { get; set; } } } + + public long GetDerivedSyncTime() { + return ((long[]) [ + AccountData?.Events?.Max(x => x.OriginServerTs) ?? 0, + Presence?.Events?.Max(x => x.OriginServerTs) ?? 0, + ToDevice?.Events?.Max(x => x.OriginServerTs) ?? 0, + Rooms?.Join?.Values?.Max(x => x.Timeline?.Events?.Max(y => y.OriginServerTs)) ?? 0, + Rooms?.Invite?.Values?.Max(x => x.InviteState?.Events?.Max(y => y.OriginServerTs)) ?? 0, + Rooms?.Leave?.Values?.Max(x => x.Timeline?.Events?.Max(y => y.OriginServerTs)) ?? 0 + ]).Max(); + } + + [JsonConverter(typeof(JsonStringEnumConverter<Msc4222SyncType>))] + public enum Msc4222SyncType { + None, + Server, + Emulated + } } \ No newline at end of file diff --git a/LibMatrix/Responses/UserDirectoryResponse.cs b/LibMatrix/Responses/UserDirectoryResponse.cs new file mode 100644
index 0000000..13235d9 --- /dev/null +++ b/LibMatrix/Responses/UserDirectoryResponse.cs
@@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class UserDirectoryResponse { + [JsonPropertyName("limited")] + public bool Limited { get; set; } + + [JsonPropertyName("results")] + public List<UserDirectoryResult> Results { get; set; } + + public class UserDirectoryResult { + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("display_name")] + public string? DisplayName { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + } +} + +public class UserDirectoryRequest { + [JsonPropertyName("search_term")] + public string SearchTerm { get; set; } + + [JsonPropertyName("limit")] + public int? Limit { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/Responses/UserIdAndReason.cs
index 99c9eaf..176cf7c 100644 --- a/LibMatrix/UserIdAndReason.cs +++ b/LibMatrix/Responses/UserIdAndReason.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix; +namespace LibMatrix.Responses; internal class UserIdAndReason(string userId = null!, string reason = null!) { [JsonPropertyName("user_id")] diff --git a/LibMatrix/Responses/UserProfileResponse.cs b/LibMatrix/Responses/UserProfileResponse.cs
index 6c9380f..30e4c32 100644 --- a/LibMatrix/Responses/UserProfileResponse.cs +++ b/LibMatrix/Responses/UserProfileResponse.cs
@@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace LibMatrix.Responses; @@ -8,4 +9,18 @@ public class UserProfileResponse { [JsonPropertyName("displayname")] public string? DisplayName { get; set; } + + // MSC 4133 - Extending User Profile API with Key:Value pairs + [JsonExtensionData] + public Dictionary<string, JsonElement>? CustomKeys { get; set; } + + public JsonElement? this[string key] { + get => CustomKeys?[key]; + set { + if (value is null) + CustomKeys?.Remove(key); + else + (CustomKeys ??= [])[key] = value.Value; + } + } } \ No newline at end of file diff --git a/LibMatrix/WhoAmIResponse.cs b/LibMatrix/Responses/WhoAmIResponse.cs
index 10fff35..db47152 100644 --- a/LibMatrix/WhoAmIResponse.cs +++ b/LibMatrix/Responses/WhoAmIResponse.cs
@@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix; +namespace LibMatrix.Responses; public class WhoAmIResponse { [JsonPropertyName("user_id")] diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 349ccb5..fd4db4d 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -1,5 +1,4 @@ using System.Collections.Frozen; -using System.Diagnostics; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; @@ -8,13 +7,11 @@ using System.Web; using ArcaneLibs.Extensions; using LibMatrix.EventTypes; using LibMatrix.EventTypes.Spec; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Filters; using LibMatrix.Helpers; using LibMatrix.Homeservers; -using LibMatrix.Services; -using Microsoft.Extensions.Logging.Abstractions; +using LibMatrix.Responses; namespace LibMatrix.RoomTypes; @@ -26,8 +23,6 @@ public class GenericRoom { throw new ArgumentException("Room ID cannot be null or whitespace", nameof(roomId)); Homeserver = homeserver; RoomId = roomId; - // if (GetType() != typeof(SpaceRoom)) - if (GetType() == typeof(GenericRoom)) AsSpace = new SpaceRoom(homeserver, RoomId); } public string RoomId { get; set; } @@ -106,7 +101,7 @@ public class GenericRoom { Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync"); var sh = new SyncHelper(Homeserver); var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]); - var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]); + var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []); sh.Filter = new() { Presence = emptyFilter, AccountData = emptyFilter, @@ -121,10 +116,11 @@ public class GenericRoom { var sync = await sh.SyncAsync(); var state = sync.Rooms.Join[RoomId].State.Events; var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey); - if (stateEvent is null) throw new LibMatrixException() { - ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, - Error = "State event not found in sync response" - }; + if (stateEvent is null) + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, + Error = "State event not found in sync response" + }; return stateEvent.EventId; } @@ -137,16 +133,17 @@ public class GenericRoom { return await GetStateEventAsync(type, stateKey); } catch (MatrixException e) { - if (e.ErrorCode == "M_NOT_FOUND") return default; + if (e.ErrorCode == "M_NOT_FOUND") return null; throw; } } - public async Task<MessagesResponse> GetMessagesAsync(string from = "", int? limit = null, string dir = "b", string filter = "") { + public async Task<MessagesResponse> GetMessagesAsync(string from = "", int? limit = null, string dir = "b", string? filter = "") { var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?dir={dir}"; if (!string.IsNullOrWhiteSpace(from)) url += $"&from={from}"; if (limit is not null) url += $"&limit={limit}"; if (!string.IsNullOrWhiteSpace(filter)) url += $"&filter={filter}"; + var res = await Homeserver.ClientHttpClient.GetFromJsonAsync<MessagesResponse>(url); return res; } @@ -154,8 +151,8 @@ public class GenericRoom { /// <summary> /// Same as <see cref="GetMessagesAsync"/>, except keeps fetching more responses until the beginning of the room is found, or the target message limit is reached /// </summary> - public async IAsyncEnumerable<MessagesResponse> GetManyMessagesAsync(string from = "", int limit = 100, string dir = "b", string filter = "", bool includeState = true, - bool fixForward = false, int chunkSize = 100) { + public async IAsyncEnumerable<MessagesResponse> GetManyMessagesAsync(string from = "", int limit = int.MaxValue, string dir = "b", string filter = "", bool includeState = true, + bool fixForward = false, int chunkSize = 250) { if (dir == "f" && fixForward) { var concat = new List<MessagesResponse>(); while (true) { @@ -207,7 +204,7 @@ public class GenericRoom { public async Task<string?> GetNameAsync() => (await GetStateOrNullAsync<RoomNameEventContent>("m.room.name"))?.Name; - public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) { + public async Task<RoomIdResponse> JoinAsync(IEnumerable<string>? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) { if (checkIfAlreadyMember) try { var ser = await GetStateEventOrNullAsync(RoomMemberEventContent.EventId, Homeserver.UserId); @@ -216,68 +213,74 @@ public class GenericRoom { RoomId = RoomId }; } - catch { } //ignore + catch { + // ignored + } var joinUrl = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(RoomId)}"; - Console.WriteLine($"Calling {joinUrl} with {homeservers?.Length ?? 0} via's..."); - if (homeservers == null || homeservers.Length == 0) homeservers = new[] { RoomId.Split(':', 2)[1] }; - var fullJoinUrl = $"{joinUrl}?server_name=" + string.Join("&server_name=", homeservers); + + var materialisedHomeservers = homeservers as string[] ?? homeservers?.ToArray() ?? []; + if (!materialisedHomeservers.Any()) materialisedHomeservers = [RoomId.Split(':', 2)[1]]; + + Console.WriteLine($"Calling {joinUrl} with {materialisedHomeservers.Length} via(s)..."); + + var fullJoinUrl = $"{joinUrl}?server_name=" + string.Join("&server_name=", materialisedHomeservers); + var res = await Homeserver.ClientHttpClient.PostAsJsonAsync(fullJoinUrl, new { reason }); return await res.Content.ReadFromJsonAsync<RoomIdResponse>() ?? throw new Exception("Failed to join room?"); } - public async IAsyncEnumerable<StateEventResponse> GetMembersEnumerableAsync(bool joinedOnly = true) { - // var sw = Stopwatch.StartNew(); - var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); - // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); - // else sw.Restart(); - // var resText = await res.Content.ReadAsStringAsync(); - // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); + public async IAsyncEnumerable<StateEventResponse> GetMembersEnumerableAsync(string? membership = null) { + var url = $"/_matrix/client/v3/rooms/{RoomId}/members"; + var isMembershipSet = !string.IsNullOrWhiteSpace(membership); + if (isMembershipSet) url += $"?membership={membership}"; + var res = await Homeserver.ClientHttpClient.GetAsync(url); var result = await JsonSerializer.DeserializeAsync<ChunkedStateEventResponse>(await res.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); - // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); - // else sw.Restart(); - foreach (var resp in result.Chunk) { - if (resp?.Type != "m.room.member") continue; - if (joinedOnly && resp.RawContent?["membership"]?.GetValue<string>() != "join") continue; + + if (result is null) throw new Exception("Failed to deserialise members response"); + + foreach (var resp in result.Chunk ?? []) { + if (resp.Type != "m.room.member") continue; + if (isMembershipSet && resp.RawContent?["membership"]?.GetValue<string>() != membership) continue; yield return resp; } - - // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); } - public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(bool joinedOnly = true) { - // var sw = Stopwatch.StartNew(); - var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); - // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); - // else sw.Restart(); - // var resText = await res.Content.ReadAsStringAsync(); - // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); + public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(string? membership = null) { + var url = $"/_matrix/client/v3/rooms/{RoomId}/members"; + var isMembershipSet = !string.IsNullOrWhiteSpace(membership); + if (isMembershipSet) url += $"?membership={membership}"; + var res = await Homeserver.ClientHttpClient.GetAsync(url); var result = await JsonSerializer.DeserializeAsync<ChunkedStateEventResponse>(await res.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); - // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); - // else sw.Restart(); + + if (result is null) throw new Exception("Failed to deserialise members response"); + var members = new List<StateEventResponse>(); - foreach (var resp in result.Chunk) { - if (resp?.Type != "m.room.member") continue; - if (joinedOnly && resp.RawContent?["membership"]?.GetValue<string>() != "join") continue; + foreach (var resp in result.Chunk ?? []) { + if (resp.Type != "m.room.member") continue; + if (isMembershipSet && resp.RawContent?["membership"]?.GetValue<string>() != membership) continue; members.Add(resp); } - // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); return members.ToFrozenSet(); } + public async IAsyncEnumerable<string> GetMemberIdsEnumerableAsync(string? membership = null) { + await foreach (var evt in GetMembersEnumerableAsync(membership)) + yield return evt.StateKey!; + } + + public async Task<FrozenSet<string>> GetMemberIdsListAsync(string? membership = null) { + var members = await GetMembersListAsync(membership); + return members.Select(x => x.StateKey!).ToFrozenSet(); + } + #region Utility shortcuts public Task<EventIdResponse> SendMessageEventAsync(RoomMessageEventContent content) => @@ -285,7 +288,7 @@ public class GenericRoom { public async Task<List<string>?> GetAliasesAsync() { var res = await GetStateAsync<RoomAliasEventContent>("m.room.aliases"); - return res.Aliases; + return res?.Aliases; } public Task<RoomCanonicalAliasEventContent?> GetCanonicalAliasAsync() => @@ -317,16 +320,17 @@ public class GenericRoom { public Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() => GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels"); - [Obsolete("This method will be merged into GetNameAsync() in the future.")] public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) { try { - return await GetNameAsync(); + var name = await GetNameAsync(); + if (!string.IsNullOrEmpty(name)) return name; + throw new(); } catch { try { var alias = await GetCanonicalAliasAsync(); - if (alias?.Alias is not null) return alias.Alias; - throw new Exception("No name or alias"); + if (!string.IsNullOrWhiteSpace(alias?.Alias)) return alias.Alias; + throw new Exception("No alias"); } catch { try { @@ -334,7 +338,8 @@ public class GenericRoom { var memberList = new List<string>(); var memberCount = 0; await foreach (var member in members) - memberList.Add(member.RawContent?["displayname"]?.GetValue<string>() ?? ""); + if (member.StateKey != Homeserver.UserId) + memberList.Add(member.RawContent?["displayname"]?.GetValue<string>() ?? ""); memberCount = memberList.Count; memberList.RemoveAll(string.IsNullOrWhiteSpace); memberList = memberList.OrderBy(x => x).ToList(); @@ -374,9 +379,9 @@ public class GenericRoom { await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", new UserIdAndReason { UserId = userId, Reason = reason }); - public async Task UnbanAsync(string userId) => + public async Task UnbanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", - new UserIdAndReason { UserId = userId }); + new UserIdAndReason { UserId = userId, Reason = reason }); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null) @@ -393,7 +398,7 @@ public class GenericRoom { .Content.ReadFromJsonAsync<EventIdResponse>(); public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) => - await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content)) + await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, TimelineEventContent content) { @@ -404,6 +409,15 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<EventIdResponse>() ?? throw new Exception("Failed to send event"); } + public async Task<EventIdResponse> SendReactionAsync(string eventId, string key) => + await SendTimelineEventAsync("m.reaction", new RoomMessageReactionEventContent() { + RelatesTo = new() { + RelationType = "m.annotation", + EventId = eventId, + Key = key + } + }); + public async Task<EventIdResponse?> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file", string contentType = "application/octet-stream") { var url = await Homeserver.UploadFile(fileName, fileStream); var content = new RoomMessageEventContent() { @@ -429,6 +443,16 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<T>(); } + public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) { + try { + return await GetRoomAccountDataAsync<T>(key); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") return default; + throw; + } + } + public async Task SetRoomAccountDataAsync(string key, object data) { var res = await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data); if (!res.IsSuccessStatusCode) { @@ -437,13 +461,65 @@ public class GenericRoom { } } - public Task<StateEventResponse> GetEventAsync(string eventId) => - Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}"); + public Task<StateEventResponse> GetEventAsync(string eventId, bool includeUnredactedContent = false) => + Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}?fi.mau.msc2815.include_unredacted_content={includeUnredactedContent}"); - public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) { + public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) { var data = new { reason }; - return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync( - $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + var url = $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid().ToString()}"; + while (true) { + try { + return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(url, data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + } + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw; + throw; + } + } + } + +#endregion + +#region Ephemeral Events + + /// <summary> + /// This tells the server that the user is typing for the next N milliseconds where + /// N is the value specified in the timeout key. Alternatively, if typing is false, + /// it tells the server that the user has stopped typing. + /// </summary> + /// <param name="typing">Whether the user is typing or not.</param> + /// <param name="timeout">The length of time in milliseconds to mark this user as typing.</param> + public async Task SendTypingNotificationAsync(bool typing, int timeout = 30000) { + await Homeserver.ClientHttpClient.PutAsJsonAsync( + $"/_matrix/client/v3/rooms/{RoomId}/typing/{Homeserver.UserId}", new JsonObject { + ["timeout"] = typing ? timeout : null, + ["typing"] = typing + }, new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + /// <summary> + /// Updates the marker for the given receipt type to the event ID specified. + /// </summary> + /// <param name="eventId">The event ID to acknowledge up to.</param> + /// <param name="threadId"> + /// The root thread event’s ID (or main) for which thread this receipt is intended to be under. + /// If not specified, the read receipt is unthreaded (default). + /// </param> + /// <param name="isPrivate"> + /// If set to true, a receipt type of m.read.private is sent instead of m.read, which marks the + /// room as "read" only for the current user + /// </param> + public async Task SendReadReceiptAsync(string eventId, string? threadId = null, bool isPrivate = false) { + var request = new JsonObject(); + if (threadId != null) + request.Add("thread_id", threadId); + await Homeserver.ClientHttpClient.PostAsJsonAsync( + $"/_matrix/client/v3/rooms/{RoomId}/receipt/m.read{(isPrivate ? ".private" : "")}/{eventId}", request, + new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); } #endregion @@ -510,7 +586,7 @@ public class GenericRoom { var uri = new Uri(path, UriKind.Relative); if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir); - else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); + else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from); if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString()); if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString()); @@ -528,10 +604,11 @@ public class GenericRoom { } } - public readonly SpaceRoom AsSpace; + public SpaceRoom AsSpace() => new SpaceRoom(Homeserver, RoomId); + public PolicyRoom AsPolicyRoom() => new PolicyRoom(Homeserver, RoomId); } public class RoomIdResponse { [JsonPropertyName("room_id")] - public string RoomId { get; set; } = null!; + public string RoomId { get; set; } } \ No newline at end of file diff --git a/LibMatrix/RoomTypes/PolicyRoom.cs b/LibMatrix/RoomTypes/PolicyRoom.cs new file mode 100644
index 0000000..c6eec63 --- /dev/null +++ b/LibMatrix/RoomTypes/PolicyRoom.cs
@@ -0,0 +1,51 @@ +using System.Collections.Frozen; +using LibMatrix.EventTypes; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Homeservers; + +namespace LibMatrix.RoomTypes; + +public class PolicyRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) { + public const string TypeName = "support.feline.policy.lists.msc.v1"; + + public static readonly FrozenSet<string> UserPolicyEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>().ToFrozenSet(); + public static readonly FrozenSet<string> ServerPolicyEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>().ToFrozenSet(); + public static readonly FrozenSet<string> RoomPolicyEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>().ToFrozenSet(); + public static readonly FrozenSet<string> SpecPolicyEventTypes = [..UserPolicyEventTypes, ..ServerPolicyEventTypes, ..RoomPolicyEventTypes]; + + public async IAsyncEnumerable<StateEventResponse> GetPoliciesAsync() { + var fullRoomState = GetFullStateAsync(); + await foreach (var eventResponse in fullRoomState) { + if (SpecPolicyEventTypes.Contains(eventResponse!.Type)) { + yield return eventResponse; + } + } + } + + public async IAsyncEnumerable<StateEventResponse> GetUserPoliciesAsync() { + var fullRoomState = GetPoliciesAsync(); + await foreach (var eventResponse in fullRoomState) { + if (UserPolicyEventTypes.Contains(eventResponse!.Type)) { + yield return eventResponse; + } + } + } + + public async IAsyncEnumerable<StateEventResponse> GetServerPoliciesAsync() { + var fullRoomState = GetPoliciesAsync(); + await foreach (var eventResponse in fullRoomState) { + if (ServerPolicyEventTypes.Contains(eventResponse!.Type)) { + yield return eventResponse; + } + } + } + + public async IAsyncEnumerable<StateEventResponse> GetRoomPoliciesAsync() { + var fullRoomState = GetPoliciesAsync(); + await foreach (var eventResponse in fullRoomState) { + if (RoomPolicyEventTypes.Contains(eventResponse!.Type)) { + yield return eventResponse; + } + } + } +} \ No newline at end of file diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
index b40ccc6..96abd77 100644 --- a/LibMatrix/RoomTypes/SpaceRoom.cs +++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -1,9 +1,12 @@ using ArcaneLibs.Extensions; using LibMatrix.Homeservers; +using LibMatrix.Responses; namespace LibMatrix.RoomTypes; public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) { + public const string TypeName = "m.space"; + public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) { // var rooms = new List<GenericRoom>(); var state = GetFullStateAsync(); @@ -15,7 +18,7 @@ public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) } public async Task<EventIdResponse> AddChildAsync(GenericRoom room) { - var members = room.GetMembersEnumerableAsync(true); + var members = room.GetMembersEnumerableAsync("join"); Dictionary<string, int> memberCountByHs = new(); await foreach (var member in members) { var server = member.StateKey.Split(':')[1]; @@ -31,7 +34,7 @@ public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) }); return resp; } - + public async Task<EventIdResponse> AddChildByIdAsync(string id) { return await AddChildAsync(Homeserver.GetRoom(id)); } diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 3e42c21..36bc828 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -1,6 +1,5 @@ using System.Net.Http.Json; using ArcaneLibs.Collections; -using ArcaneLibs.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; using Microsoft.Extensions.Logging; @@ -8,9 +7,9 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Services; public class HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService hsResolver) { - private static SemaphoreCache<AuthenticatedHomeserverGeneric> AuthenticatedHomeserverCache = new(); - private static SemaphoreCache<RemoteHomeserver> RemoteHomeserverCache = new(); - private static SemaphoreCache<FederationClient> FederationClientCache = new(); + private static readonly SemaphoreCache<AuthenticatedHomeserverGeneric> AuthenticatedHomeserverCache = new(); + private static readonly SemaphoreCache<RemoteHomeserver> RemoteHomeserverCache = new(); + private static readonly SemaphoreCache<FederationClient> FederationClientCache = new(); public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedWithToken(string homeserver, string accessToken, string? proxy = null, string? impersonatedMxid = null, bool useGeneric = false, bool enableClient = true, bool enableServer = true) { @@ -23,9 +22,11 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger AuthenticatedHomeserverGeneric? hs = null; if (!useGeneric) { - ClientVersionsResponse? clientVersions = new(); + var clientVersionsTask = rhs.GetClientVersionsAsync(); + var serverVersionTask = rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)!; + ClientVersionsResponse clientVersions = new(); try { - clientVersions = await rhs.GetClientVersionsAsync(); + clientVersions = await clientVersionsTask; } catch (Exception e) { logger.LogError(e, "Failed to get client versions for {homeserver}", homeserver); @@ -33,7 +34,7 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger ServerVersionResponse? serverVersion; try { - serverVersion = await (rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)!); + serverVersion = await serverVersionTask; } catch (Exception e) { logger.LogWarning(e, "Failed to get server version for {homeserver}", homeserver); @@ -46,6 +47,8 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger else { if (serverVersion is { Server.Name: "Synapse" }) hs = new AuthenticatedHomeserverSynapse(homeserver, wellKnownUris, proxy, accessToken); + else if (serverVersion is { Server.Name: "LibMatrix.HomeserverEmulator"}) + hs = new AuthenticatedHomeserverHSE(homeserver, wellKnownUris, proxy, accessToken); } } catch (Exception e) { @@ -65,7 +68,7 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger }); } - public async Task<RemoteHomeserver> GetRemoteHomeserver(string homeserver, string? proxy = null, bool useCache = true, bool enableServer = true) => + public async Task<RemoteHomeserver> GetRemoteHomeserver(string homeserver, string? proxy = null, bool useCache = true, bool enableServer = false) => useCache ? await RemoteHomeserverCache.GetOrAdd($"{homeserver}{proxy}", async () => { return new RemoteHomeserver(homeserver, await hsResolver.ResolveHomeserverFromWellKnown(homeserver, enableServer: enableServer), proxy); }) diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index f899230..94a3826 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -1,8 +1,4 @@ -using System.Collections.Concurrent; using System.Diagnostics; -using System.Net.Http.Json; -using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ArcaneLibs.Collections; using ArcaneLibs.Extensions; @@ -13,9 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Services; public class HomeserverResolverService { - private readonly MatrixHttpClient _httpClient = new() { - // Timeout = TimeSpan.FromSeconds(60) // TODO: Re-implement this - }; + private readonly MatrixHttpClient _httpClient = new(); private static readonly SemaphoreCache<WellKnownUris> WellKnownCache = new(); @@ -26,68 +20,67 @@ public class HomeserverResolverService { if (logger is NullLogger<HomeserverResolverService>) { var stackFrame = new StackTrace(true).GetFrame(1); Console.WriteLine( - $"WARN | Null logger provided to HomeserverResolverService!\n{stackFrame.GetMethod().DeclaringType} at {stackFrame.GetFileName()}:{stackFrame.GetFileLineNumber()}"); + $"WARN | Null logger provided to HomeserverResolverService!\n{stackFrame?.GetMethod()?.DeclaringType?.ToString() ?? "null"} at {stackFrame?.GetFileName() ?? "null"}:{stackFrame?.GetFileLineNumber().ToString() ?? "null"}"); } } - // private static SemaphoreSlim _wellKnownSemaphore = new(1, 1); - public async Task<WellKnownUris> ResolveHomeserverFromWellKnown(string homeserver, bool enableClient = true, bool enableServer = true) { ArgumentNullException.ThrowIfNull(homeserver); return await WellKnownCache.GetOrAdd(homeserver, async () => { - // await _wellKnownSemaphore.WaitAsync(); _logger.LogTrace($"Resolving homeserver well-knowns: {homeserver}"); var client = enableClient ? _tryResolveClientEndpoint(homeserver) : null; var server = enableServer ? _tryResolveServerEndpoint(homeserver) : null; var res = new WellKnownUris(); - // try { if (client != null) - res.Client = await client ?? throw new Exception($"Could not resolve client URL for {homeserver}."); - // } - // catch (Exception e) { - // _logger.LogError(e, "Error resolving client well-known for {hs}", homeserver); - // } + res.Client = (await client)?.TrimEnd('/') ?? throw new Exception($"Could not resolve client URL for {homeserver}."); - // try { if (server != null) - res.Server = await server ?? throw new Exception($"Could not resolve server URL for {homeserver}."); - // } - // catch (Exception e) { - // _logger.LogError(e, "Error resolving server well-known for {hs}", homeserver); - // } + res.Server = (await server)?.TrimEnd('/') ?? throw new Exception($"Could not resolve server URL for {homeserver}."); _logger.LogInformation("Resolved well-knowns for {hs}: {json}", homeserver, res.ToJson(indent: false)); - // _wellKnownSemaphore.Release(); return res; }); } - - // private async Task<WellKnownUris> InternalResolveHomeserverFromWellKnown(string homeserver) { - - // } - + + private async Task<T?> GetFromJsonAsync<T>(string url) { + try { + return await _httpClient.GetFromJsonAsync<T>(url); + } + catch (Exception e) { + _logger.LogWarning(e, "Failed to get JSON from {url}", url); + return default; + } + } + private async Task<string?> _tryResolveClientEndpoint(string homeserver) { ArgumentNullException.ThrowIfNull(homeserver); _logger.LogTrace("Resolving client well-known: {homeserver}", homeserver); ClientWellKnown? clientWellKnown = null; + homeserver = homeserver.TrimEnd('/'); // check if homeserver has a client well-known if (homeserver.StartsWith("https://")) { - clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); + clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); + + if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) + return homeserver; } else if (homeserver.StartsWith("http://")) { - clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); + clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); + + if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) + return homeserver; } else { - clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client"); - clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client"); + clientWellKnown ??= await GetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client"); + clientWellKnown ??= await GetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client"); if (clientWellKnown is null) { - if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/_matrix/client/versions")) + if (await MatrixHttpClient.CheckSuccessStatus($"https://{homeserver}/_matrix/client/versions")) return $"https://{homeserver}"; - if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/_matrix/client/versions")) + if (await MatrixHttpClient.CheckSuccessStatus($"http://{homeserver}/_matrix/client/versions")) return $"http://{homeserver}"; } } @@ -104,40 +97,42 @@ public class HomeserverResolverService { ArgumentNullException.ThrowIfNull(homeserver); _logger.LogTrace($"Resolving server well-known: {homeserver}"); ServerWellKnown? serverWellKnown = null; + homeserver = homeserver.TrimEnd('/'); // check if homeserver has a server well-known if (homeserver.StartsWith("https://")) { - serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); + serverWellKnown = await GetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); } else if (homeserver.StartsWith("http://")) { - serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); + serverWellKnown = await GetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); } else { - serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server"); - serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server"); + serverWellKnown ??= await GetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server"); + serverWellKnown ??= await GetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server"); } _logger.LogInformation("Server well-known for {hs}: {json}", homeserver, serverWellKnown?.ToJson() ?? "null"); if (!string.IsNullOrWhiteSpace(serverWellKnown?.Homeserver)) { - var resolved = serverWellKnown.Homeserver; + var resolved = serverWellKnown.Homeserver.TrimEnd('/'); if (resolved.StartsWith("https://") || resolved.StartsWith("http://")) return resolved; - if (await _httpClient.CheckSuccessStatus($"https://{resolved}/_matrix/federation/v1/version")) + if (await MatrixHttpClient.CheckSuccessStatus($"https://{resolved}/_matrix/federation/v1/version")) return $"https://{resolved}"; - if (await _httpClient.CheckSuccessStatus($"http://{resolved}/_matrix/federation/v1/version")) + if (await MatrixHttpClient.CheckSuccessStatus($"http://{resolved}/_matrix/federation/v1/version")) return $"http://{resolved}"; _logger.LogWarning("Server well-known points to invalid server: {resolved}", resolved); } // fallback: most servers host C2S and S2S on the same domain - var clientUrl = await _tryResolveClientEndpoint(homeserver); - if (clientUrl is not null && await _httpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version")) + var clientUrl = (await _tryResolveClientEndpoint(homeserver)).TrimEnd('/'); + if (clientUrl is not null && await MatrixHttpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version")) return clientUrl; _logger.LogInformation("No server well-known for {server}...", homeserver); return null; } - + + [Obsolete("Use authenticated media, available on AuthenticatedHomeserverGeneric", true)] public async Task<string?> ResolveMediaUri(string homeserver, string mxc) { if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); if (mxc is null) throw new ArgumentNullException(nameof(mxc)); diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
index 06ea9de..5ffd43a 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -1,27 +1,25 @@ +using LibMatrix.Services.WellKnownResolver; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace LibMatrix.Services; public static class ServiceInstaller { public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) { - //Check required services - // if (!services.Any(x => x.ServiceType == typeof(TieredStorageService))) - // throw new Exception("[RMUCore/DI] No TieredStorageService has been registered!"); //Add config services.AddSingleton(config ?? new RoryLibMatrixConfiguration()); //Add services - services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>())); - - // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) { + services.AddSingleton<ClientWellKnownResolver>(); + services.AddSingleton<ServerWellKnownResolver>(); + services.AddSingleton<SupportWellKnownResolver>(); + if (!services.Any(x => x.ServiceType == typeof(WellKnownResolverConfiguration))) + services.AddSingleton<WellKnownResolverConfiguration>(); + services.AddSingleton<WellKnownResolverService>(); + // Legacy + services.AddSingleton<HomeserverResolverService>(); services.AddSingleton<HomeserverProviderService>(); - // } - // else { - // services.AddScoped<HomeserverProviderService>(); - // } - // services.AddScoped<MatrixHttpClient>(); return services; } } diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs new file mode 100644
index 0000000..26a4c43 --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs
@@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Services.WellKnownResolver; + +public class WellKnownResolverConfiguration { + /// <summary> + /// Allow transparent downgrades to plaintext HTTP if HTTPS fails + /// Enabling this is unsafe! + /// </summary> + [JsonPropertyName("allow_http")] + public bool AllowHttp { get; set; } = false; + + /// <summary> + /// Use DNS resolution if available, for resolving SRV records + /// </summary> + [JsonPropertyName("allow_dns")] + public bool AllowDns { get; set; } = true; + + /// <summary> + /// Use system resolver(s) if empty + /// </summary> + [JsonPropertyName("dns_servers")] + public List<string> DnsServers { get; set; } = new(); + + /// <summary> + /// Same as AllowDns, but for DNS over HTTPS - useful in browser contexts + /// </summary> + [JsonPropertyName("allow_doh")] + public bool AllowDoh { get; set; } = true; + + /// <summary> + /// Use DNS over HTTPS - useful in browser contexts + /// Disabled if empty + /// </summary> + [JsonPropertyName("doh_servers")] + public List<string> DohServers { get; set; } = new(); + + /// <summary> + /// Whether to allow fallback subdomain lookups + /// </summary> + [JsonPropertyName("allow_fallback_subdomains")] + public bool AllowFallbackSubdomains { get; set; } = true; + + /// <summary> + /// Fallback subdomains to try if the homeserver is not found + /// </summary> + [JsonPropertyName("fallback_subdomains")] + public List<string> FallbackSubdomains { get; set; } = ["matrix", "chat", "im"]; +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs new file mode 100644
index 0000000..4c78347 --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
@@ -0,0 +1,91 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace LibMatrix.Services.WellKnownResolver; + +public class WellKnownResolverService { + private readonly MatrixHttpClient _httpClient = new(); + + private readonly ILogger<WellKnownResolverService> _logger; + private readonly ClientWellKnownResolver _clientWellKnownResolver; + private readonly SupportWellKnownResolver _supportWellKnownResolver; + private readonly ServerWellKnownResolver _serverWellKnownResolver; + private readonly WellKnownResolverConfiguration _configuration; + + public WellKnownResolverService(ILogger<WellKnownResolverService> logger, ClientWellKnownResolver clientWellKnownResolver, SupportWellKnownResolver supportWellKnownResolver, + WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver) { + _logger = logger; + _clientWellKnownResolver = clientWellKnownResolver; + _supportWellKnownResolver = supportWellKnownResolver; + _configuration = configuration; + _serverWellKnownResolver = serverWellKnownResolver; + if (logger is NullLogger<WellKnownResolverService>) { + var stackFrame = new StackTrace(true).GetFrame(1); + Console.WriteLine( + $"WARN | Null logger provided to WellKnownResolverService!\n{stackFrame?.GetMethod()?.DeclaringType?.ToString() ?? "null"} at {stackFrame?.GetFileName() ?? "null"}:{stackFrame?.GetFileLineNumber().ToString() ?? "null"}"); + } + } + + public async Task<WellKnownRecords> TryResolveWellKnownRecords(string homeserver, bool includeClient = true, bool includeServer = true, bool includeSupport = true, + WellKnownResolverConfiguration? config = null) { + WellKnownRecords records = new(); + _logger.LogDebug($"Resolving well-knowns for {homeserver}"); + if (includeClient && await _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } clientResult) { + records.ClientWellKnown = clientResult; + } + + if (includeServer && await _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } serverResult) { + records.ServerWellKnown = serverResult; + } + + if (includeSupport && await _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } supportResult) { + records.SupportWellKnown = supportResult; + } + + return records; + } + + public class WellKnownRecords { + public WellKnownResolutionResult<ClientWellKnown?>? ClientWellKnown { get; set; } + public WellKnownResolutionResult<ServerWellKnown?>? ServerWellKnown { get; set; } + public WellKnownResolutionResult<SupportWellKnown?>? SupportWellKnown { get; set; } + } + + public class WellKnownResolutionResult<T> { + public WellKnownSource Source { get; set; } + public string? SourceUri { get; set; } + public T? Content { get; set; } + public List<WellKnownResolutionWarning> Warnings { get; set; } = []; + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WellKnownSource { + None, + Https, + Dns, + Http, + ManualCheck, + Search + } + + public struct WellKnownResolutionWarning { + public WellKnownResolutionWarningType Type { get; set; } + public string Message { get; set; } + [JsonIgnore] + public Exception? Exception { get; set; } + public string? ExceptionMessage => Exception?.Message; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WellKnownResolutionWarningType { + None, + Exception, + InvalidResponse, + Timeout, + SlowResponse + } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs new file mode 100644
index 0000000..cbe5b0a --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs
@@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Net.Http.Json; +using ArcaneLibs.Collections; +using LibMatrix.Extensions; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class BaseWellKnownResolver<T> where T : class, new() { + internal static readonly SemaphoreCache<WellKnownResolverService.WellKnownResolutionResult<T>> WellKnownCache = new() { + StoreNulls = false + }; + + internal static readonly MatrixHttpClient HttpClient = new(); + + internal async Task<WellKnownResolverService.WellKnownResolutionResult<T>> TryGetWellKnownFromUrl(string url, + WellKnownResolverService.WellKnownSource source) { + var sw = Stopwatch.StartNew(); + try { + var request = await HttpClient.GetAsync(url); + sw.Stop(); + var result = new WellKnownResolverService.WellKnownResolutionResult<T> { + Content = await request.Content.ReadFromJsonAsync<T>(), + Source = source, + SourceUri = url, + Warnings = [] + }; + + if (sw.ElapsedMilliseconds > 1000) { + // logger.LogWarning($"Support well-known resolution took {sw.ElapsedMilliseconds}ms: {url}"); + result.Warnings.Add(new() { + Type = WellKnownResolverService.WellKnownResolutionWarning.WellKnownResolutionWarningType.SlowResponse, + Message = $"Well-known resolution took {sw.ElapsedMilliseconds}ms" + }); + } + + return result; + } + catch (Exception e) { + return new WellKnownResolverService.WellKnownResolutionResult<T> { + Source = source, + SourceUri = url, + Warnings = [ + new() { + Exception = e, + Type = WellKnownResolverService.WellKnownResolutionWarning.WellKnownResolutionWarningType.Exception, + Message = e.Message + } + ] + }; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs new file mode 100644
index 0000000..f8de38d --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
@@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; +using ArcaneLibs.Collections; +using LibMatrix.Extensions; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown; +using ResultType = + LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown?>; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class ClientWellKnownResolver(ILogger<ClientWellKnownResolver> logger, WellKnownResolverConfiguration configuration) + : BaseWellKnownResolver<ClientWellKnown> { + private static readonly SemaphoreCache<WellKnownResolverService.WellKnownResolutionResult<ClientWellKnown>> ClientWellKnownCache = new() { + StoreNulls = false + }; + + private static readonly MatrixHttpClient HttpClient = new(); + + public Task<WellKnownResolverService.WellKnownResolutionResult<ClientWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving client well-known: {homeserver}"); + + WellKnownResolverService.WellKnownResolutionResult<ClientWellKnown> result = + await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/client", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) return result; + + + return result; + }); + } +} + +public class ClientWellKnown { + [JsonPropertyName("m.homeserver")] + public WellKnownHomeserver Homeserver { get; set; } + + public class WellKnownHomeserver { + [JsonPropertyName("base_url")] + public required string BaseUrl { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs new file mode 100644
index 0000000..a99185c --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
@@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using ArcaneLibs.Collections; +using LibMatrix.Extensions; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown; +using ResultType = + LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown?>; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class ServerWellKnownResolver(ILogger<ServerWellKnownResolver> logger, WellKnownResolverConfiguration configuration) + : BaseWellKnownResolver<ServerWellKnown> { + private static readonly SemaphoreCache<WellKnownResolverService.WellKnownResolutionResult<ServerWellKnown>> ClientWellKnownCache = new() { + StoreNulls = false + }; + + private static readonly MatrixHttpClient HttpClient = new(); + + public Task<WellKnownResolverService.WellKnownResolutionResult<ServerWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving client well-known: {homeserver}"); + + WellKnownResolverService.WellKnownResolutionResult<ServerWellKnown> result = + await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/server", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) return result; + + + return result; + }); + } +} + + +public class ServerWellKnown { + [JsonPropertyName("m.server")] + public string Homeserver { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs new file mode 100644
index 0000000..99313db --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
@@ -0,0 +1,44 @@ +using System.Diagnostics; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown; +using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult< + LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown? +>; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class SupportWellKnownResolver(ILogger<SupportWellKnownResolver> logger, WellKnownResolverConfiguration configuration) : BaseWellKnownResolver<WellKnownType> { + public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return WellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving support well-known: {homeserver}"); + + ResultType result = await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/support", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) + return result; + + return null; + }); + } +} + +public class SupportWellKnown { + [JsonPropertyName("contacts")] + public List<WellKnownContact>? Contacts { get; set; } + + [JsonPropertyName("support_page")] + public Uri? SupportPage { get; set; } + + public class WellKnownContact { + [JsonPropertyName("email_address")] + public string? EmailAddress { get; set; } + + [JsonPropertyName("matrix_id")] + public string? MatrixId { get; set; } + + [JsonPropertyName("role")] + public required string Role { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 81ee3fe..af25805 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs
@@ -13,7 +13,7 @@ using LibMatrix.Extensions; namespace LibMatrix; public class StateEvent { - public static FrozenSet<Type> KnownStateEventTypes { get; } = new ClassCollector<EventContent>().ResolveFromAllAccessibleAssemblies().ToFrozenSet(); + public static FrozenSet<Type> KnownStateEventTypes { get; } = ClassCollector<EventContent>.ResolveFromAllAccessibleAssemblies().ToFrozenSet(); public static FrozenDictionary<string, Type> KnownStateEventTypesByName { get; } = KnownStateEventTypes.Aggregate( new Dictionary<string, Type>(), @@ -44,6 +44,7 @@ public class StateEvent { public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type; private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() { + // We need these, NumberHandling covers other number types that we don't want to convert Converters = { new JsonFloatStringConverter(), new JsonDoubleStringConverter(), @@ -55,9 +56,6 @@ public class StateEvent { [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public EventContent? TypedContent { get { - // if (Type == "m.receipt") { - // return null; - // } try { var mappedType = GetStateEventType(Type); if (mappedType == typeof(UnknownEventContent)) @@ -81,6 +79,18 @@ public class StateEvent { } } + public T? ContentAs<T>() { + try { + return RawContent.Deserialize<T>(TypedContentSerializerOptions)!; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null")); + } + + return default; + } + [JsonPropertyName("state_key")] public string? StateKey { get; set; } @@ -90,44 +100,10 @@ public class StateEvent { [JsonPropertyName("replaces_state")] public string? ReplacesState { get; set; } - private JsonObject? _rawContent; - [JsonPropertyName("content")] - public JsonObject? RawContent { - get => _rawContent; - set => _rawContent = value; - } - // - // [JsonIgnore] - // public new Type GetType { - // get { - // var type = GetStateEventType(Type); - // - // //special handling for some types - // // if (type == typeof(RoomEmotesEventContent)) { - // // RawContent["emote"] = RawContent["emote"]?.AsObject() ?? new JsonObject(); - // // } - // // - // // if (this is StateEventResponse stateEventResponse) { - // // if (type == null || type == typeof(object)) { - // // Console.WriteLine($"Warning: unknown event type '{Type}'!"); - // // Console.WriteLine(RawContent.ToJson()); - // // Directory.CreateDirectory($"unknown_state_events/{Type}"); - // // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json", - // // RawContent.ToJson()); - // // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json"); - // // } - // // else if (RawContent is not null && RawContent.FindExtraJsonObjectFields(type)) { - // // Directory.CreateDirectory($"unknown_state_events/{Type}"); - // // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json", - // // RawContent.ToJson()); - // // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json"); - // // } - // // } - // - // return type; - // } - // } + // [field: AllowNull, MaybeNull] + [NotNull] + public JsonObject? RawContent { get; set; } //debug [JsonIgnore] @@ -143,6 +119,18 @@ public class StateEvent { [JsonIgnore] public string InternalContentTypeName => TypedContent?.GetType().Name ?? "null"; + + public static bool TypeKeyPairMatches(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey; + public static bool Equals(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey && x.EventId == y.EventId; + + /// <summary> + /// Compares two state events for deep equality, including type, state key, and raw content. + /// If you trust the server, use Equals instead, as that compares by event ID instead of raw content. + /// </summary> + /// <param name="x"></param> + /// <param name="y"></param> + /// <returns></returns> + public static bool DeepEquals(StateEventResponse x, StateEventResponse y) => x.Type == y.Type && x.StateKey == y.StateKey && JsonNode.DeepEquals(x.RawContent, y.RawContent); } public class StateEventResponse : StateEvent { @@ -156,13 +144,13 @@ public class StateEventResponse : StateEvent { public string? Sender { get; set; } [JsonPropertyName("unsigned")] - public UnsignedData? Unsigned { get; set; } + public JsonObject? Unsigned { get; set; } [JsonPropertyName("event_id")] public string? EventId { get; set; } public class UnsignedData { - [JsonPropertyName("age")] + [JsonPropertyName("age"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public ulong? Age { get; set; } [JsonPropertyName("redacted_because")] @@ -254,4 +242,23 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR } */ +/* +public class ForgivingObjectConverter<T> : JsonConverter<T> where T : new() { + public override T? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { + try { + var text = JsonDocument.ParseValue(ref reader).RootElement.GetRawText(); + return JsonSerializer.Deserialize<T>(text, options); + } + catch (JsonException ex) { + Console.WriteLine(ex); + return null; + } + } + + public override bool CanConvert(Type typeToConvert) => true; + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => JsonSerializer.Serialize<T>(writer, value, options); +}*/ + #endregion \ No newline at end of file diff --git a/LibMatrix/Utilities/CommonSyncFilters.cs b/LibMatrix/Utilities/CommonSyncFilters.cs
index bf8b987..503cc1f 100644 --- a/LibMatrix/Utilities/CommonSyncFilters.cs +++ b/LibMatrix/Utilities/CommonSyncFilters.cs
@@ -1,7 +1,7 @@ using System.Collections.Frozen; using LibMatrix.EventTypes.Common; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.EventTypes.Spec.State.Space; using LibMatrix.Filters; namespace LibMatrix.Utilities; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs deleted file mode 100644
index 66548e2..0000000 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs +++ /dev/null
@@ -1,69 +0,0 @@ -using System.Text.Json.Nodes; -using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; -using LibMatrix.Services; -using Microsoft.AspNetCore.Mvc; - -namespace LibMatrix.HomeserverEmulator.Controllers; - -[ApiController] -[Route("/_matrix/client/{version}/")] -public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService) : ControllerBase { - [HttpPost("login")] - public async Task<LoginResponse> Login(LoginRequest request) { - if (!request.Identifier.User.StartsWith('@')) - request.Identifier.User = $"@{request.Identifier.User}:{tokenService.GenerateServerName(HttpContext)}"; - if (request.Identifier.User.EndsWith("localhost")) - request.Identifier.User = request.Identifier.User.Replace("localhost", tokenService.GenerateServerName(HttpContext)); - - var user = await userStore.GetUserById(request.Identifier.User); - if (user is null) { - user = await userStore.CreateUser(request.Identifier.User); - } - - return user.Login(); - } - - [HttpGet("login")] - public async Task<LoginFlowsResponse> GetLoginFlows() { - return new LoginFlowsResponse { - Flows = ((string[]) [ - "m.login.password", - "m.login.recaptcha", - "m.login.sso", - "m.login.email.identity", - "m.login.msisdn", - "m.login.dummy", - "m.login.registration_token", - ]).Select(x => new LoginFlowsResponse.LoginFlow { Type = x }).ToList() - }; - } - - [HttpPost("logout")] - public async Task<object> Logout() { - var token = tokenService.GetAccessToken(HttpContext); - var user = await userStore.GetUserByToken(token); - if (user == null) - throw new MatrixException() { - ErrorCode = "M_UNKNOWN_TOKEN", - Error = "No such user" - }; - - if (!user.AccessTokens.ContainsKey(token)) - throw new MatrixException() { - ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND, - Error = "Token not found" - }; - - user.AccessTokens.Remove(token); - return new { }; - } -} - -public class LoginFlowsResponse { - public required List<LoginFlow> Flows { get; set; } - - public class LoginFlow { - public required string Type { get; set; } - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs b/Tests/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs deleted file mode 100644
index 52d5932..0000000 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs +++ /dev/null
@@ -1,34 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Homeservers; -using LibMatrix.Responses; -using LibMatrix.Services; -using Microsoft.AspNetCore.Mvc; - -namespace LibMatrix.HomeserverEmulator.Controllers; - -[ApiController] -[Route("/_matrix/")] -public class DirectoryController(ILogger<DirectoryController> logger, RoomStore roomStore) : ControllerBase { - [HttpGet("client/v3/directory/room/{alias}")] - public async Task<AliasResult> GetRoomAliasV3(string alias) { - var match = roomStore._rooms.FirstOrDefault(x => - x.State.Any(y => y.Type == RoomCanonicalAliasEventContent.EventId && y.StateKey == "" && y.RawContent?["alias"]?.ToString() == alias)); - - if (match == null) - throw new MatrixException() { - ErrorCode = "M_NOT_FOUND", - Error = "Room not found" - }; - - var servers = match.State.Where(x => x.Type == RoomMemberEventContent.EventId && x.RawContent?["membership"]?.ToString() == "join") - .Select(x => x.StateKey!.Split(':', 2)[1]).ToList(); - - return new() { - RoomId = match.RoomId, - Servers = servers - }; - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj deleted file mode 100644
index a6b6214..0000000 --- a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj +++ /dev/null
@@ -1,23 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> - - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <Nullable>enable</Nullable> - <ImplicitUsings>enable</ImplicitUsings> - <InvariantGlobalization>true</InvariantGlobalization> - <LangVersion>preview</LangVersion> - <GenerateDocumentationFile>true</GenerateDocumentationFile> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="EasyCompressor.LZMA" Version="2.0.2" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj"/> -<!-- <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj"/>--> - </ItemGroup> - -</Project> diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
index 13b5c1b..6878b44 100644 --- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
@@ -2,72 +2,81 @@ using ArcaneLibs.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.Services; -using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; namespace LibMatrix.Tests.Abstractions; -public static class HomeserverAbstraction { - private static HomeserverResolverService _hsResolver = new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance); - private static HomeserverProviderService _hsProvider = new HomeserverProviderService(NullLogger<HomeserverProviderService>.Instance, _hsResolver); +public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config _config, ILogger<HomeserverAbstraction> _logger) { + // private static readonly HomeserverResolverService _hsResolver = new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance); + // private static readonly HomeserverProviderService _hsProvider = new HomeserverProviderService(NullLogger<HomeserverProviderService>.Instance, _hsResolver); + + private static AuthenticatedHomeserverGeneric? ConfiguredHomeserver { get; set; } + private static readonly SemaphoreSlim _lock = new(1, 1); + + public async Task<AuthenticatedHomeserverGeneric> GetConfiguredHomeserver(ITestOutputHelper? testOutputHelper = null) { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername)); + Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword)); + + _logger.LogDebug("Using homeserver '{0}' with login '{1}' '{2}", _config.TestHomeserver, _config.TestUsername, _config.TestPassword); + testOutputHelper?.WriteLine($"Using homeserver '{_config.TestHomeserver}' with login '{_config.TestUsername}' '{_config.TestPassword}'"); - public static async Task<AuthenticatedHomeserverGeneric> GetHomeserver() { - var rhs = await _hsProvider.GetRemoteHomeserver("matrixunittests.rory.gay"); - var username = "@f1a2d2d6-1924-421b-91d0-893b347b2a49:matrixunittests.rory.gay"; - var password = "d6d782d6-8bc9-4fac-9cd8-78e101b4298b"; + await _lock.WaitAsync(); + if (ConfiguredHomeserver is not null) { + _lock.Release(); + return ConfiguredHomeserver; + } + + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + LoginResponse reg; try { - reg = await rhs.LoginAsync(username, password); + reg = await rhs.LoginAsync(_config.TestUsername, _config.TestPassword); } catch (MatrixException e) { if (e.ErrorCode == "M_FORBIDDEN") { await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); - reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + reg = await rhs.RegisterAsync(_config.TestUsername, _config.TestPassword, "Unit tests!"); } else throw new Exception("Failed to register", e); } var hs = await _hsProvider.GetAuthenticatedWithToken(reg.Homeserver, reg.AccessToken); - //var rooms = await hs.GetJoinedRooms(); - - // var disbandRoomTasks = rooms.Select(async room => { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // return room; - // }).ToList(); - // await Task.WhenAll(disbandRoomTasks); - - // foreach (var room in rooms) { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // } + ConfiguredHomeserver = hs; + _lock.Release(); return hs; } - public static async Task<AuthenticatedHomeserverGeneric> GetRandomHomeserver() { - var rhs = await _hsProvider.GetRemoteHomeserver("matrixunittests.rory.gay"); - var reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); + public async Task<AuthenticatedHomeserverGeneric> GetNewHomeserver() { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + var username = Guid.NewGuid().ToString(); + var password = Guid.NewGuid().ToString(); + + _logger.LogDebug("Creating new homeserver '{0}' with login '{1}' '{2}'", _config.TestHomeserver, username, password); + + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); var hs = await _hsProvider.GetAuthenticatedWithToken(reg.Homeserver, reg.AccessToken); - - // var rooms = await hs.GetJoinedRooms(); - // - // var disbandRoomTasks = rooms.Select(async room => { - // // await room.DisbandRoomAsync(); - // await room.LeaveAsync(); - // await room.ForgetAsync(); - // return room; - // }).ToList(); - // await Task.WhenAll(disbandRoomTasks); - + return hs; } - public static async IAsyncEnumerable<AuthenticatedHomeserverGeneric> GetRandomHomeservers(int count = 1) { + public async IAsyncEnumerable<AuthenticatedHomeserverGeneric> GetNewHomeservers(int count = 1) { var createRandomUserTasks = Enumerable .Range(0, count) - .Select(_ => GetRandomHomeserver()).ToAsyncEnumerable(); + .Select(_ => GetNewHomeserver()).ToAsyncEnumerable(); await foreach (var hs in createRandomUserTasks) yield return hs; } + + public async Task<(string username, string password, string token)> GetKnownCredentials() { + Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver)); + var rhs = await _hsProvider.GetRemoteHomeserver(_config.TestHomeserver); + + var username = _config.TestUsername; + var password = _config.TestPassword; + var reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + return ("", "", ""); + } } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs
index 2a380fc..b1176ca 100644 --- a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs
@@ -1,6 +1,6 @@ using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.EventTypes.Spec.State.Space; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.RoomTypes; @@ -16,28 +16,28 @@ public static class RoomAbstraction { }; crq.InitialState ??= new List<StateEvent>(); crq.InitialState.Add(new StateEvent() { - Type = "m.room.topic", + Type = RoomTopicEventContent.EventId, StateKey = "", TypedContent = new RoomTopicEventContent() { Topic = "LibMatrix Test Room " + DateTime.Now.ToString("O") } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.name", + Type = RoomNameEventContent.EventId, StateKey = "", TypedContent = new RoomNameEventContent() { Name = "LibMatrix Test Room " + DateTime.Now.ToString("O") } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.avatar", + Type = RoomAvatarEventContent.EventId, StateKey = "", TypedContent = new RoomAvatarEventContent() { Url = "mxc://conduit.rory.gay/r9KiT0f9eQbv8pv4RxwBZFuzhfKjGWHx" } }); crq.InitialState.Add(new StateEvent() { - Type = "m.room.aliases", + Type = RoomAliasEventContent.EventId, StateKey = "", TypedContent = new RoomAliasEventContent() { Aliases = Enumerable @@ -52,7 +52,7 @@ public static class RoomAbstraction { return testRoom; } - private static SemaphoreSlim _spaceSemaphore = null!; + private static SemaphoreSlim _spaceSemaphore = new(1, 1); public static async Task<SpaceRoom> GetTestSpace(AuthenticatedHomeserverGeneric hs, int roomCount = 100, bool addSpaces = false, int spaceSizeReduction = 10) { _spaceSemaphore ??= new SemaphoreSlim(roomCount / spaceSizeReduction, roomCount / spaceSizeReduction); @@ -96,7 +96,7 @@ public static class RoomAbstraction { }); } - var testSpace = (await hs.CreateRoom(crq)).AsSpace; + var testSpace = (await hs.CreateRoom(crq)).AsSpace(); await testSpace.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new object()); diff --git a/Tests/LibMatrix.Tests/Config.cs b/Tests/LibMatrix.Tests/Config.cs
index ddbf705..b59b238 100644 --- a/Tests/LibMatrix.Tests/Config.cs +++ b/Tests/LibMatrix.Tests/Config.cs
@@ -1,18 +1,41 @@ +using Microsoft.Extensions.Configuration; + namespace LibMatrix.Tests; public class Config { + public Config(IConfiguration? config) { + config.GetSection("Configuration").Bind(this); + } + public string? TestHomeserver { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_HOMESERVER") ?? null; - public string? TestUsername { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_USERNAME") ?? null; - public string? TestPassword { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_PASSWORD") ?? null; - public string? TestRoomId { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ID") ?? null; - public string? TestRoomAlias { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ALIAS") ?? null; + public string? TestUsername { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_USERNAME") ?? Guid.NewGuid().ToString(); + + public string? TestPassword { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_PASSWORD") ?? Guid.NewGuid().ToString(); + // public string? TestRoomId { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ID") ?? null; + // public string? TestRoomAlias { get; set; } = Environment.GetEnvironmentVariable("LIBMATRIX_TEST_ROOM_ALIAS") ?? null; - public Dictionary<string, string> ExpectedHomeserverMappings { get; set; } = new() { + public Dictionary<string, string> ExpectedHomeserverClientMappings { get; set; } = new() { { "matrix.org", "https://matrix-client.matrix.org" }, - { "rory.gay", "https://matrix.rory.gay" } + { "rory.gay", "https://matrix.rory.gay" }, + { "feline.support", "https://matrix.feline.support" }, + { "transfem.dev", "https://matrix.transfem.dev/" }, + { "the-apothecary.club", "https://the-apothecary.club" }, + { "nixos.org", "https://matrix.nixos.org" }, + { "fedora.im", "https://fedora.ems.host" } + }; + + public Dictionary<string, string> ExpectedHomeserverFederationMappings { get; set; } = new() { + { "rory.gay", "https://matrix.rory.gay:443" }, + { "matrix.org", "https://matrix-federation.matrix.org:443" }, + { "feline.support", "https://matrix.feline.support:8448" }, + { "transfem.dev", "https://matrix.transfem.dev:443" }, + { "the-apothecary.club", "https://the-apothecary.club:443" }, + { "nixos.org", "https://matrix.nixos.org:443" }, + { "fedora.im", "https://fedora.ems.host:443" } }; public Dictionary<string, string> ExpectedAliasMappings { get; set; } = new() { - { "#libmatrix:rory.gay", "!tuiLEoMqNOQezxILzt:rory.gay" } + { "#libmatrix:rory.gay", "!tuiLEoMqNOQezxILzt:rory.gay" }, + { "#matrix:matrix.org", "!OGEhHVWSdvArJzumhm:matrix.org" } }; } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
index e1da3d5..2a25056 100644 --- a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs +++ b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
@@ -1,3 +1,5 @@ +using LibMatrix.Responses; + namespace LibMatrix.Tests.DataTests; public static class WhoAmITests { diff --git a/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs b/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs
index 35c8704..e3c4388 100644 --- a/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs +++ b/Tests/LibMatrix.Tests/Fixtures/TestFixture.cs
@@ -1,5 +1,5 @@ -using ArcaneLibs.Extensions; using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit.Microsoft.DependencyInjection; @@ -8,24 +8,19 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Fixtures; public class TestFixture : TestBedFixture { - protected override void AddServices(IServiceCollection services, IConfiguration? configuration) { - services.AddSingleton<TieredStorageService>(x => - new TieredStorageService( - null, - null - ) - ); + protected override void AddServices(IServiceCollection services, IConfiguration configuration) { + // services.AddSingleton<TieredStorageService>(x => + // new TieredStorageService( + // null, + // null + // ) + // ); + services.AddSingleton(configuration); services.AddRoryLibMatrixServices(); - - services.AddSingleton<Config>(config => { - var conf = new Config(); - configuration?.GetSection("Configuration").Bind(conf); - - File.WriteAllText("configuration.json", conf.ToJson()); - - return conf; - }); + services.AddLogging(); + services.AddSingleton<HomeserverAbstraction>(); + services.AddSingleton<Config>(); } protected override ValueTask DisposeAsyncCore() diff --git a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj
index 095985a..98c8101 100644 --- a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj +++ b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> @@ -10,20 +10,20 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" /> - <PackageReference Include="xunit" Version="2.8.1" /> - <PackageReference Include="Xunit.Microsoft.DependencyInjection" Version="8.1.0" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.8.1"> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> + <PackageReference Include="xunit" Version="2.9.3" /> + <PackageReference Include="Xunit.Microsoft.DependencyInjection" Version="9.0.0" /> + <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="coverlet.collector" Version="6.0.2"> + <PackageReference Include="coverlet.collector" Version="6.0.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="Xunit.SkippableFact" Version="1.4.13"/> + <PackageReference Include="Xunit.SkippableFact" Version="1.5.23" /> </ItemGroup> <ItemGroup> diff --git a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
index b2f5627..29d37e0 100644 --- a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs +++ b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
@@ -1,6 +1,5 @@ using ArcaneLibs.Extensions; using ArcaneLibs.Extensions.Streams; -using LibMatrix.Homeservers; using LibMatrix.Services; using LibMatrix.Tests.Abstractions; using LibMatrix.Tests.Fixtures; @@ -14,19 +13,19 @@ public class AuthMediaTests : TestBed<TestFixture> { private readonly HomeserverResolverService _resolver; private readonly Config _config; private readonly HomeserverProviderService _provider; + private readonly HomeserverAbstraction _hsAbstraction; public AuthMediaTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { _fixture = fixture; _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); } - private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() => await HomeserverAbstraction.GetHomeserver(); - [Fact] public async Task UploadFileAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes()); Assert.NotNull(mxcUri); @@ -34,7 +33,7 @@ public class AuthMediaTests : TestBed<TestFixture> { [Fact] public async Task DownloadFileAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes()); Assert.NotNull(mxcUri); @@ -48,7 +47,7 @@ public class AuthMediaTests : TestBed<TestFixture> { [SkippableFact(typeof(LibMatrixException))] // This test will fail if the homeserver does not support URL previews public async Task GetUrlPreviewAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var preview = await hs.GetUrlPreviewAsync("https://matrix.org"); Assert.NotNull(preview); diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
index 3ffadf0..69e6231 100644 --- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs +++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
@@ -1,4 +1,5 @@ using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; using LibMatrix.Tests.DataTests; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; @@ -7,18 +8,16 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Tests; public class AuthTests : TestBed<TestFixture> { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; private readonly Config _config; private readonly HomeserverProviderService _provider; + private readonly HomeserverAbstraction _hsAbstraction; public AuthTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); } - + [Fact] public async Task LoginWithPassword() { Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); @@ -34,9 +33,7 @@ public class AuthTests : TestBed<TestFixture> { var login = await _provider.Login(_config.TestHomeserver!, username, password); Assert.NotNull(login); - var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken); - Assert.NotNull(hs); - await hs.Logout(); + Assert.NotNull(login.AccessToken); } [Fact] @@ -63,7 +60,7 @@ public class AuthTests : TestBed<TestFixture> { [Fact] public async Task RegisterAsync() { - var rhs = await _provider.GetRemoteHomeserver("matrixunittests.rory.gay"); + var rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver); var reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); Assert.NotNull(reg); Assert.NotNull(reg.AccessToken); diff --git a/Tests/LibMatrix.Tests/Tests/CanonicalJsonTests.cs b/Tests/LibMatrix.Tests/Tests/CanonicalJsonTests.cs new file mode 100644
index 0000000..b6bde71 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/CanonicalJsonTests.cs
@@ -0,0 +1,49 @@ +using System.Collections.Frozen; +using System.Diagnostics; +using System.Text.Json; +using LibMatrix.Extensions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class CanonicalJsonTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed<TestFixture>(testOutputHelper, fixture) { + // Test cases from https://spec.matrix.org/v1.11/appendices/#examples + private static readonly FrozenDictionary<string, string> testCases = new Dictionary<string, string>() { + ["{}"] = "{}", + ["{\n \"one\": 1,\n \"two\": \"Two\"\n}\n"] = "{\"one\":1,\"two\":\"Two\"}", + ["{\n \"b\": \"2\",\n \"a\": \"1\"\n}\n"] = "{\"a\":\"1\",\"b\":\"2\"}", + ["{\"b\":\"2\",\"a\":\"1\"}"] = "{\"a\":\"1\",\"b\":\"2\"}", + ["{\n \"auth\": {\n \"success\": true,\n \"mxid\": \"@john.doe:example.com\",\n \"profile\": {\n \"display_name\": \"John Doe\",\n \"three_pids\": [\n {\n \"medium\": \"email\",\n \"address\": \"[email protected]\"\n },\n {\n \"medium\": \"msisdn\",\n \"address\": \"123456789\"\n }\n ]\n }\n }\n}\n"] = + "{\"auth\":{\"mxid\":\"@john.doe:example.com\",\"profile\":{\"display_name\":\"John Doe\",\"three_pids\":[{\"address\":\"[email protected]\",\"medium\":\"email\"},{\"address\":\"123456789\",\"medium\":\"msisdn\"}]},\"success\":true}}", + ["{\n \"a\": \"日本語\"\n}\n"] = "{\"a\":\"日本語\"}", + ["{\n \"本\": 2,\n \"日\": 1\n}\n"] = "{\"日\":1,\"本\":2}", + ["{\n \"a\": \"\\u65E5\"\n}\n"] = "{\"a\":\"日\"}", + ["{\n \"a\": null\n}\n"] = "{\"a\":null}", + ["{\n \"a\": -0,\n \"b\": 1e10\n}\n"] = "{\"a\":0,\"b\":10000000000}" + }.ToFrozenDictionary(); + + [Fact] + public void SpecTests() { + var i = 0; + foreach (var (input, expected) in testCases) { + var deserialised = JsonSerializer.Deserialize<Dictionary<string, object>>(input); + var actual = CanonicalJsonSerializer.Serialize(deserialised); + Assert.Equal(expected, actual); + // testOutputHelper.WriteLine($"Test case {i++} successful!"); + } + } + + [Fact] + public void RepeatTests() { + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 1_000_000; i++) { + SpecTests(); + if (i % 10000 == 0) { + testOutputHelper.WriteLine($"{i} loops successful! Delta: {sw.Elapsed}"); + sw.Restart(); + } + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs new file mode 100644
index 0000000..ea494fa --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs
@@ -0,0 +1,41 @@ +using LibMatrix.Services; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.HomeserverResolverTests; + +public class ClientWellKnownResolverTests : TestBed<TestFixture> { + private readonly Config _config; + private readonly ClientWellKnownResolver _resolver; + + public ClientWellKnownResolverTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _resolver = _fixture.GetService<ClientWellKnownResolver>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + } + + [Fact] + public async Task ResolveServerClient() { + var tasks = _config.ExpectedHomeserverClientMappings.Select(async mapping => { + var server = await _resolver.TryResolveWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Content.Homeserver.BaseUrl); + return server; + }).ToList(); + await Task.WhenAll(tasks); + } + + private async Task AssertClientWellKnown(string homeserver, string expected) { + var server = await _resolver.TryResolveWellKnown(homeserver); + Assert.Equal(expected, server.Content.Homeserver.BaseUrl); + } + + [Fact] + public Task ResolveMatrixOrg() => AssertClientWellKnown("matrix.org", "https://matrix-client.matrix.org"); + + [Fact] + public Task ResolveRoryGay() => AssertClientWellKnown("rory.gay", "https://matrix.rory.gay"); + + [Fact] + public Task ResolveTransfemDev() => AssertClientWellKnown("transfem.dev", "https://matrix.transfem.dev/"); +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/LegacyHomeserverResolverTests.cs b/Tests/LibMatrix.Tests/Tests/LegacyHomeserverResolverTests.cs new file mode 100644
index 0000000..20dc4fb --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/LegacyHomeserverResolverTests.cs
@@ -0,0 +1,36 @@ +using LibMatrix.Services; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class LegacyHomeserverResolverTests : TestBed<TestFixture> { + private readonly Config _config; + private readonly HomeserverResolverService _resolver; + + public LegacyHomeserverResolverTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + } + + [Fact] + public async Task ResolveServerClient() { + var tasks = _config.ExpectedHomeserverClientMappings.Select(async mapping => { + var server = await _resolver.ResolveHomeserverFromWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Client); + return server; + }).ToList(); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task ResolveServerServer() { + var tasks = _config.ExpectedHomeserverFederationMappings.Select(async mapping => { + var server = await _resolver.ResolveHomeserverFromWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Server); + return server; + }).ToList(); + await Task.WhenAll(tasks); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
index 700aa96..20f975e 100644 --- a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs +++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
@@ -5,43 +5,44 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Tests; -public class ResolverTest : TestBed<TestFixture> { +public class RemoteHomeserverTests : TestBed<TestFixture> { private readonly TestFixture _fixture; private readonly HomeserverResolverService _resolver; private readonly Config _config; private readonly HomeserverProviderService _provider; - public ResolverTest(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + public RemoteHomeserverTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { _fixture = fixture; _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); } - [Fact] - public async Task ResolveServer() { - foreach (var (domain, expected) in _config.ExpectedHomeserverMappings) { - var server = await _resolver.ResolveHomeserverFromWellKnown(domain); - Assert.Equal(expected, server.Client); - } - } - - [Fact] - public async Task ResolveMedia() { - var media = await _resolver.ResolveMediaUri("matrix.org", "mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); - Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); - } + // [Fact] + // public async Task ResolveMedia() { + // var hs = await _provider.GetRemoteHomeserver("matrix.org"); + // var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo"); + // + // Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media); + // } [Fact] public async Task ResolveRoomAliasAsync() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); - var alias = await hs.ResolveRoomAliasAsync("#matrix:matrix.org"); - Assert.Equal("!OGEhHVWSdvArJzumhm:matrix.org", alias.RoomId); + // var hs = await _provider.GetRemoteHomeserver("matrix.org"); + // var alias = await hs.ResolveRoomAliasAsync("#matrix:matrix.org"); + // Assert.Equal("!OGEhHVWSdvArJzumhm:matrix.org", alias.RoomId); + var tasks = _config.ExpectedAliasMappings.Select(async mapping => { + var hs = await _provider.GetRemoteHomeserver("matrix.org"); + var alias = await hs.ResolveRoomAliasAsync(mapping.Key); + Assert.Equal(mapping.Value, alias.RoomId); + return alias; + }).ToList(); + await Task.WhenAll(tasks); } [Fact] public async Task GetClientVersionsAsync() { - var hs = await _provider.GetRemoteHomeserver("matrix.org"); + var hs = await _provider.GetRemoteHomeserver(_config.TestHomeserver); var versions = await hs.GetClientVersionsAsync(); Assert.NotNull(versions); } diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs deleted file mode 100644
index 4c8dcb4..0000000 --- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs +++ /dev/null
@@ -1,248 +0,0 @@ -using System.Text; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Homeservers; -using LibMatrix.Responses; -using LibMatrix.Services; -using LibMatrix.Tests.Abstractions; -using LibMatrix.Tests.Fixtures; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; - -namespace LibMatrix.Tests.Tests; - -public class RoomTests : TestBed<TestFixture> { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; - - public RoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); - } - - private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() => await HomeserverAbstraction.GetHomeserver(); - - [Fact] - public async Task GetJoinedRoomsAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - //make 100 rooms - var createRoomTasks = Enumerable.Range(0, 10).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); - await Task.WhenAll(createRoomTasks); - - var rooms = await hs.GetJoinedRooms(); - Assert.NotNull(rooms); - Assert.NotEmpty(rooms); - Assert.All(rooms, Assert.NotNull); - Assert.True(rooms.Count >= 10, "Not enough rooms were found"); - - await hs.Logout(); - } - - [Fact] - public async Task GetMembersAsync() { - Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); - Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); - - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var members = room.GetMembersEnumerableAsync(); - Assert.NotNull(members); - var hitMembers = false; - await foreach (var member in members) { - Assert.NotNull(member); - Assert.NotNull(member.StateKey); - Assert.NotEmpty(member.StateKey); - Assert.NotNull(member.Sender); - Assert.NotEmpty(member.Sender); - Assert.NotNull(member.RawContent); - Assert.NotEmpty(member.RawContent); - Assert.NotNull(member.TypedContent); - Assert.IsType<RoomMemberEventContent>(member.TypedContent); - var content = (RoomMemberEventContent)member.TypedContent; - Assert.NotNull(content); - Assert.NotNull(content.Membership); - Assert.NotEmpty(content.Membership); - hitMembers = true; - } - - Assert.True(hitMembers, "No members were found in the room"); - } - - [Fact] - public async Task JoinAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var id = await room.JoinAsync(); - Assert.NotNull(id); - Assert.NotNull(id.RoomId); - Assert.NotEmpty(id.RoomId); - } - - [Fact] - public async Task ForgetAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.ForgetAsync(); - } - - [Fact] - public async Task LeaveAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.LeaveAsync(); - } - - [Fact] - public async Task KickAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.InviteUserAsync(hs2.UserId, "Unit test!"); - await hs2.GetRoom(room.RoomId).JoinAsync(); - await room.KickAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("leave", banState.Membership); - } - - [Fact] - public async Task BanAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.BanAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("ban", banState.Membership); - } - - [Fact] - public async Task UnbanAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - await room.BanAsync(hs2.UserId, "test"); - var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); - Assert.NotNull(banState); - Assert.Equal("ban", banState.Membership); - await room.UnbanAsync(hs2.UserId); - var unbanState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); - Assert.NotNull(unbanState); - Assert.Equal("leave", unbanState.Membership); - } - - [SkippableFact(typeof(MatrixException))] - public async Task SendStateEventAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "no" - }); - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "yes" - }); - } - - [SkippableFact(typeof(MatrixException))] - public async Task SendAndGetStateEventAsync() { - await SendStateEventAsync(); - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "no" - }); - await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { - DisplayName = "wee_woo", - AvatarUrl = "yes" - }); - - var state1 = await room.GetStateAsync<UserProfileResponse>("gay.rory.libmatrix.unit_tests"); - Assert.NotNull(state1); - Assert.NotNull(state1.DisplayName); - Assert.NotEmpty(state1.DisplayName); - Assert.NotNull(state1.AvatarUrl); - Assert.NotEmpty(state1.AvatarUrl); - Assert.Equal("wee_woo", state1.DisplayName); - Assert.Equal("no", state1.AvatarUrl); - - var state2 = await room.GetStateAsync<UserProfileResponse>("gay.rory.libmatrix.unit_tests", "state_key_maybe"); - Assert.NotNull(state2); - Assert.NotNull(state2.DisplayName); - Assert.NotEmpty(state2.DisplayName); - Assert.NotNull(state2.AvatarUrl); - Assert.NotEmpty(state2.AvatarUrl); - Assert.Equal("wee_woo", state2.DisplayName); - Assert.Equal("yes", state2.AvatarUrl); - } - - [Fact] - public async Task DisbandAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - await room.PermanentlyBrickRoomAsync(); - } - - [Fact] - public async Task SendFileAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - - var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." + - "\nIf you are reading this on matrix, it means the unit test for uploading a file works!"))); - Assert.NotNull(res); - Assert.NotNull(res.EventId); - } - - [Fact] - public async Task GetSpaceChildrenAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var space = await RoomAbstraction.GetTestSpace(hs, 2, false, 1); - Assert.NotNull(space); - var children = space.GetChildrenAsync(); - Assert.NotNull(children); - var found = 0; - await foreach (var room in children) found++; - Assert.Equal(2, found); - } - - [Fact] - public async Task InviteAndJoinAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - var otherUsers = HomeserverAbstraction.GetRandomHomeservers(15); - Assert.NotNull(room); - - // var expectedCount = 1; - - var tasks = new List<Task>(); - await foreach (var otherUser in otherUsers) - tasks.Add(Task.Run(async () => { - await room.InviteUserAsync(otherUser.UserId); - await otherUser.GetRoom(room.RoomId).JoinAsync(); - })); - await Task.WhenAll(tasks); - - var states = await room.GetMembersListAsync(false); - Assert.Equal(16, states.Count); - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs new file mode 100644
index 0000000..1ae195d --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/OtherRoomTests.cs
@@ -0,0 +1,101 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class OtherRoomTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public OtherRoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetCanonicalAliasAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var alias = await room.GetCanonicalAliasAsync(); + Assert.NotNull(alias); + Assert.NotNull(alias.Alias); + Assert.NotEmpty(alias.Alias); + } + + [Fact] + public async Task GetJoinRuleAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var rule = await room.GetJoinRuleAsync(); + Assert.NotNull(rule); + Assert.NotNull(rule.JoinRuleValue); + Assert.NotEmpty(rule.JoinRuleValue); + } + + [Fact] + public async Task GetHistoryVisibilityAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var visibility = await room.GetHistoryVisibilityAsync(); + Assert.NotNull(visibility); + Assert.NotNull(visibility.HistoryVisibility); + Assert.NotEmpty(visibility.HistoryVisibility); + } + + [Fact] + public async Task GetGuestAccessAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + try { + var access = await room.GetGuestAccessAsync(); + Assert.NotNull(access); + Assert.NotNull(access.GuestAccess); + Assert.NotEmpty(access.GuestAccess); + } + catch (Exception e) { + if (e is not MatrixException exception) throw; + Assert.Equal("M_NOT_FOUND", exception.ErrorCode); + } + } + + [Fact] + public async Task GetCreateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var create = await room.GetCreateEventAsync(); + Assert.NotNull(create); + Assert.NotNull(create.Creator); + Assert.NotEmpty(create.RoomVersion!); + } + + [Fact] + public async Task GetRoomType() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.GetRoomType(); + } + + [Fact] + public async Task GetPowerLevelsAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var power = await room.GetPowerLevelsAsync(); + Assert.NotNull(power); + Assert.NotNull(power.Ban); + Assert.NotNull(power.Kick); + Assert.NotNull(power.Invite); + Assert.NotNull(power.Redact); + Assert.NotNull(power.StateDefault); + Assert.NotNull(power.EventsDefault); + Assert.NotNull(power.UsersDefault); + Assert.NotNull(power.Users); + // Assert.NotNull(power.Events); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs new file mode 100644
index 0000000..78f007c --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomAvatarTests.cs
@@ -0,0 +1,27 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomAvatarTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomAvatarTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetAvatarUrlAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var url = await room.GetAvatarUrlAsync(); + Assert.NotNull(url); + Assert.NotNull(url.Url); + Assert.NotEmpty(url.Url); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs new file mode 100644
index 0000000..7f53b08 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomNameTests.cs
@@ -0,0 +1,42 @@ +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomNameTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomNameTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetNameAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = await room.GetNameAsync(); + Assert.NotNull(name); + Assert.NotEmpty(name); + + await room.LeaveAsync(); + } + + [Fact] + public async Task SetNameAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = Guid.NewGuid().ToString(); + await room.SendStateEventAsync(RoomNameEventContent.EventId, new RoomNameEventContent { Name = name }); + var newName = await room.GetNameAsync(); + Assert.Equal(name, newName); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs new file mode 100644
index 0000000..6610035 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/BasicRoomEventTests/RoomTopicTests.cs
@@ -0,0 +1,27 @@ +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests.BasicRoomEventTests; + +public class RoomTopicTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomTopicTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetTopicAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var topic = await room.GetTopicAsync(); + Assert.NotNull(topic); + Assert.NotNull(topic.Topic); + Assert.NotEmpty(topic.Topic); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs
index 932909f..97f4525 100644 --- a/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomEventTests.cs
@@ -1,5 +1,3 @@ -using LibMatrix.Homeservers; -using LibMatrix.Services; using LibMatrix.Tests.Abstractions; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; @@ -8,23 +6,16 @@ using Xunit.Microsoft.DependencyInjection.Abstracts; namespace LibMatrix.Tests.Tests; public class RoomEventTests : TestBed<TestFixture> { - private readonly TestFixture _fixture; - private readonly HomeserverResolverService _resolver; - private readonly Config _config; - private readonly HomeserverProviderService _provider; + private readonly HomeserverAbstraction _hsAbstraction; public RoomEventTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { - _fixture = fixture; - _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); - _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); - _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); } - private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() => await HomeserverAbstraction.GetHomeserver(); - [Fact] public async Task GetNameAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var name = await room.GetNameAsync(); @@ -34,7 +25,7 @@ public class RoomEventTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetTopicAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var topic = await room.GetTopicAsync(); @@ -44,19 +35,8 @@ public class RoomEventTests : TestBed<TestFixture> { } [SkippableFact(typeof(MatrixException))] - public async Task GetAliasesAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var aliases = await room.GetAliasesAsync(); - Assert.NotNull(aliases); - Assert.NotEmpty(aliases); - Assert.All(aliases, Assert.NotNull); - } - - [SkippableFact(typeof(MatrixException))] public async Task GetCanonicalAliasAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var alias = await room.GetCanonicalAliasAsync(); @@ -67,40 +47,46 @@ public class RoomEventTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetAvatarUrlAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var url = await room.GetAvatarUrlAsync(); Assert.NotNull(url); Assert.NotNull(url.Url); Assert.NotEmpty(url.Url); + + await room.LeaveAsync(); } [Fact] public async Task GetJoinRuleAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var rule = await room.GetJoinRuleAsync(); Assert.NotNull(rule); Assert.NotNull(rule.JoinRuleValue); Assert.NotEmpty(rule.JoinRuleValue); + + await room.LeaveAsync(); } [Fact] public async Task GetHistoryVisibilityAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var visibility = await room.GetHistoryVisibilityAsync(); Assert.NotNull(visibility); Assert.NotNull(visibility.HistoryVisibility); Assert.NotEmpty(visibility.HistoryVisibility); + + await room.LeaveAsync(); } [Fact] public async Task GetGuestAccessAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); try { @@ -113,30 +99,36 @@ public class RoomEventTests : TestBed<TestFixture> { if (e is not MatrixException exception) throw; Assert.Equal("M_NOT_FOUND", exception.ErrorCode); } + + await room.LeaveAsync(); } [Fact] public async Task GetCreateEventAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var create = await room.GetCreateEventAsync(); Assert.NotNull(create); Assert.NotNull(create.Creator); Assert.NotEmpty(create.RoomVersion!); + + await room.LeaveAsync(); } [Fact] public async Task GetRoomType() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); await room.GetRoomType(); + + await room.LeaveAsync(); } [Fact] public async Task GetPowerLevelsAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var power = await room.GetPowerLevelsAsync(); @@ -150,5 +142,7 @@ public class RoomEventTests : TestBed<TestFixture> { Assert.NotNull(power.UsersDefault); Assert.NotNull(power.Users); // Assert.NotNull(power.Events); + + await room.LeaveAsync(); } } \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs new file mode 100644
index 0000000..8419518 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomMembershipTests.cs
@@ -0,0 +1,174 @@ +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Homeservers; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class RoomMembershipTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomMembershipTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetMembersAsync() { + Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); + Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var members = room.GetMembersEnumerableAsync(); + Assert.NotNull(members); + var hitMembers = false; + await foreach (var member in members) { + Assert.NotNull(member); + Assert.NotNull(member.StateKey); + Assert.NotEmpty(member.StateKey); + Assert.NotNull(member.Sender); + Assert.NotEmpty(member.Sender); + Assert.NotNull(member.RawContent); + Assert.NotEmpty(member.RawContent); + Assert.NotNull(member.TypedContent); + Assert.IsType<RoomMemberEventContent>(member.TypedContent); + var content = (RoomMemberEventContent)member.TypedContent; + Assert.NotNull(content); + Assert.NotNull(content.Membership); + Assert.NotEmpty(content.Membership); + hitMembers = true; + } + + Assert.True(hitMembers, "No members were found in the room"); + + await room.LeaveAsync(); + } + + [Fact] + public async Task JoinAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(_testOutputHelper); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.SendStateEventAsync(RoomJoinRulesEventContent.EventId, new RoomJoinRulesEventContent() { + JoinRule = RoomJoinRulesEventContent.JoinRules.Public + }); + // var id = await room.JoinAsync(); + var id = await hs2.GetRoom(room.RoomId).JoinAsync(); + Assert.NotNull(id); + Assert.NotNull(id.RoomId); + Assert.NotEmpty(id.RoomId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task ForgetAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.ForgetAsync(); + } + + [Fact] + public async Task LeaveAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.LeaveAsync(); + } + + [Fact] + public async Task KickAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.InviteUserAsync(hs2.UserId, "Unit test!"); + await hs2.GetRoom(room.RoomId).JoinAsync(); + await room.KickAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("leave", banState.Membership); + Assert.Equal("test", banState.Reason); + + await room.LeaveAsync(); + } + + [Fact] + public async Task BanAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); + + await room.LeaveAsync(); + } + + [Fact] + public async Task UnbanAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var hs2 = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); + await room.UnbanAsync(hs2.UserId, "testing"); + + var unbanState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(unbanState); + Assert.Equal("leave", unbanState.Membership); + Assert.Equal("testing", unbanState.Reason); + + await room.LeaveAsync(); + } + + [Fact] + public async Task InviteAndJoinAsync() { + int count = 5; + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + var otherUsers = _hsAbstraction.GetNewHomeservers(count); + Assert.NotNull(room); + + // var expectedCount = 1; + + // var tasks = new List<Task>(); + // await foreach (var otherUser in otherUsers) + // tasks.AddRange([ + // room.InviteUserAsync(otherUser.UserId), + // otherUser.GetRoom(room.RoomId).JoinAsync() + // ]); + + Dictionary<AuthenticatedHomeserverGeneric, Task> tasks = new(); + await foreach (var otherUser in otherUsers) { + _testOutputHelper.WriteLine($"Inviting {otherUser.UserId} to {room.RoomId}"); + tasks.Add(otherUser, room.InviteUserAsync(otherUser.UserId, "Unit test!")); + } + + await foreach (var otherUser in tasks.ToAsyncEnumerable()) { + _testOutputHelper.WriteLine($"Joining {otherUser.UserId} to {room.RoomId}"); + await otherUser.GetRoom(room.RoomId).JoinAsync(reason: "Unit test!"); + } + + var states = await room.GetMembersListAsync(); + Assert.Equal(count + 1, states.Count); + + await room.LeaveAsync(); + await foreach (var authenticatedHomeserverGeneric in otherUsers) + { + await authenticatedHomeserverGeneric.GetRoom(room.RoomId).LeaveAsync(); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs new file mode 100644
index 0000000..7801ed0 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/RoomTests.cs
@@ -0,0 +1,316 @@ +using System.Text; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Responses; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class RoomTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public RoomTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task GetJoinedRoomsAsync() { + var hs = await _hsAbstraction.GetNewHomeserver(); + //make 100 rooms + var createRoomTasks = Enumerable.Range(0, 10).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); + await Task.WhenAll(createRoomTasks); + + var rooms = await hs.GetJoinedRooms(); + Assert.NotNull(rooms); + Assert.NotEmpty(rooms); + Assert.All(rooms, Assert.NotNull); + // Assert.True(rooms.Count >= 10, "Not enough rooms were found"); + Assert.Equal(10, rooms.Count); + await hs.Logout(); + } + + [Fact] + public async Task GetMembersAsync() { + Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); + Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); + + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var members = room.GetMembersEnumerableAsync(); + Assert.NotNull(members); + var hitMembers = false; + await foreach (var member in members) { + Assert.NotNull(member); + Assert.NotNull(member.StateKey); + Assert.NotEmpty(member.StateKey); + Assert.NotNull(member.Sender); + Assert.NotEmpty(member.Sender); + Assert.NotNull(member.RawContent); + Assert.NotEmpty(member.RawContent); + Assert.NotNull(member.TypedContent); + Assert.IsType<RoomMemberEventContent>(member.TypedContent); + var content = (RoomMemberEventContent)member.TypedContent; + Assert.NotNull(content); + Assert.NotNull(content.Membership); + Assert.NotEmpty(content.Membership); + hitMembers = true; + } + + Assert.True(hitMembers, "No members were found in the room"); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(MatrixException))] + public async Task SendStateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "no" + }); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "yes" + }); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(MatrixException))] + public async Task SendAndGetStateEventAsync() { + await SendStateEventAsync(); + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "no" + }); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new UserProfileResponse() { + DisplayName = "wee_woo", + AvatarUrl = "yes" + }); + + var state1 = await room.GetStateAsync<UserProfileResponse>("gay.rory.libmatrix.unit_tests"); + Assert.NotNull(state1); + Assert.NotNull(state1.DisplayName); + Assert.NotEmpty(state1.DisplayName); + Assert.NotNull(state1.AvatarUrl); + Assert.NotEmpty(state1.AvatarUrl); + Assert.Equal("wee_woo", state1.DisplayName); + Assert.Equal("no", state1.AvatarUrl); + + var state2 = await room.GetStateAsync<UserProfileResponse>("gay.rory.libmatrix.unit_tests", "state_key_maybe"); + Assert.NotNull(state2); + Assert.NotNull(state2.DisplayName); + Assert.NotEmpty(state2.DisplayName); + Assert.NotNull(state2.AvatarUrl); + Assert.NotEmpty(state2.AvatarUrl); + Assert.Equal("wee_woo", state2.DisplayName); + Assert.Equal("yes", state2.AvatarUrl); + + await room.LeaveAsync(); + } + + [Fact] + public async Task DisbandAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.PermanentlyBrickRoomAsync(); + + await room.LeaveAsync(); + } + + [Fact] + public async Task SendFileAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." + + "\nIf you are reading this on matrix, it means the unit test for uploading a file works!"))); + Assert.NotNull(res); + Assert.NotNull(res.EventId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetFullStateAsListAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetFullStateAsListAsync(); + Assert.NotNull(state); + Assert.NotEmpty(state); + Assert.All(state, Assert.NotNull); + Assert.All(state, s => { + Assert.NotNull(s.EventId); + Assert.NotEmpty(s.EventId); + Assert.NotNull(s.Sender); + Assert.NotEmpty(s.Sender); + Assert.NotNull(s.RawContent); + Assert.NotNull(s.TypedContent); + }); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(LibMatrixException))] + public async Task GetStateEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotNull(state.EventId); + Assert.NotEmpty(state.EventId); + Assert.NotNull(state.Sender); + Assert.NotEmpty(state.Sender); + Assert.NotNull(state.RawContent); + Assert.NotEmpty(state.RawContent); + Assert.NotNull(state.TypedContent); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetStateEventIdAsync() { + var hs = await _hsAbstraction.GetNewHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventIdAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotEmpty(state); + + await room.LeaveAsync(); + } + + [SkippableFact(typeof(LibMatrixException))] + public async Task GetStateEventOrNullAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var state = await room.GetStateEventOrNullAsync("m.room.name"); + Assert.NotNull(state); + Assert.NotNull(state.EventId); + Assert.NotEmpty(state.EventId); + Assert.NotNull(state.Sender); + Assert.NotEmpty(state.Sender); + Assert.NotNull(state.RawContent); + Assert.NotEmpty(state.RawContent); + Assert.NotNull(state.TypedContent); + + await room.LeaveAsync(); + } + + [Fact] + public async Task GetMessagesAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var messages = await room.GetMessagesAsync(); + Assert.NotNull(messages); + Assert.NotNull(messages.Chunk); + Assert.NotEmpty(messages.Chunk); + Assert.All(messages.Chunk, Assert.NotNull); + Assert.All(messages.Chunk, m => { + Assert.NotNull(m.EventId); + Assert.NotEmpty(m.EventId); + Assert.NotNull(m.Sender); + Assert.NotEmpty(m.Sender); + Assert.NotNull(m.RawContent); + Assert.NotNull(m.TypedContent); + }); + + await room.LeaveAsync(); + + await File.WriteAllTextAsync("test.json", messages.ToJson()); + } + + [Fact] + public async Task GetManyMessagesAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var messages = room.GetManyMessagesAsync(chunkSize: 2); + await foreach (var resp in messages) { + Assert.NotNull(resp); + Assert.NotNull(resp.Chunk); + // Assert.NotEmpty(resp.Chunk); + Assert.All(resp.Chunk, Assert.NotNull); + Assert.All(resp.Chunk, m => { + Assert.NotNull(m.EventId); + Assert.NotEmpty(m.EventId); + Assert.NotNull(m.Sender); + Assert.NotEmpty(m.Sender); + Assert.NotNull(m.RawContent); + Assert.NotNull(m.TypedContent); + }); + } + + await room.LeaveAsync(); + } + + [Fact] + public async Task SendMessageEventAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var res = await room.SendMessageEventAsync(new RoomMessageEventContent(body: "This test was written by Emma [it/its], member of the Rory& system." + + "\nIf you are reading this on matrix, it means the unit test for sending a message works!", messageType: "m.text")); + Assert.NotNull(res); + Assert.NotNull(res.EventId); + + await room.LeaveAsync(); + } + + [Fact] + public async Task InviteUsersAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var users = _hsAbstraction.GetNewHomeservers(32).ToBlockingEnumerable().ToList(); + Assert.NotNull(users); + Assert.NotEmpty(users); + Assert.All(users, Assert.NotNull); + Assert.All(users, u => { + Assert.NotNull(u); + Assert.NotNull(u.UserId); + Assert.NotEmpty(u.UserId); + }); + + await room.InviteUsersAsync(users.Select(u => u.UserId)); + var members = await room.GetMembersListAsync(); + Assert.NotNull(members); + Assert.NotEmpty(members); + Assert.All(members, Assert.NotNull); + Assert.All(members, m => { + Assert.NotNull(m); + Assert.NotNull(m.StateKey); + Assert.NotEmpty(m.StateKey); + }); + Assert.All(users, u => Assert.Contains(u.UserId, members.Select(m => m.StateKey))); + + await room.LeaveAsync(); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs new file mode 100644
index 0000000..be8076a --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomTests/SpaceTests.cs
@@ -0,0 +1,97 @@ +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.Space; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.Fixtures; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class SpaceTests : TestBed<TestFixture> { + private readonly HomeserverAbstraction _hsAbstraction; + + public SpaceTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}"); + } + + [Fact] + public async Task AddChildAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var crq = new CreateRoomRequest() { + Name = "Test space" + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace(); + + var child = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + + await space.AddChildAsync(child); + + //validate children + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Single(children, x => x.RoomId == child.RoomId); + } + + [Fact] + public async Task AddChildByIdAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var crq = new CreateRoomRequest() { + Name = "Test space" + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace(); + + var child = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + + await space.AddChildByIdAsync(child.RoomId); + + //validate children + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Single(children, x => x.RoomId == child.RoomId); + } + + [Fact] + public async Task GetChildrenAsync() { + var hs = await _hsAbstraction.GetConfiguredHomeserver(); + var expectedChildren = Enumerable.Range(0, 10).Select(async _ => { + var room = await hs.CreateRoom(new CreateRoomRequest() { + Name = "Test child" + }); + return room; + }).ToAsyncEnumerable().ToBlockingEnumerable().ToList(); + + var crq = new CreateRoomRequest() { + Name = "Test space", + InitialState = expectedChildren.Select(c => new StateEvent() { + Type = "m.space.child", + StateKey = c.RoomId, + TypedContent = new SpaceChildEventContent() { + Via = new List<string> { + c.RoomId.Split(":")[1] + } + } + }).ToList() + }; + crq.CreationContent["type"] = SpaceRoom.TypeName; + var space = (await hs.CreateRoom(crq)).AsSpace(); + + var children = space.GetChildrenAsync().ToBlockingEnumerable().ToList(); + Assert.NotNull(children); + Assert.NotEmpty(children); + Assert.Equal(expectedChildren.Count, children.Count); + foreach (var expectedChild in expectedChildren) + { + Assert.Single(children, x => x.RoomId == expectedChild.RoomId); + } + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs deleted file mode 100644
index d9bea94..0000000 --- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs +++ /dev/null
@@ -1,74 +0,0 @@ -// using System.Diagnostics; -// using LibMatrix.Helpers; -// using LibMatrix.Services; -// using LibMatrix.Tests.Abstractions; -// using LibMatrix.Tests.Fixtures; -// using Microsoft.Extensions.Logging; -// using Xunit.Abstractions; -// using Xunit.Microsoft.DependencyInjection.Abstracts; -// -// namespace LibMatrix.Tests.Tests; -// -// public class TestCleanup : TestBed<TestFixture> { -// // private readonly TestFixture _fixture; -// private readonly HomeserverResolverService _resolver; -// private readonly Config _config; -// private readonly HomeserverProviderService _provider; -// private readonly ILogger<TestCleanup> _logger; -// -// public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { -// // _fixture = fixture; -// _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); -// _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); -// _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); -// _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}"); -// } -// -// [Fact] -// public async Task Cleanup() { -// Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); -// Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!"); -// Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!"); -// -// var hs = await HomeserverAbstraction.GetHomeserver(); -// Assert.NotNull(hs); -// -// var syncHelper = new SyncHelper(hs, _logger) { -// Timeout = 3000 -// }; -// _testOutputHelper.WriteLine("Starting sync loop"); -// var cancellationTokenSource = new CancellationTokenSource(); -// var sw = Stopwatch.StartNew(); -// syncHelper.SyncReceivedHandlers.Add(async response => { -// if (sw.ElapsedMilliseconds >= 3000) { -// _testOutputHelper.WriteLine("Cancelling sync loop"); -// -// var tasks = (await hs.GetJoinedRooms()).Select(async room => { -// _logger.LogInformation("Leaving room: {}", room.RoomId); -// await room.LeaveAsync(); -// await room.ForgetAsync(); -// return room; -// }).ToList(); -// await Task.WhenAll(tasks); -// -// cancellationTokenSource.Cancel(); -// } -// -// sw.Restart(); -// if (response.Rooms?.Leave is { Count: > 0 }) { -// // foreach (var room in response.Rooms.Leave) { -// // await hs.GetRoom(room.Key).ForgetAsync(); -// // } -// var tasks = response.Rooms.Leave.Select(async room => { -// await hs.GetRoom(room.Key).ForgetAsync(); -// return room; -// }).ToList(); -// await Task.WhenAll(tasks); -// } -// }); -// await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token); -// -// Assert.NotNull(hs); -// await hs.Logout(); -// } -// } \ No newline at end of file diff --git a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj
index 7d97b70..ee4fd58 100644 --- a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj +++ b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <InvariantGlobalization>true</InvariantGlobalization> @@ -9,8 +9,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> </ItemGroup> <ItemGroup> diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Commands/CmdCommand.cs b/Utilities/LibMatrix.DevTestBot/Bot/Commands/CmdCommand.cs
index 89a9033..874d195 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/Commands/CmdCommand.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/Commands/CmdCommand.cs
@@ -1,11 +1,13 @@ using ArcaneLibs.StringNormalisation; using LibMatrix.EventTypes.Spec; -using LibMatrix.ExampleBot.Bot.Interfaces; +using LibMatrix.Utilities.Bot.Interfaces; namespace LibMatrix.ExampleBot.Bot.Commands; public class CmdCommand : ICommand { public string Name => "cmd"; + public string[]? Aliases => []; + public bool Unlisted => false; public string Description => "Runs a command on the host system"; public Task<bool> CanInvoke(CommandContext ctx) => Task.FromResult(ctx.MessageEvent.Sender.EndsWith(":rory.gay") || ctx.MessageEvent.Sender.EndsWith(":conduit.rory.gay")); diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Commands/DbgAniRainbowTest.cs b/Utilities/LibMatrix.DevTestBot/Bot/Commands/DbgAniRainbowTest.cs
index f75c863..0dde297 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/Commands/DbgAniRainbowTest.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/Commands/DbgAniRainbowTest.cs
@@ -1,16 +1,20 @@ using System.Diagnostics; using LibMatrix.EventTypes.Spec; -using LibMatrix.ExampleBot.Bot.Interfaces; using LibMatrix.Helpers; -using LibMatrix.RoomTypes; using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; namespace ModerationBot.Commands; public class DbgAniRainbowTest(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver) : ICommand { public string Name { get; } = "ani-rainbow"; + + public string[]? Aliases => []; + public bool Unlisted => false; + public string Description { get; } = "[Debug] animated rainbow :)"; + public async Task<bool> CanInvoke(CommandContext ctx) => ctx.Room.RoomId == "!hLEefBaYvNfJwcTjmt:rory.gay"; public async Task Invoke(CommandContext ctx) { diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Commands/HelpCommand.cs b/Utilities/LibMatrix.DevTestBot/Bot/Commands/HelpCommand.cs
index 7ecbeb3..33bbc5a 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/Commands/HelpCommand.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/Commands/HelpCommand.cs
@@ -1,12 +1,14 @@ using System.Text; using LibMatrix.EventTypes.Spec; -using LibMatrix.ExampleBot.Bot.Interfaces; +using LibMatrix.Utilities.Bot.Interfaces; using Microsoft.Extensions.DependencyInjection; namespace LibMatrix.ExampleBot.Bot.Commands; public class HelpCommand(IServiceProvider services) : ICommand { public string Name { get; } = "help"; + public string[]? Aliases => []; + public bool Unlisted => false; public string Description { get; } = "Displays this help message"; public async Task Invoke(CommandContext ctx) { diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Commands/PingCommand.cs b/Utilities/LibMatrix.DevTestBot/Bot/Commands/PingCommand.cs
index 85c86a3..350d89e 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/Commands/PingCommand.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/Commands/PingCommand.cs
@@ -1,11 +1,24 @@ using LibMatrix.EventTypes.Spec; -using LibMatrix.ExampleBot.Bot.Interfaces; +using LibMatrix.Utilities.Bot.Interfaces; namespace LibMatrix.ExampleBot.Bot.Commands; public class PingCommand : ICommand { - public string Name { get; } = "ping"; + public string Name { get; } = "do-ping"; + public string[]? Aliases => []; + public bool Unlisted => false; public string Description { get; } = "Pong!"; - public async Task Invoke(CommandContext ctx) => await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: "pong!")); + // public async Task Invoke(CommandContext ctx) => await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: "pong!")); + public async Task Invoke(CommandContext ctx) { + // await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: "pong!")); + var count = ctx.Args.Length > 0 ? int.Parse(ctx.Args[0]) : 1; + var tasks = Enumerable.Range(0, count).Select(async i => { + await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: $"!ping {i}", messageType: "m.text")); + await Task.Delay(1000); + }).ToList(); + await Task.WhenAll(tasks); + + await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: "Pong!")); + } } \ No newline at end of file diff --git a/Utilities/LibMatrix.DevTestBot/Bot/DevTestBot.cs b/Utilities/LibMatrix.DevTestBot/Bot/DevTestBot.cs
index fa80bfd..3bb0c25 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/DevTestBot.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/DevTestBot.cs
@@ -1,8 +1,7 @@ using System.Diagnostics.CodeAnalysis; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.ExampleBot.Bot.Interfaces; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.Services; @@ -16,17 +15,13 @@ public class DevTestBot : IHostedService { private readonly HomeserverProviderService _homeserverProviderService; private readonly ILogger<DevTestBot> _logger; private readonly DevTestBotConfiguration _configuration; - private readonly IEnumerable<ICommand> _commands; public DevTestBot(HomeserverProviderService homeserverProviderService, ILogger<DevTestBot> logger, - DevTestBotConfiguration configuration, IServiceProvider services) { + DevTestBotConfiguration configuration) { logger.LogInformation("{} instantiated!", GetType().Name); _homeserverProviderService = homeserverProviderService; _logger = logger; _configuration = configuration; - _logger.LogInformation("Getting commands..."); - _commands = services.GetServices<ICommand>(); - _logger.LogInformation("Got {} commands!", _commands.Count()); } /// <summary>Triggered when the application host is ready to start the service.</summary> @@ -59,48 +54,6 @@ public class DevTestBot : IHostedService { // _logger.LogInformation($"Got room state for {room.RoomId}!"); // } - syncHelper.InviteReceivedHandlers.Add(async Task (args) => { - var inviteEvent = - args.Value.InviteState.Events.FirstOrDefault(x => - x.Type == "m.room.member" && x.StateKey == hs.UserId); - _logger.LogInformation( - $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}"); - if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender == "@mxidupwitch:the-apothecary.club") - try { - var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender); - await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); - } - catch (Exception e) { - _logger.LogError("{}", e.ToString()); - await hs.GetRoom(args.Key).LeaveAsync("I was unable to join the room: " + e); - } - }); - syncHelper.TimelineEventHandlers.Add(async @event => { - _logger.LogInformation( - "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(false, true)); - - var room = hs.GetRoom(@event.RoomId); - // _logger.LogInformation(eventResponse.ToJson(indent: false)); - if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) - if (message is { MessageType: "m.text" } && message.Body.StartsWith(_configuration.Prefix)) { - var command = _commands.FirstOrDefault(x => x.Name == message.Body.Split(' ')[0][_configuration.Prefix.Length..]); - if (command == null) { - await room.SendMessageEventAsync( - new RoomMessageEventContent("m.text", "Command not found!")); - return; - } - - var ctx = new CommandContext { - Room = room, - MessageEvent = @event - }; - if (await command.CanInvoke(ctx)) - await command.Invoke(ctx); - else - await room.SendMessageEventAsync( - new RoomMessageEventContent("m.text", "You do not have permission to run this command!")); - } - }); await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationToken); } diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/CommandContext.cs b/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/CommandContext.cs deleted file mode 100644
index 90a95e4..0000000 --- a/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/CommandContext.cs +++ /dev/null
@@ -1,11 +0,0 @@ -using LibMatrix.EventTypes.Spec; -using LibMatrix.RoomTypes; - -namespace LibMatrix.ExampleBot.Bot.Interfaces; - -public class CommandContext { - public GenericRoom Room { get; set; } - public StateEventResponse MessageEvent { get; set; } - public string CommandName => (MessageEvent.TypedContent as RoomMessageEventContent).Body.Split(' ')[0][1..]; - public string[] Args => (MessageEvent.TypedContent as RoomMessageEventContent).Body.Split(' ')[1..]; -} \ No newline at end of file diff --git a/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/ICommand.cs b/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/ICommand.cs deleted file mode 100644
index a6dc8da..0000000 --- a/Utilities/LibMatrix.DevTestBot/Bot/Interfaces/ICommand.cs +++ /dev/null
@@ -1,10 +0,0 @@ -namespace LibMatrix.ExampleBot.Bot.Interfaces; - -public interface ICommand { - public string Name { get; } - public string Description { get; } - - public Task<bool> CanInvoke(CommandContext ctx) => Task.FromResult(true); - - public Task Invoke(CommandContext ctx); -} \ No newline at end of file diff --git a/Utilities/LibMatrix.DevTestBot/Bot/PingTestBot.cs b/Utilities/LibMatrix.DevTestBot/Bot/PingTestBot.cs new file mode 100644
index 0000000..e992e3c --- /dev/null +++ b/Utilities/LibMatrix.DevTestBot/Bot/PingTestBot.cs
@@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using System.Windows.Input; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Filters; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.ExampleBot.Bot; + +public class PingTestBot : IHostedService { + private readonly HomeserverProviderService _homeserverProviderService; + private readonly ILogger<DevTestBot> _logger; + private readonly DevTestBotConfiguration _configuration; + private readonly IEnumerable<ICommand> _commands; + + public PingTestBot(HomeserverProviderService homeserverProviderService, ILogger<DevTestBot> logger, + DevTestBotConfiguration configuration, IServiceProvider services) { + logger.LogInformation("{} instantiated!", GetType().Name); + _homeserverProviderService = homeserverProviderService; + _logger = logger; + _configuration = configuration; + _logger.LogInformation("Getting commands..."); + _commands = services.GetServices<ICommand>(); + _logger.LogInformation("Got {} commands!", _commands.Count()); + } + + /// <summary>Triggered when the application host is ready to start the service.</summary> + /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> + [SuppressMessage("ReSharper", "FunctionNeverReturns")] + public async Task StartAsync(CancellationToken cancellationToken) { + // Directory.GetFiles("bot_data/cache").ToList().ForEach(File.Delete); + AuthenticatedHomeserverGeneric hs; + try { + hs = await _homeserverProviderService.GetAuthenticatedWithToken(_configuration.Homeserver, + _configuration.AccessToken); + } + catch (Exception e) { + _logger.LogError("{}", e.Message); + throw; + } + + var msg = new MessageBuilder().WithRainbowString("Meanwhile, I'm sitting here, still struggling with trying to rainbow. ^^'").Build(); + + var syncHelper = new SyncHelper(hs); + syncHelper.Filter = new SyncFilter { + Room = new SyncFilter.RoomFilter { + Timeline = new SyncFilter.RoomFilter.StateFilter() { + Limit = 1, + Senders = ["@me"] + }, + Rooms = ["!ping-v11:maunium.net"], + AccountData = new(types: []), + IncludeLeave = false + } + }; + + // await hs.GetRoom("!VJwxdebqoQlhGSEncc:codestorm.net").JoinAsync(); + + // foreach (var room in await hs.GetJoinedRooms()) { + // if(room.RoomId is "!OGEhHVWSdvArJzumhm:matrix.org") continue; + // foreach (var stateEvent in await room.GetStateAsync<List<StateEvent>>("")) { + // var _ = stateEvent.GetType; + // } + // _logger.LogInformation($"Got room state for {room.RoomId}!"); + // } + + // syncHelper.InviteReceivedHandlers.Add(async Task (args) => { + // var inviteEvent = + // args.Value.InviteState.Events.FirstOrDefault(x => + // x.Type == "m.room.member" && x.StateKey == hs.UserId); + // _logger.LogInformation( + // $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}"); + // if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender == "@mxidupwitch:the-apothecary.club") + // try { + // var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender); + // await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); + // } + // catch (Exception e) { + // _logger.LogError("{}", e.ToString()); + // await hs.GetRoom(args.Key).LeaveAsync("I was unable to join the room: " + e); + // } + // }); + // Deprecated, using Bot Utils instead: + // syncHelper.TimelineEventHandlers.Add(async @event => { + // _logger.LogInformation( + // "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(false, true)); + // + // var room = hs.GetRoom(@event.RoomId); + // // _logger.LogInformation(eventResponse.ToJson(indent: false)); + // if (@event is not { Sender: "@emma:rory.gay" }) return; + // if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) + // if (message is { MessageType: "m.text" } && message.Body.StartsWith(_configuration.Prefix)) { + // var command = _commands.FirstOrDefault(x => x.Name == message.Body.Split(' ')[0][_configuration.Prefix.Length..]); + // if (command == null) { + // await room.SendMessageEventAsync( + // new RoomMessageEventContent("m.text", "Command not found!")); + // return; + // } + // + // var ctx = new CommandContext { + // Room = room, + // MessageEvent = @event + // }; + // if (await command.CanInvoke(ctx)) + // await command.Invoke(ctx); + // else + // await room.SendMessageEventAsync( + // new RoomMessageEventContent("m.text", "You do not have permission to run this command!")); + // } + // }); + await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationToken); + } + + /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> + /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> + public Task StopAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Shutting down bot!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.DevTestBot/Bot/StartupTasks/ServerRoomSizeCalulator.cs b/Utilities/LibMatrix.DevTestBot/Bot/StartupTasks/ServerRoomSizeCalulator.cs
index 253eb37..fdc7717 100644 --- a/Utilities/LibMatrix.DevTestBot/Bot/StartupTasks/ServerRoomSizeCalulator.cs +++ b/Utilities/LibMatrix.DevTestBot/Bot/StartupTasks/ServerRoomSizeCalulator.cs
@@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using LibMatrix.ExampleBot.Bot.Interfaces; using LibMatrix.Homeservers; using LibMatrix.Services; using Microsoft.Extensions.Hosting; @@ -11,10 +10,9 @@ public class ServerRoomSizeCalulator : IHostedService { private readonly HomeserverProviderService _homeserverProviderService; private readonly ILogger<ServerRoomSizeCalulator> _logger; private readonly DevTestBotConfiguration _configuration; - private readonly IEnumerable<ICommand> _commands; public ServerRoomSizeCalulator(HomeserverProviderService homeserverProviderService, ILogger<ServerRoomSizeCalulator> logger, - DevTestBotConfiguration configuration, IServiceProvider services) { + DevTestBotConfiguration configuration) { logger.LogInformation("Server room size calculator hosted service instantiated!"); _homeserverProviderService = homeserverProviderService; _logger = logger; diff --git a/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj b/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj
index 6518c67..acff3e2 100644 --- a/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj +++ b/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj
@@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> @@ -18,13 +18,16 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.0-preview.20250419-174711" Condition="'$(Configuration)' == 'Release'" /> + <ProjectReference Include="..\..\ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj" Condition="'$(Configuration)' == 'Debug'"/> <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj"/> + <ProjectReference Include="..\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" /> </ItemGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.0-preview7205256004.28c0e5a"/> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1"/> </ItemGroup> + <ItemGroup> <Content Include="appsettings*.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> diff --git a/Utilities/LibMatrix.DevTestBot/Program.cs b/Utilities/LibMatrix.DevTestBot/Program.cs
index eb5ad76..7eb74d4 100644 --- a/Utilities/LibMatrix.DevTestBot/Program.cs +++ b/Utilities/LibMatrix.DevTestBot/Program.cs
@@ -1,30 +1,24 @@ // See https://aka.ms/new-console-template for more information -using ArcaneLibs; using LibMatrix.ExampleBot.Bot; -using LibMatrix.ExampleBot.Bot.Interfaces; using LibMatrix.Services; +using LibMatrix.Utilities.Bot; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; Console.WriteLine("Hello, World!"); var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => { - // services.AddScoped<TieredStorageService>(x => - // new TieredStorageService( - // cacheStorageProvider: new FileStorageProvider("bot_data/cache/"), - // dataStorageProvider: new FileStorageProvider("bot_data/data/") - // ) - // ); services.AddScoped<DevTestBotConfiguration>(); services.AddRoryLibMatrixServices(); - foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) { - Console.WriteLine($"Adding command {commandClass.Name}"); - services.AddScoped(typeof(ICommand), commandClass); - } + + services.AddMatrixBot() + .AddCommandHandler() + .DiscoverAllCommands() + .WithInviteHandler(ctx => Task.FromResult(ctx.MemberEvent.Sender!.EndsWith(":rory.gay"))); // services.AddHostedService<ServerRoomSizeCalulator>(); - services.AddHostedService<DevTestBot>(); + services.AddHostedService<PingTestBot>(); }).UseConsoleLifetime().Build(); await host.RunAsync(); \ No newline at end of file diff --git a/Utilities/LibMatrix.DevTestBot/appsettings.json b/Utilities/LibMatrix.DevTestBot/appsettings.json
index db64c22..815d2a5 100644 --- a/Utilities/LibMatrix.DevTestBot/appsettings.json +++ b/Utilities/LibMatrix.DevTestBot/appsettings.json
@@ -8,7 +8,7 @@ }, "Bot": { "Homeserver": "rory.gay", - "AccessToken": "syt_xxxxxxxxxxxxxxxxx", - "Prefix": "!" + "AccessToken": "syt_xxxxxx_xxxxxxxxxxxxxxxxxxxx_xxxxxx", + "Prefix": "$" } } \ No newline at end of file diff --git a/Utilities/LibMatrix.E2eeTestKit/App.razor b/Utilities/LibMatrix.E2eeTestKit/App.razor new file mode 100644
index 0000000..623580d --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/App.razor
@@ -0,0 +1,12 @@ +<Router AppAssembly="@typeof(App).Assembly"> + <Found Context="routeData"> + <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> + <FocusOnNavigate RouteData="@routeData" Selector="h1" /> + </Found> + <NotFound> + <PageTitle>Not found</PageTitle> + <LayoutView Layout="@typeof(MainLayout)"> + <p role="alert">Sorry, there's nothing at this address.</p> + </LayoutView> + </NotFound> +</Router> diff --git a/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor b/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor new file mode 100644
index 0000000..e465845 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor
@@ -0,0 +1,16 @@ +@inherits LayoutComponentBase +<div class="page"> + <div class="sidebar"> + <NavMenu /> + </div> + + <main> + <div class="top-row px-4"> + <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> + </div> + + <article class="content px-4"> + @Body + </article> + </main> +</div> diff --git a/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor.css b/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor.css new file mode 100644
index 0000000..baef3ee --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Layout/MainLayout.razor.css
@@ -0,0 +1,77 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor b/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor new file mode 100644
index 0000000..43bc9cc --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor
@@ -0,0 +1,39 @@ +<div class="top-row ps-3 navbar navbar-dark"> + <div class="container-fluid"> + <a class="navbar-brand" href="">LibMatrix.E2eeTestKit</a> + <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> + <span class="navbar-toggler-icon"></span> + </button> + </div> +</div> + +<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu"> + <nav class="flex-column"> + <div class="nav-item px-3"> + <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> + <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home + </NavLink> + </div> + <div class="nav-item px-3"> + <NavLink class="nav-link" href="CSTJApiComparison"> + <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> CanonicalJSON API + </NavLink> + </div> + <div class="nav-item px-3"> + <NavLink class="nav-link" href="CSTJTest"> + <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> CanonicalJSON Test + </NavLink> + </div> + </nav> +</div> + +@code { + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor.css b/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor.css new file mode 100644
index 0000000..07d4c0f --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Layout/NavMenu.razor.css
@@ -0,0 +1,83 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj b/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj new file mode 100644
index 0000000..78cdb67 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj
@@ -0,0 +1,23 @@ +<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" /> + </ItemGroup> + + <ItemGroup> + <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> + +</Project> diff --git a/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJApiComparison.razor b/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJApiComparison.razor new file mode 100644
index 0000000..3aad27c --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJApiComparison.razor
@@ -0,0 +1,134 @@ +@page "/CSTJApiComparison" +@using System.Reflection +@using System.Text +@using System.Text.Json +@using LibMatrix.Extensions + +<PageTitle>Counter</PageTitle> + +<h3>Compare STJ API</h3> + +// side by side table +<table class="table table-bordered"> + <thead> + <tr> + @foreach (var type in TypesToCompare) { + <th>@type.Name</th> + } + </tr> + </thead> + <tbody> + @* // display all public static methods in matching rows, put non matching at the end, by signature *@ + @* // do not use type.GetMethod as it throws ambiguous match errors *@ + @{ var methods = TypesToCompare.Select(t => (Type: t, Methods: t.GetMethods())); } + @foreach (var method in methods.SelectMany(m => m.Methods).DistinctBy(m => GetMethodSignature(m))) { + <tr> + @foreach (var (type, _) in methods) { + var methodInfo = type.GetMethods().FirstOrDefault(m => GetMethodSignature(m) == GetMethodSignature(method)); + if (methodInfo != null) { + <td>@GetMethodSignature(methodInfo)</td> + } + else { + <td></td> + } + } + </tr> + } + + </tbody> +</table> + +@code { + private static readonly Type[] TypesToCompare = [typeof(JsonSerializer), typeof(CanonicalJsonSerializer)]; + + private string GetMethodSignature(MethodInfo method, bool includeModifiers = true, bool includeReturnType = true, bool includeParameters = true) { + var sb = new StringBuilder(); + + //modifiers + if (includeModifiers) { + if (method.IsPublic) { + sb.Append("public "); + } + if (method.IsStatic) { + sb.Append("static "); + } + } + + //return type + if (includeReturnType) { + if (method.ReturnType.IsGenericType) { + sb.Append(method.ReturnType.Name.Split('`')[0]); + sb.Append("<"); + var genericArguments = method.ReturnType.GetGenericArguments(); + foreach (var genericArgument in genericArguments) { + sb.Append(genericArgument.Name); + if (genericArgument != genericArguments.Last()) + sb.Append(", "); + } + + sb.Append(">"); + } + else { + //lowercase primitives + sb.Append(method.ReturnType.Name); + } + + sb.Append(' '); + } + + sb.Append(method.Name); + if (method.IsGenericMethod) { + sb.Append("<"); + var genericArguments = method.GetGenericArguments(); + foreach (var genericArgument in genericArguments) { + sb.Append(genericArgument.Name); + if (genericArgument != genericArguments.Last()) + sb.Append(", "); + } + + sb.Append(">"); + } + sb.Append("("); + var parameters = method.GetParameters(); + foreach (var parameter in parameters) { + //handle generics + if (parameter.ParameterType.IsGenericType) { + sb.Append(parameter.ParameterType.Name.Split('`')[0]); + sb.Append("<"); + var genericArguments = parameter.ParameterType.GetGenericArguments(); + foreach (var genericArgument in genericArguments) { + sb.Append(genericArgument.Name); + if (genericArgument != genericArguments.Last()) + sb.Append(", "); + } + + sb.Append(">"); + } + else { + sb.Append(parameter.ParameterType.Name); + } + sb.Append(" "); + sb.Append(parameter.Name); + if (parameter.HasDefaultValue) { + sb.Append(" = "); + //handle default value + if (parameter.DefaultValue == null) { + sb.Append("null"); + } + else if (parameter.ParameterType == typeof(string)) { + sb.Append($"\"{parameter.DefaultValue}\""); + } + else { + sb.Append(parameter.DefaultValue); + } + } + + if (parameter != parameters.Last()) + sb.Append(", "); + } + + sb.Append(")"); + return sb.ToString(); + } + +} \ No newline at end of file diff --git a/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJTest.razor b/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJTest.razor new file mode 100644
index 0000000..2b787b3 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Pages/CSTJTest.razor
@@ -0,0 +1,38 @@ +@page "/CSTJTest" +@using System.Text.Json +@using LibMatrix.Extensions + +<PageTitle>Counter</PageTitle> + +<h3>Canonicalise JSON</h3> +<hr/> + +<InputTextArea @bind-Value="@JsonInput" rows="@(JsonInput.Split('\n').Length + 1)"></InputTextArea> +<br/> +<pre>@JsonOutput</pre> + +@code { + private string _jsonInput = ""; + + private string JsonInput { + get => _jsonInput; + set { + _jsonInput = value; + try { + Console.WriteLine("Input updated"); + var obj = JsonSerializer.Deserialize<dynamic>(value); + Console.WriteLine("Deserialised"); + JsonOutput = CanonicalJsonSerializer.Serialize(obj); + Console.WriteLine("Serialised: " + JsonOutput ?? "null"); + } + catch (Exception e) { + JsonOutput = e.ToString(); + } + + StateHasChanged(); + } + } + + private string? JsonOutput { get; set; } + +} \ No newline at end of file diff --git a/Utilities/LibMatrix.E2eeTestKit/Pages/Home.razor b/Utilities/LibMatrix.E2eeTestKit/Pages/Home.razor new file mode 100644
index 0000000..df05023 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Pages/Home.razor
@@ -0,0 +1,7 @@ +@page "/" + +<PageTitle>Home</PageTitle> + +<h1>Hello, world!</h1> + +Welcome to your new app. diff --git a/Utilities/LibMatrix.E2eeTestKit/Program.cs b/Utilities/LibMatrix.E2eeTestKit/Program.cs new file mode 100644
index 0000000..32cb883 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Program.cs
@@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using LibMatrix.E2eeTestKit; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add<App>("#app"); +builder.RootComponents.Add<HeadOutlet>("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +await builder.Build().RunAsync(); diff --git a/Utilities/LibMatrix.E2eeTestKit/Properties/launchSettings.json b/Utilities/LibMatrix.E2eeTestKit/Properties/launchSettings.json new file mode 100644
index 0000000..ed96f3c --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/Properties/launchSettings.json
@@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14072", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5297", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Utilities/LibMatrix.E2eeTestKit/_Imports.razor b/Utilities/LibMatrix.E2eeTestKit/_Imports.razor new file mode 100644
index 0000000..6adfbaa --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/_Imports.razor
@@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using LibMatrix.E2eeTestKit +@using LibMatrix.E2eeTestKit.Layout diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/app.css b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/app.css new file mode 100644
index 0000000..0a4519e --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/app.css
@@ -0,0 +1,103 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css new file mode 100644
index 0000000..02ae65b --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css
@@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css.map b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css.map new file mode 100644
index 0000000..afcd9e3 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/css/bootstrap/bootstrap.min.css.map
@@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `<p>`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `<td>` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`<button>` buttons\n//\n// Details at https://github.com/twbs/bootstrap/pull/30562\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n // Remove the inheritance of word-wrap in Safari.\n // See https://github.com/twbs/bootstrap/issues/24990\n word-wrap: normal;\n\n // Undo the opacity change from Chrome\n &:disabled {\n opacity: 1;\n }\n}\n\n// Remove the dropdown arrow in Chrome from inputs built with datalists.\n// See https://stackoverflow.com/a/54997118\n\n[list]::-webkit-calendar-picker-indicator {\n display: none;\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\n// 3. Opinionated: add \"hand\" cursor to non-disabled button elements.\n\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n\n @if $enable-button-pointers {\n &:not(:disabled) {\n cursor: pointer; // 3\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\n// 1. Textareas should really only resize vertically so they don't break their (horizontal) containers.\n\ntextarea {\n resize: vertical; // 1\n}\n\n// 1. Browsers set a default `min-width: min-content;` on fieldsets,\n// unlike e.g. `<div>`s, which have `min-width: 0;` by default.\n// So we reset that to ensure fieldsets behave more like a standard block element.\n// See https://github.com/twbs/bootstrap/issues/12359\n// and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n// 2. Reset the default outline behavior of fieldsets so they don't affect page layout.\n\nfieldset {\n min-width: 0; // 1\n padding: 0; // 2\n margin: 0; // 2\n border: 0; // 2\n}\n\n// 1. By using `float: left`, the legend will behave like a block element.\n// This way the border of a fieldset wraps around the legend if present.\n// 2. Fix wrapping bug.\n// See https://github.com/twbs/bootstrap/issues/29712\n\nlegend {\n float: left; // 1\n width: 100%;\n padding: 0;\n margin-bottom: $legend-margin-bottom;\n @include font-size($legend-font-size);\n font-weight: $legend-font-weight;\n line-height: inherit;\n\n + * {\n clear: left; // 2\n }\n}\n\n// Fix height of inputs with a type of datetime-local, date, month, week, or time\n// See https://github.com/twbs/bootstrap/issues/18842\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n// 1. Correct the outline style in Safari.\n// 2. This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\n[type=\"search\"] {\n outline-offset: -2px; // 1\n -webkit-appearance: textfield; // 2\n}\n\n// 1. A few input types should stay LTR\n// See https://rtlstyling.com/posts/rtl-styling#form-inputs\n// 2. RTL only output\n// See https://rtlcss.com/learn/usage-guide/control-directives/#raw\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n\n// Remove the inner padding in Chrome and Safari on macOS.\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n// Remove padding around color pickers in webkit browsers\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n\n// Inherit font family and line height for file input buttons\n\n::file-selector-button {\n font: inherit;\n}\n\n// 1. Change font properties to `inherit`\n// 2. Correct the inability to style clickable types in iOS and Safari.\n\n::-webkit-file-upload-button {\n font: inherit; // 1\n -webkit-appearance: button; // 2\n}\n\n// Correct element displays\n\noutput {\n display: inline-block;\n}\n\n// Remove border from iframe\n\niframe {\n border: 0;\n}\n\n// Summary\n//\n// 1. Add the correct display in all browsers\n\nsummary {\n display: list-item; // 1\n cursor: pointer;\n}\n\n\n// Progress\n//\n// Add the correct vertical alignment in Chrome, Firefox, and Opera.\n\nprogress {\n vertical-align: baseline;\n}\n\n\n// Hidden attribute\n//\n// Always hide an element with the `hidden` HTML attribute.\n\n[hidden] {\n display: none !important;\n}\n","@charset \"UTF-8\";\n/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-body-rgb: 33, 37, 41;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Liberation Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-bg: #fff;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n background-color: currentColor;\n border: 0;\n opacity: 0.25;\n}\n\nhr:not([size]) {\n height: 1px;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-bs-original-title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: #0d6efd;\n text-decoration: underline;\n}\na:hover {\n color: #0a58ca;\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n direction: ltr /* rtl:ignore */;\n unicode-bidi: bidi-override;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: #d63384;\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 0.875em;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n font-weight: 700;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: #6c757d;\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]::-webkit-calendar-picker-indicator {\n display: none;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n outline-offset: -2px;\n -webkit-appearance: textfield;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::file-selector-button {\n font: inherit;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: calc(1.625rem + 4.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-size: calc(1.575rem + 3.9vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-size: calc(1.525rem + 3.3vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-size: calc(1.475rem + 2.7vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-size: calc(1.425rem + 2.1vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-size: calc(1.375rem + 1.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--bs-gutter-y) * -1);\n margin-right: calc(var(--bs-gutter-x) * -.5);\n margin-left: calc(var(--bs-gutter-x) * -.5);\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * .5);\n padding-left: calc(var(--bs-gutter-x) * .5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-bg: transparent;\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: #212529;\n --bs-table-striped-bg: rgba(0, 0, 0, 0.05);\n --bs-table-active-color: #212529;\n --bs-table-active-bg: rgba(0, 0, 0, 0.1);\n --bs-table-hover-color: #212529;\n --bs-table-hover-bg: rgba(0, 0, 0, 0.075);\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n vertical-align: top;\n border-color: #dee2e6;\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n background-color: var(--bs-table-bg);\n border-bottom-width: 1px;\n box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n.table > :not(:last-child) > :last-child > * {\n border-bottom-color: currentColor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: 1px 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 1px;\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) {\n --bs-table-accent-bg: var(--bs-table-striped-bg);\n color: var(--bs-table-striped-color);\n}\n\n.table-active {\n --bs-table-accent-bg: var(--bs-table-active-bg);\n color: var(--bs-table-active-color);\n}\n\n.table-hover > tbody > tr:hover {\n --bs-table-accent-bg: var(--bs-table-hover-bg);\n color: var(--bs-table-hover-color);\n}\n\n.table-primary {\n --bs-table-bg: #cfe2ff;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bacbe6;\n}\n\n.table-secondary {\n --bs-table-bg: #e2e3e5;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #cbccce;\n}\n\n.table-success {\n --bs-table-bg: #d1e7dd;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bcd0c7;\n}\n\n.table-info {\n --bs-table-bg: #cff4fc;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #badce3;\n}\n\n.table-warning {\n --bs-table-bg: #fff3cd;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #e6dbb9;\n}\n\n.table-danger {\n --bs-table-bg: #f8d7da;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfc2c4;\n}\n\n.table-light {\n --bs-table-bg: #f8f9fa;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfe0e1;\n}\n\n.table-dark {\n --bs-table-bg: #212529;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: #fff;\n border-color: #373b3e;\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: #212529;\n background-color: #fff;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n height: 1.5em;\n}\n.form-control::-moz-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: #dde0e3;\n}\n.form-control::-webkit-file-upload-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n -webkit-margin-end: 0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::-webkit-file-upload-button {\n -webkit-transition: none;\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: #dde0e3;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n.form-control-sm::-webkit-file-upload-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n -webkit-margin-end: 0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n.form-control-lg::-webkit-file-upload-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n -webkit-margin-end: 1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + 0.75rem + 2px);\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + 0.5rem + 2px);\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + 1rem + 2px);\n}\n\n.form-control-color {\n width: 3rem;\n height: auto;\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n.form-control-color::-webkit-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n\n.form-select {\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n -moz-padding-start: calc(0.75rem - 3px);\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: #e9ecef;\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #212529;\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-input {\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n background-color: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: 1px solid rgba(0, 0, 0, 0.25);\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n -webkit-print-color-adjust: exact;\n color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n width: 2em;\n margin-left: -2.5em;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n -webkit-transition: none;\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -moz-appearance: none;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n -moz-transition: none;\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-select {\n height: calc(3.5rem + 2px);\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n padding: 1rem 0.75rem;\n pointer-events: none;\n border: 1px solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::-moz-placeholder {\n color: transparent;\n}\n.form-floating > .form-control::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-select ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus {\n z-index: 3;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 3;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: -1px;\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #198754;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(25, 135, 84, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #198754;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: #198754;\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: #198754;\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: #198754;\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #198754;\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,\n.was-validated .input-group .form-select:valid,\n.input-group .form-select.is-valid {\n z-index: 1;\n}\n.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus,\n.was-validated .input-group .form-select:valid:focus,\n.input-group .form-select.is-valid:focus {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,\n.was-validated .input-group .form-select:invalid,\n.input-group .form-select.is-invalid {\n z-index: 2;\n}\n.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus,\n.was-validated .input-group .form-select:invalid:focus,\n.input-group .form-select.is-invalid:focus {\n z-index: 3;\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: #212529;\n}\n.btn-check:focus + .btn, .btn:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n pointer-events: none;\n opacity: 0.65;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n}\n.btn-check:focus + .btn-primary, .btn-primary:focus {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0a58ca;\n border-color: #0a53be;\n}\n.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-primary:disabled, .btn-primary.disabled {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-secondary:hover {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n}\n.btn-check:focus + .btn-secondary, .btn-secondary:focus {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #565e64;\n border-color: #51585e;\n}\n.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-secondary:disabled, .btn-secondary.disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-success {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n}\n.btn-check:focus + .btn-success, .btn-success:focus {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #146c43;\n border-color: #13653f;\n}\n.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-success:disabled, .btn-success.disabled {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n\n.btn-info {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-info:hover {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n}\n.btn-check:focus + .btn-info, .btn-info:focus {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle {\n color: #000;\n background-color: #3dd5f3;\n border-color: #25cff2;\n}\n.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-info:disabled, .btn-info.disabled {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n\n.btn-warning {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-warning:hover {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n}\n.btn-check:focus + .btn-warning, .btn-warning:focus {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle {\n color: #000;\n background-color: #ffcd39;\n border-color: #ffc720;\n}\n.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-warning:disabled, .btn-warning.disabled {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n}\n.btn-check:focus + .btn-danger, .btn-danger:focus {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #b02a37;\n border-color: #a52834;\n}\n.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-danger:disabled, .btn-danger.disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-light {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-light:hover {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:focus + .btn-light, .btn-light:focus {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-light:disabled, .btn-light.disabled {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-dark {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-dark:hover {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n}\n.btn-check:focus + .btn-dark, .btn-dark:focus {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1a1e21;\n border-color: #191c1f;\n}\n.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-dark:disabled, .btn-dark.disabled {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n\n.btn-outline-primary {\n color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-outline-primary:disabled, .btn-outline-primary.disabled {\n color: #0d6efd;\n background-color: transparent;\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-outline-secondary:disabled, .btn-outline-secondary.disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-success {\n color: #198754;\n border-color: #198754;\n}\n.btn-outline-success:hover {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:focus + .btn-outline-success, .btn-outline-success:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-outline-success:disabled, .btn-outline-success.disabled {\n color: #198754;\n background-color: transparent;\n}\n\n.btn-outline-info {\n color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-outline-info:hover {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:focus + .btn-outline-info, .btn-outline-info:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-outline-info:disabled, .btn-outline-info.disabled {\n color: #0dcaf0;\n background-color: transparent;\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n.btn-outline-warning:hover {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-outline-warning:disabled, .btn-outline-warning.disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-outline-danger:disabled, .btn-outline-danger.disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-outline-light:hover {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:focus + .btn-outline-light, .btn-outline-light:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-outline-light:disabled, .btn-outline-light.disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-dark {\n color: #212529;\n border-color: #212529;\n}\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-outline-dark:disabled, .btn-outline-dark.disabled {\n color: #212529;\n background-color: transparent;\n}\n\n.btn-link {\n font-weight: 400;\n color: #0d6efd;\n text-decoration: underline;\n}\n.btn-link:hover {\n color: #0a58ca;\n}\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n.collapsing.collapse-horizontal {\n width: 0;\n height: auto;\n transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing.collapse-horizontal {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n z-index: 1000;\n display: none;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: 0.125rem;\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid rgba(0, 0, 0, 0.15);\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: #1e2125;\n background-color: #e9ecef;\n}\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #0d6efd;\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #adb5bd;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1rem;\n color: #212529;\n}\n\n.dropdown-menu-dark {\n color: #dee2e6;\n background-color: #343a40;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.15);\n}\n.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active {\n color: #fff;\n background-color: #0d6efd;\n}\n.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled {\n color: #adb5bd;\n}\n.dropdown-menu-dark .dropdown-divider {\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item-text {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-header {\n color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn ~ .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n color: #0d6efd;\n text-decoration: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: #0a58ca;\n}\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n.nav-tabs .nav-link {\n margin-bottom: -1px;\n background: none;\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n isolation: isolate;\n}\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n background: none;\n border: 0;\n border-radius: 0.25rem;\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #0d6efd;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n transition: box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 0.25rem;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n .navbar-expand-sm .offcanvas-header {\n display: none;\n }\n .navbar-expand-sm .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-sm .offcanvas-top,\n.navbar-expand-sm .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-sm .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n .navbar-expand-md .offcanvas-header {\n display: none;\n }\n .navbar-expand-md .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-md .offcanvas-top,\n.navbar-expand-md .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-md .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n .navbar-expand-lg .offcanvas-header {\n display: none;\n }\n .navbar-expand-lg .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-lg .offcanvas-top,\n.navbar-expand-lg .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-lg .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xl .offcanvas-header {\n display: none;\n }\n .navbar-expand-xl .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-xl .offcanvas-top,\n.navbar-expand-xl .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-xl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xxl .offcanvas-header {\n display: none;\n }\n .navbar-expand-xxl .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-xxl .offcanvas-top,\n.navbar-expand-xxl .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-xxl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n.navbar-expand .offcanvas-header {\n display: none;\n}\n.navbar-expand .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n}\n.navbar-expand .offcanvas-top,\n.navbar-expand .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n}\n.navbar-expand .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.55);\n border-color: rgba(0, 0, 0, 0.1);\n}\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-text a,\n.navbar-light .navbar-text a:hover,\n.navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.55);\n border-color: rgba(255, 255, 255, 0.1);\n}\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-text a,\n.navbar-dark .navbar-text a:hover,\n.navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1rem 1rem;\n}\n\n.card-title {\n margin-bottom: 0.5rem;\n}\n\n.card-subtitle {\n margin-top: -0.25rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link + .card-link {\n margin-left: 1rem;\n}\n\n.card-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-footer {\n padding: 0.5rem 1rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n margin-left: -0.5rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.5rem;\n margin-left: -0.5rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1rem;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-group > .card {\n margin-bottom: 0.75rem;\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n.card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n.card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n.card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n.card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: 1rem 1.25rem;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n background-color: #fff;\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: #0c63e4;\n background-color: #e7f1ff;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n transform: rotate(-180deg);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: 1.25rem;\n height: 1.25rem;\n margin-left: auto;\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-size: 1.25rem;\n transition: transform 0.2s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.accordion-item:first-of-type .accordion-button {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.accordion-item:last-of-type .accordion-button.collapsed {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.accordion-item:last-of-type .accordion-collapse {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.accordion-body {\n padding: 1rem 1.25rem;\n}\n\n.accordion-flush .accordion-collapse {\n border-width: 0;\n}\n.accordion-flush .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush .accordion-item .accordion-button {\n border-radius: 0;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0 0;\n margin-bottom: 1rem;\n list-style: none;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: 0.5rem;\n color: #6c757d;\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n color: #0d6efd;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #dee2e6;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: #0a58ca;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n.page-link:focus {\n z-index: 3;\n color: #0a58ca;\n background-color: #e9ecef;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: -1px;\n}\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.page-link {\n padding: 0.375rem 0.75rem;\n}\n\n.page-item:first-child .page-link {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n}\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n}\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.35em 0.65em;\n font-size: 0.75em;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n position: relative;\n padding: 1rem 1rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n color: #084298;\n background-color: #cfe2ff;\n border-color: #b6d4fe;\n}\n.alert-primary .alert-link {\n color: #06357a;\n}\n\n.alert-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n border-color: #d3d6d8;\n}\n.alert-secondary .alert-link {\n color: #34383c;\n}\n\n.alert-success {\n color: #0f5132;\n background-color: #d1e7dd;\n border-color: #badbcc;\n}\n.alert-success .alert-link {\n color: #0c4128;\n}\n\n.alert-info {\n color: #055160;\n background-color: #cff4fc;\n border-color: #b6effb;\n}\n.alert-info .alert-link {\n color: #04414d;\n}\n\n.alert-warning {\n color: #664d03;\n background-color: #fff3cd;\n border-color: #ffecb5;\n}\n.alert-warning .alert-link {\n color: #523e02;\n}\n\n.alert-danger {\n color: #842029;\n background-color: #f8d7da;\n border-color: #f5c2c7;\n}\n.alert-danger .alert-link {\n color: #6a1a21;\n}\n\n.alert-light {\n color: #636464;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n.alert-light .alert-link {\n color: #4f5050;\n}\n\n.alert-dark {\n color: #141619;\n background-color: #d3d3d4;\n border-color: #bcbebf;\n}\n.alert-dark .alert-link {\n color: #101214;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #0d6efd;\n transition: width 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n -webkit-animation: 1s linear infinite progress-bar-stripes;\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n -webkit-animation: none;\n animation: none;\n }\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > li::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.5rem 1rem;\n color: #212529;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #084298;\n background-color: #cfe2ff;\n}\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #084298;\n background-color: #bacbe6;\n}\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #084298;\n border-color: #084298;\n}\n\n.list-group-item-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n}\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #41464b;\n background-color: #cbccce;\n}\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #41464b;\n border-color: #41464b;\n}\n\n.list-group-item-success {\n color: #0f5132;\n background-color: #d1e7dd;\n}\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #0f5132;\n background-color: #bcd0c7;\n}\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #0f5132;\n border-color: #0f5132;\n}\n\n.list-group-item-info {\n color: #055160;\n background-color: #cff4fc;\n}\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #055160;\n background-color: #badce3;\n}\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #055160;\n border-color: #055160;\n}\n\n.list-group-item-warning {\n color: #664d03;\n background-color: #fff3cd;\n}\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #664d03;\n background-color: #e6dbb9;\n}\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #664d03;\n border-color: #664d03;\n}\n\n.list-group-item-danger {\n color: #842029;\n background-color: #f8d7da;\n}\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #842029;\n background-color: #dfc2c4;\n}\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #842029;\n border-color: #842029;\n}\n\n.list-group-item-light {\n color: #636464;\n background-color: #fefefe;\n}\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #636464;\n background-color: #e5e5e5;\n}\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #636464;\n border-color: #636464;\n}\n\n.list-group-item-dark {\n color: #141619;\n background-color: #d3d3d4;\n}\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #141619;\n background-color: #bebebf;\n}\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #141619;\n border-color: #141619;\n}\n\n.btn-close {\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: #000;\n background: transparent url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\") center/1em auto no-repeat;\n border: 0;\n border-radius: 0.25rem;\n opacity: 0.5;\n}\n.btn-close:hover {\n color: #000;\n text-decoration: none;\n opacity: 0.75;\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n opacity: 1;\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n opacity: 0.25;\n}\n\n.btn-close-white {\n filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n.toast {\n width: 350px;\n max-width: 100%;\n font-size: 0.875rem;\n pointer-events: auto;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.toast.showing {\n opacity: 0;\n}\n.toast:not(.show) {\n display: none;\n}\n\n.toast-container {\n width: -webkit-max-content;\n width: -moz-max-content;\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.5rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.toast-header .btn-close {\n margin-right: -0.375rem;\n margin-left: 0.75rem;\n}\n\n.toast-body {\n padding: 0.75rem;\n word-wrap: break-word;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1055;\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - 1rem);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.modal-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin: -0.5rem -0.5rem -0.5rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n flex-shrink: 0;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n\n .modal-dialog-scrollable {\n height: calc(100% - 3.5rem);\n }\n\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n\n .modal-sm {\n max-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n.modal-xl {\n max-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1080;\n display: block;\n margin: 0;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: 0.9;\n}\n.tooltip .tooltip-arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: 0;\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: 0;\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0 /* rtl:ignore */;\n z-index: 1070;\n display: block;\n max-width: 276px;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n.popover .popover-arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-0.5rem - 1px);\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-0.5rem - 1px);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f0f0f0;\n}\n\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f0f0f0;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 1rem 1rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n/* rtl:begin:ignore */\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n/* rtl:end:ignore */\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n.carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n/* rtl:options: {\n \"autoRename\": true,\n \"stringMap\":[ {\n \"name\" : \"prev-next\",\n \"search\" : \"prev\",\n \"replace\" : \"next\"\n } ]\n} */\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n list-style: none;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: #fff;\n text-align: center;\n}\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n.carousel-dark .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n.carousel-dark .carousel-caption {\n color: #000;\n}\n\n@-webkit-keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n -webkit-animation: 0.75s linear infinite spinner-border;\n animation: 0.75s linear infinite spinner-border;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@-webkit-keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n -webkit-animation: 0.75s linear infinite spinner-grow;\n animation: 0.75s linear infinite spinner-grow;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n.spinner-grow {\n -webkit-animation-duration: 1.5s;\n animation-duration: 1.5s;\n }\n}\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: 1045;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n visibility: hidden;\n background-color: #fff;\n background-clip: padding-box;\n outline: 0;\n transition: transform 0.3s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n\n.offcanvas-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.offcanvas-backdrop.fade {\n opacity: 0;\n}\n.offcanvas-backdrop.show {\n opacity: 0.5;\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n}\n.offcanvas-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin-top: -0.5rem;\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: 1rem 1rem;\n overflow-y: auto;\n}\n\n.offcanvas-start {\n top: 0;\n left: 0;\n width: 400px;\n border-right: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(-100%);\n}\n\n.offcanvas-end {\n top: 0;\n right: 0;\n width: 400px;\n border-left: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(100%);\n}\n\n.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(-100%);\n}\n\n.offcanvas-bottom {\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-top: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(100%);\n}\n\n.offcanvas.show {\n transform: none;\n}\n\n.placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentColor;\n opacity: 0.5;\n}\n.placeholder.btn::before {\n display: inline-block;\n content: \"\";\n}\n\n.placeholder-xs {\n min-height: 0.6em;\n}\n\n.placeholder-sm {\n min-height: 0.8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n -webkit-animation: placeholder-glow 2s ease-in-out infinite;\n animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@-webkit-keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n.placeholder-wave {\n -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n -webkit-mask-size: 200% 100%;\n mask-size: 200% 100%;\n -webkit-animation: placeholder-wave 2s linear infinite;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@-webkit-keyframes placeholder-wave {\n 100% {\n -webkit-mask-position: -200% 0%;\n mask-position: -200% 0%;\n }\n}\n\n@keyframes placeholder-wave {\n 100% {\n -webkit-mask-position: -200% 0%;\n mask-position: -200% 0%;\n }\n}\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.link-primary {\n color: #0d6efd;\n}\n.link-primary:hover, .link-primary:focus {\n color: #0a58ca;\n}\n\n.link-secondary {\n color: #6c757d;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: #565e64;\n}\n\n.link-success {\n color: #198754;\n}\n.link-success:hover, .link-success:focus {\n color: #146c43;\n}\n\n.link-info {\n color: #0dcaf0;\n}\n.link-info:hover, .link-info:focus {\n color: #3dd5f3;\n}\n\n.link-warning {\n color: #ffc107;\n}\n.link-warning:hover, .link-warning:focus {\n color: #ffcd39;\n}\n\n.link-danger {\n color: #dc3545;\n}\n.link-danger:hover, .link-danger:focus {\n color: #b02a37;\n}\n\n.link-light {\n color: #f8f9fa;\n}\n.link-light:hover, .link-light:focus {\n color: #f9fafb;\n}\n\n.link-dark {\n color: #212529;\n}\n.link-dark:hover, .link-dark:focus {\n color: #1a1e21;\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: calc(3 / 4 * 100%);\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: calc(9 / 16 * 100%);\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: calc(9 / 21 * 100%);\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.vr {\n display: inline-block;\n align-self: stretch;\n width: 1px;\n min-height: 1em;\n background-color: currentColor;\n opacity: 0.25;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.opacity-0 {\n opacity: 0 !important;\n}\n\n.opacity-25 {\n opacity: 0.25 !important;\n}\n\n.opacity-50 {\n opacity: 0.5 !important;\n}\n\n.opacity-75 {\n opacity: 0.75 !important;\n}\n\n.opacity-100 {\n opacity: 1 !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #0d6efd !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #198754 !important;\n}\n\n.border-info {\n border-color: #0dcaf0 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #212529 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.border-1 {\n border-width: 1px !important;\n}\n\n.border-2 {\n border-width: 2px !important;\n}\n\n.border-3 {\n border-width: 3px !important;\n}\n\n.border-4 {\n border-width: 4px !important;\n}\n\n.border-5 {\n border-width: 5px !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-body-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n --bs-text-opacity: 1;\n color: #6c757d !important;\n}\n\n.text-black-50 {\n --bs-text-opacity: 1;\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n --bs-text-opacity: 1;\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-reset {\n --bs-text-opacity: 1;\n color: inherit !important;\n}\n\n.text-opacity-25 {\n --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n --bs-text-opacity: 1;\n}\n\n.bg-primary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-body-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n --bs-bg-opacity: 1;\n background-color: transparent !important;\n}\n\n.bg-opacity-10 {\n --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n --bs-bg-opacity: 1;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n -webkit-user-select: all !important;\n -moz-user-select: all !important;\n user-select: all !important;\n}\n\n.user-select-auto {\n -webkit-user-select: auto !important;\n -moz-user-select: auto !important;\n user-select: auto !important;\n}\n\n.user-select-none {\n -webkit-user-select: none !important;\n -moz-user-select: none !important;\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: 0.2rem !important;\n}\n\n.rounded-2 {\n border-radius: 0.25rem !important;\n}\n\n.rounded-3 {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-end {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: 0.25rem !important;\n border-top-left-radius: 0.25rem !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n\n .float-sm-end {\n float: right !important;\n }\n\n .float-sm-none {\n float: none !important;\n }\n\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-sm-0 {\n gap: 0 !important;\n }\n\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n\n .gap-sm-3 {\n gap: 1rem !important;\n }\n\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n\n .gap-sm-5 {\n gap: 3rem !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n\n .text-sm-start {\n text-align: left !important;\n }\n\n .text-sm-end {\n text-align: right !important;\n }\n\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n\n .float-md-end {\n float: right !important;\n }\n\n .float-md-none {\n float: none !important;\n }\n\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-md-0 {\n gap: 0 !important;\n }\n\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n\n .gap-md-3 {\n gap: 1rem !important;\n }\n\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n\n .gap-md-5 {\n gap: 3rem !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n\n .text-md-start {\n text-align: left !important;\n }\n\n .text-md-end {\n text-align: right !important;\n }\n\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n\n .float-lg-end {\n float: right !important;\n }\n\n .float-lg-none {\n float: none !important;\n }\n\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-lg-0 {\n gap: 0 !important;\n }\n\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n\n .gap-lg-3 {\n gap: 1rem !important;\n }\n\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n\n .gap-lg-5 {\n gap: 3rem !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n\n .text-lg-start {\n text-align: left !important;\n }\n\n .text-lg-end {\n text-align: right !important;\n }\n\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n\n .float-xl-end {\n float: right !important;\n }\n\n .float-xl-none {\n float: none !important;\n }\n\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xl-0 {\n gap: 0 !important;\n }\n\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xl-3 {\n gap: 1rem !important;\n }\n\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xl-start {\n text-align: left !important;\n }\n\n .text-xl-end {\n text-align: right !important;\n }\n\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n\n .float-xxl-end {\n float: right !important;\n }\n\n .float-xxl-none {\n float: none !important;\n }\n\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xxl-0 {\n gap: 0 !important;\n }\n\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xxl-start {\n text-align: left !important;\n }\n\n .text-xxl-end {\n text-align: right !important;\n }\n\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n\n .fs-2 {\n font-size: 2rem !important;\n }\n\n .fs-3 {\n font-size: 1.75rem !important;\n }\n\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + ' ' + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n }\n @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + ' ' + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + ' ' + $value;\n }\n\n @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + ' calc(' + $min-width + if($value < 0, ' - ', ' + ') + $variable-width + ')';\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluidVal: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluidVal {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluidVal);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule {\n #{$property}: if($rfs-mode == max-media-query, $fluidVal, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// stylelint-disable property-disallowed-list\n// Single side border-radius\n\n// Helper function to replace negative values with 0\n@function valid-radius($radius) {\n $return: ();\n @each $value in $radius {\n @if type-of($value) == number {\n $return: append($return, max($value, 0));\n } @else {\n $return: append($return, $value);\n }\n }\n @return $return;\n}\n\n// scss-docs-start border-radius-mixins\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: valid-radius($radius);\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-end-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-start-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-start-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-end-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-end-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-start-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n// scss-docs-end border-radius-mixins\n","//\n// Headings\n//\n.h1 {\n @extend h1;\n}\n\n.h2 {\n @extend h2;\n}\n\n.h3 {\n @extend h3;\n}\n\n.h4 {\n @extend h4;\n}\n\n.h5 {\n @extend h5;\n}\n\n.h6 {\n @extend h6;\n}\n\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n@each $display, $font-size in $display-font-sizes {\n .display-#{$display} {\n @include font-size($font-size);\n font-weight: $display-font-weight;\n line-height: $display-line-height;\n }\n}\n\n//\n// Emphasis\n//\n.small {\n @extend small;\n}\n\n.mark {\n @extend mark;\n}\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size($initialism-font-size);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-font-size);\n\n > :last-child {\n margin-bottom: 0;\n }\n}\n\n.blockquote-footer {\n margin-top: -$blockquote-margin-y;\n margin-bottom: $blockquote-margin-y;\n @include font-size($blockquote-footer-font-size);\n color: $blockquote-footer-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all `<img>`s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid();\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid();\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer * .5;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n width: 100%;\n padding-right: var(--#{$variable-prefix}gutter-x, #{$gutter});\n padding-left: var(--#{$variable-prefix}gutter-x, #{$gutter});\n margin-right: auto;\n margin-left: auto;\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$variable-prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$variable-prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$variable-prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$variable-prefix}gutter-x: #{$gutter};\n --#{$variable-prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(var(--#{$variable-prefix}gutter-y) * -1); // stylelint-disable-line function-disallowed-list\n margin-right: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n margin-left: calc(var(--#{$variable-prefix}gutter-x) * -.5); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$variable-prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$variable-prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: divide(100%, $count);\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0%; // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$variable-prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is and invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n --#{$variable-prefix}table-bg: #{$table-bg};\n --#{$variable-prefix}table-accent-bg: #{$table-accent-bg};\n --#{$variable-prefix}table-striped-color: #{$table-striped-color};\n --#{$variable-prefix}table-striped-bg: #{$table-striped-bg};\n --#{$variable-prefix}table-active-color: #{$table-active-color};\n --#{$variable-prefix}table-active-bg: #{$table-active-bg};\n --#{$variable-prefix}table-hover-color: #{$table-hover-color};\n --#{$variable-prefix}table-hover-bg: #{$table-hover-bg};\n\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n vertical-align: $table-cell-vertical-align;\n border-color: $table-border-color;\n\n // Target th & td\n // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class.\n // We use the universal selectors here to simplify the selector (else we would need 6 different selectors).\n // Another advantage is that this generates less code and makes the selector less specific making it easier to override.\n // stylelint-disable-next-line selector-max-universal\n > :not(caption) > * > * {\n padding: $table-cell-padding-y $table-cell-padding-x;\n background-color: var(--#{$variable-prefix}table-bg);\n border-bottom-width: $table-border-width;\n box-shadow: inset 0 0 0 9999px var(--#{$variable-prefix}table-accent-bg);\n }\n\n > tbody {\n vertical-align: inherit;\n }\n\n > thead {\n vertical-align: bottom;\n }\n\n // Highlight border color between thead, tbody and tfoot.\n > :not(:last-child) > :last-child > * {\n border-bottom-color: $table-group-separator-color;\n }\n}\n\n\n//\n// Change placement of captions with a class\n//\n\n.caption-top {\n caption-side: top;\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n // stylelint-disable-next-line selector-max-universal\n > :not(caption) > * > * {\n padding: $table-cell-padding-y-sm $table-cell-padding-x-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n//\n// When borders are added on all sides of the cells, the corners can render odd when\n// these borders do not have the same color or if they are semi-transparent.\n// Therefor we add top and border bottoms to the `tr`s and left and right borders\n// to the `td`s or `th`s\n\n.table-bordered {\n > :not(caption) > * {\n border-width: $table-border-width 0;\n\n // stylelint-disable-next-line selector-max-universal\n > * {\n border-width: 0 $table-border-width;\n }\n }\n}\n\n.table-borderless {\n // stylelint-disable-next-line selector-max-universal\n > :not(caption) > * > * {\n border-bottom-width: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(#{$table-striped-order}) {\n --#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-striped-bg);\n color: var(--#{$variable-prefix}table-striped-color);\n }\n}\n\n// Active table\n//\n// The `.table-active` class can be added to highlight rows or cells\n\n.table-active {\n --#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-active-bg);\n color: var(--#{$variable-prefix}table-active-color);\n}\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n --#{$variable-prefix}table-accent-bg: var(--#{$variable-prefix}table-hover-bg);\n color: var(--#{$variable-prefix}table-hover-color);\n }\n}\n\n\n// Table variants\n//\n// Table variants set the table cell backgrounds, border colors\n// and the colors of the striped, hovered & active tables\n\n@each $color, $value in $table-variants {\n @include table-variant($color, $value);\n}\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @include media-breakpoint-down($breakpoint) {\n .table-responsive#{$infix} {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n }\n}\n","// scss-docs-start table-variant\n@mixin table-variant($state, $background) {\n .table-#{$state} {\n $color: color-contrast(opaque($body-bg, $background));\n $hover-bg: mix($color, $background, percentage($table-hover-bg-factor));\n $striped-bg: mix($color, $background, percentage($table-striped-bg-factor));\n $active-bg: mix($color, $background, percentage($table-active-bg-factor));\n\n --#{$variable-prefix}table-bg: #{$background};\n --#{$variable-prefix}table-striped-bg: #{$striped-bg};\n --#{$variable-prefix}table-striped-color: #{color-contrast($striped-bg)};\n --#{$variable-prefix}table-active-bg: #{$active-bg};\n --#{$variable-prefix}table-active-color: #{color-contrast($active-bg)};\n --#{$variable-prefix}table-hover-bg: #{$hover-bg};\n --#{$variable-prefix}table-hover-color: #{color-contrast($hover-bg)};\n\n color: $color;\n border-color: mix($color, $background, percentage($table-border-factor));\n }\n}\n// scss-docs-end table-variant\n","//\n// Labels\n//\n\n.form-label {\n margin-bottom: $form-label-margin-bottom;\n @include font-size($form-label-font-size);\n font-style: $form-label-font-style;\n font-weight: $form-label-font-weight;\n color: $form-label-color;\n}\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: add($input-padding-y, $input-border-width);\n padding-bottom: add($input-padding-y, $input-border-width);\n margin-bottom: 0; // Override the `<legend>` default\n @include font-size(inherit); // Override the `<legend>` default\n font-style: $form-label-font-style;\n font-weight: $form-label-font-weight;\n line-height: $input-line-height;\n color: $form-label-color;\n}\n\n.col-form-label-lg {\n padding-top: add($input-padding-y-lg, $input-border-width);\n padding-bottom: add($input-padding-y-lg, $input-border-width);\n @include font-size($input-font-size-lg);\n}\n\n.col-form-label-sm {\n padding-top: add($input-padding-y-sm, $input-border-width);\n padding-bottom: add($input-padding-y-sm, $input-border-width);\n @include font-size($input-font-size-sm);\n}\n","//\n// Form text\n//\n\n.form-text {\n margin-top: $form-text-margin-top;\n @include font-size($form-text-font-size);\n font-style: $form-text-font-style;\n font-weight: $form-text-font-weight;\n color: $form-text-color;\n}\n","//\n// General form controls (plus a few specific high-level interventions)\n//\n\n.form-control {\n display: block;\n width: 100%;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n appearance: none; // Fix appearance for date inputs in Safari\n\n // Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n &[type=\"file\"] {\n overflow: hidden; // prevent pseudo element button overlap\n\n &:not(:disabled):not([readonly]) {\n cursor: pointer;\n }\n }\n\n // Customize the `:focus` state to imitate native WebKit styles.\n &:focus {\n color: $input-focus-color;\n background-color: $input-focus-bg;\n border-color: $input-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($input-box-shadow, $input-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $input-focus-box-shadow;\n }\n }\n\n // Add some height to date inputs on iOS\n // https://github.com/twbs/bootstrap/issues/23307\n // TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved\n &::-webkit-date-and-time-value {\n // Multiply line-height by 1em if it has no unit\n height: if(unit($input-line-height) == \"\", $input-line-height * 1em, $input-line-height);\n }\n\n // Placeholder\n &::placeholder {\n color: $input-placeholder-color;\n // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526.\n opacity: 1;\n }\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &:disabled,\n &[readonly] {\n background-color: $input-disabled-bg;\n border-color: $input-disabled-border-color;\n // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.\n opacity: 1;\n }\n\n // File input buttons theming\n &::file-selector-button {\n padding: $input-padding-y $input-padding-x;\n margin: (-$input-padding-y) (-$input-padding-x);\n margin-inline-end: $input-padding-x;\n color: $form-file-button-color;\n @include gradient-bg($form-file-button-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: $input-border-width;\n border-radius: 0; // stylelint-disable-line property-disallowed-list\n @include transition($btn-transition);\n }\n\n &:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: $form-file-button-hover-bg;\n }\n\n &::-webkit-file-upload-button {\n padding: $input-padding-y $input-padding-x;\n margin: (-$input-padding-y) (-$input-padding-x);\n margin-inline-end: $input-padding-x;\n color: $form-file-button-color;\n @include gradient-bg($form-file-button-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: $input-border-width;\n border-radius: 0; // stylelint-disable-line property-disallowed-list\n @include transition($btn-transition);\n }\n\n &:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: $form-file-button-hover-bg;\n }\n}\n\n// Readonly controls as plain text\n//\n// Apply class to a readonly input to make it appear like regular plain\n// text (without any border, background color, focus indicator)\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: $input-padding-y 0;\n margin-bottom: 0; // match inputs if this class comes on inputs with default margins\n line-height: $input-line-height;\n color: $input-plaintext-color;\n background-color: transparent;\n border: solid transparent;\n border-width: $input-border-width 0;\n\n &.form-control-sm,\n &.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n//\n// Repeated in `_input_group.scss` to avoid Sass extend issues.\n\n.form-control-sm {\n min-height: $input-height-sm;\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n @include border-radius($input-border-radius-sm);\n\n &::file-selector-button {\n padding: $input-padding-y-sm $input-padding-x-sm;\n margin: (-$input-padding-y-sm) (-$input-padding-x-sm);\n margin-inline-end: $input-padding-x-sm;\n }\n\n &::-webkit-file-upload-button {\n padding: $input-padding-y-sm $input-padding-x-sm;\n margin: (-$input-padding-y-sm) (-$input-padding-x-sm);\n margin-inline-end: $input-padding-x-sm;\n }\n}\n\n.form-control-lg {\n min-height: $input-height-lg;\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n @include border-radius($input-border-radius-lg);\n\n &::file-selector-button {\n padding: $input-padding-y-lg $input-padding-x-lg;\n margin: (-$input-padding-y-lg) (-$input-padding-x-lg);\n margin-inline-end: $input-padding-x-lg;\n }\n\n &::-webkit-file-upload-button {\n padding: $input-padding-y-lg $input-padding-x-lg;\n margin: (-$input-padding-y-lg) (-$input-padding-x-lg);\n margin-inline-end: $input-padding-x-lg;\n }\n}\n\n// Make sure textareas don't shrink too much when resized\n// https://github.com/twbs/bootstrap/pull/29124\n// stylelint-disable selector-no-qualifying-type\ntextarea {\n &.form-control {\n min-height: $input-height;\n }\n\n &.form-control-sm {\n min-height: $input-height-sm;\n }\n\n &.form-control-lg {\n min-height: $input-height-lg;\n }\n}\n// stylelint-enable selector-no-qualifying-type\n\n.form-control-color {\n width: $form-color-width;\n height: auto; // Override fixed browser height\n padding: $input-padding-y;\n\n &:not(:disabled):not([readonly]) {\n cursor: pointer;\n }\n\n &::-moz-color-swatch {\n height: if(unit($input-line-height) == \"\", $input-line-height * 1em, $input-line-height);\n @include border-radius($input-border-radius);\n }\n\n &::-webkit-color-swatch {\n height: if(unit($input-line-height) == \"\", $input-line-height * 1em, $input-line-height);\n @include border-radius($input-border-radius);\n }\n}\n","// stylelint-disable property-disallowed-list\n@mixin transition($transition...) {\n @if length($transition) == 0 {\n $transition: $transition-base;\n }\n\n @if length($transition) > 1 {\n @each $value in $transition {\n @if $value == null or $value == none {\n @warn \"The keyword 'none' or 'null' must be used as a single argument.\";\n }\n }\n }\n\n @if $enable-transitions {\n @if nth($transition, 1) != null {\n transition: $transition;\n }\n\n @if $enable-reduced-motion and nth($transition, 1) != null and nth($transition, 1) != none {\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n}\n","// Gradients\n\n// scss-docs-start gradient-bg-mixin\n@mixin gradient-bg($color: null) {\n background-color: $color;\n\n @if $enable-gradients {\n background-image: var(--#{$variable-prefix}gradient);\n }\n}\n// scss-docs-end gradient-bg-mixin\n\n// scss-docs-start gradient-mixins\n// Horizontal gradient, from left to right\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);\n}\n\n// Vertical gradient, from top to bottom\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: null, $end-percent: null) {\n background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);\n}\n\n@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {\n background-image: linear-gradient($deg, $start-color, $end-color);\n}\n\n@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);\n}\n\n@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);\n}\n\n@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {\n background-image: radial-gradient(circle, $inner-color, $outer-color);\n}\n\n@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {\n background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);\n}\n// scss-docs-end gradient-mixins\n","// Select\n//\n// Replaces the browser default select with a custom one, mostly pulled from\n// https://primer.github.io/.\n\n.form-select {\n display: block;\n width: 100%;\n padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x;\n // stylelint-disable-next-line property-no-vendor-prefix\n -moz-padding-start: subtract($form-select-padding-x, 3px); // See https://github.com/twbs/bootstrap/issues/32636\n font-family: $form-select-font-family;\n @include font-size($form-select-font-size);\n font-weight: $form-select-font-weight;\n line-height: $form-select-line-height;\n color: $form-select-color;\n background-color: $form-select-bg;\n background-image: escape-svg($form-select-indicator);\n background-repeat: no-repeat;\n background-position: $form-select-bg-position;\n background-size: $form-select-bg-size;\n border: $form-select-border-width solid $form-select-border-color;\n @include border-radius($form-select-border-radius, 0);\n @include box-shadow($form-select-box-shadow);\n @include transition($form-select-transition);\n appearance: none;\n\n &:focus {\n border-color: $form-select-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $form-select-focus-box-shadow;\n }\n }\n\n &[multiple],\n &[size]:not([size=\"1\"]) {\n padding-right: $form-select-padding-x;\n background-image: none;\n }\n\n &:disabled {\n color: $form-select-disabled-color;\n background-color: $form-select-disabled-bg;\n border-color: $form-select-disabled-border-color;\n }\n\n // Remove outline from select box in FF\n &:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 $form-select-color;\n }\n}\n\n.form-select-sm {\n padding-top: $form-select-padding-y-sm;\n padding-bottom: $form-select-padding-y-sm;\n padding-left: $form-select-padding-x-sm;\n @include font-size($form-select-font-size-sm);\n}\n\n.form-select-lg {\n padding-top: $form-select-padding-y-lg;\n padding-bottom: $form-select-padding-y-lg;\n padding-left: $form-select-padding-x-lg;\n @include font-size($form-select-font-size-lg);\n}\n","//\n// Check/radio\n//\n\n.form-check {\n display: block;\n min-height: $form-check-min-height;\n padding-left: $form-check-padding-start;\n margin-bottom: $form-check-margin-bottom;\n\n .form-check-input {\n float: left;\n margin-left: $form-check-padding-start * -1;\n }\n}\n\n.form-check-input {\n width: $form-check-input-width;\n height: $form-check-input-width;\n margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height\n vertical-align: top;\n background-color: $form-check-input-bg;\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: $form-check-input-border;\n appearance: none;\n color-adjust: exact; // Keep themed appearance for print\n @include transition($form-check-transition);\n\n &[type=\"checkbox\"] {\n @include border-radius($form-check-input-border-radius);\n }\n\n &[type=\"radio\"] {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: $form-check-radio-border-radius;\n }\n\n &:active {\n filter: $form-check-input-active-filter;\n }\n\n &:focus {\n border-color: $form-check-input-focus-border;\n outline: 0;\n box-shadow: $form-check-input-focus-box-shadow;\n }\n\n &:checked {\n background-color: $form-check-input-checked-bg-color;\n border-color: $form-check-input-checked-border-color;\n\n &[type=\"checkbox\"] {\n @if $enable-gradients {\n background-image: escape-svg($form-check-input-checked-bg-image), var(--#{$variable-prefix}gradient);\n } @else {\n background-image: escape-svg($form-check-input-checked-bg-image);\n }\n }\n\n &[type=\"radio\"] {\n @if $enable-gradients {\n background-image: escape-svg($form-check-radio-checked-bg-image), var(--#{$variable-prefix}gradient);\n } @else {\n background-image: escape-svg($form-check-radio-checked-bg-image);\n }\n }\n }\n\n &[type=\"checkbox\"]:indeterminate {\n background-color: $form-check-input-indeterminate-bg-color;\n border-color: $form-check-input-indeterminate-border-color;\n\n @if $enable-gradients {\n background-image: escape-svg($form-check-input-indeterminate-bg-image), var(--#{$variable-prefix}gradient);\n } @else {\n background-image: escape-svg($form-check-input-indeterminate-bg-image);\n }\n }\n\n &:disabled {\n pointer-events: none;\n filter: none;\n opacity: $form-check-input-disabled-opacity;\n }\n\n // Use disabled attribute in addition of :disabled pseudo-class\n // See: https://github.com/twbs/bootstrap/issues/28247\n &[disabled],\n &:disabled {\n ~ .form-check-label {\n opacity: $form-check-label-disabled-opacity;\n }\n }\n}\n\n.form-check-label {\n color: $form-check-label-color;\n cursor: $form-check-label-cursor;\n}\n\n//\n// Switch\n//\n\n.form-switch {\n padding-left: $form-switch-padding-start;\n\n .form-check-input {\n width: $form-switch-width;\n margin-left: $form-switch-padding-start * -1;\n background-image: escape-svg($form-switch-bg-image);\n background-position: left center;\n @include border-radius($form-switch-border-radius);\n @include transition($form-switch-transition);\n\n &:focus {\n background-image: escape-svg($form-switch-focus-bg-image);\n }\n\n &:checked {\n background-position: $form-switch-checked-bg-position;\n\n @if $enable-gradients {\n background-image: escape-svg($form-switch-checked-bg-image), var(--#{$variable-prefix}gradient);\n } @else {\n background-image: escape-svg($form-switch-checked-bg-image);\n }\n }\n }\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: $form-check-inline-margin-end;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n\n &[disabled],\n &:disabled {\n + .btn {\n pointer-events: none;\n filter: none;\n opacity: $form-check-btn-check-disabled-opacity;\n }\n }\n}\n","// Range\n//\n// Style range inputs the same across browsers. Vendor-specific rules for pseudo\n// elements cannot be mixed. As such, there are no shared styles for focus or\n// active states on prefixed selectors.\n\n.form-range {\n width: 100%;\n height: add($form-range-thumb-height, $form-range-thumb-focus-box-shadow-width * 2);\n padding: 0; // Need to reset padding\n background-color: transparent;\n appearance: none;\n\n &:focus {\n outline: 0;\n\n // Pseudo-elements must be split across multiple rulesets to have an effect.\n // No box-shadow() mixin for focus accessibility.\n &::-webkit-slider-thumb { box-shadow: $form-range-thumb-focus-box-shadow; }\n &::-moz-range-thumb { box-shadow: $form-range-thumb-focus-box-shadow; }\n }\n\n &::-moz-focus-outer {\n border: 0;\n }\n\n &::-webkit-slider-thumb {\n width: $form-range-thumb-width;\n height: $form-range-thumb-height;\n margin-top: ($form-range-track-height - $form-range-thumb-height) * .5; // Webkit specific\n @include gradient-bg($form-range-thumb-bg);\n border: $form-range-thumb-border;\n @include border-radius($form-range-thumb-border-radius);\n @include box-shadow($form-range-thumb-box-shadow);\n @include transition($form-range-thumb-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($form-range-thumb-active-bg);\n }\n }\n\n &::-webkit-slider-runnable-track {\n width: $form-range-track-width;\n height: $form-range-track-height;\n color: transparent; // Why?\n cursor: $form-range-track-cursor;\n background-color: $form-range-track-bg;\n border-color: transparent;\n @include border-radius($form-range-track-border-radius);\n @include box-shadow($form-range-track-box-shadow);\n }\n\n &::-moz-range-thumb {\n width: $form-range-thumb-width;\n height: $form-range-thumb-height;\n @include gradient-bg($form-range-thumb-bg);\n border: $form-range-thumb-border;\n @include border-radius($form-range-thumb-border-radius);\n @include box-shadow($form-range-thumb-box-shadow);\n @include transition($form-range-thumb-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($form-range-thumb-active-bg);\n }\n }\n\n &::-moz-range-track {\n width: $form-range-track-width;\n height: $form-range-track-height;\n color: transparent;\n cursor: $form-range-track-cursor;\n background-color: $form-range-track-bg;\n border-color: transparent; // Firefox specific?\n @include border-radius($form-range-track-border-radius);\n @include box-shadow($form-range-track-box-shadow);\n }\n\n &:disabled {\n pointer-events: none;\n\n &::-webkit-slider-thumb {\n background-color: $form-range-thumb-disabled-bg;\n }\n\n &::-moz-range-thumb {\n background-color: $form-range-thumb-disabled-bg;\n }\n }\n}\n",".form-floating {\n position: relative;\n\n > .form-control,\n > .form-select {\n height: $form-floating-height;\n line-height: $form-floating-line-height;\n }\n\n > label {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%; // allow textareas\n padding: $form-floating-padding-y $form-floating-padding-x;\n pointer-events: none;\n border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model\n transform-origin: 0 0;\n @include transition($form-floating-transition);\n }\n\n // stylelint-disable no-duplicate-selectors\n > .form-control {\n padding: $form-floating-padding-y $form-floating-padding-x;\n\n &::placeholder {\n color: transparent;\n }\n\n &:focus,\n &:not(:placeholder-shown) {\n padding-top: $form-floating-input-padding-t;\n padding-bottom: $form-floating-input-padding-b;\n }\n // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped\n &:-webkit-autofill {\n padding-top: $form-floating-input-padding-t;\n padding-bottom: $form-floating-input-padding-b;\n }\n }\n\n > .form-select {\n padding-top: $form-floating-input-padding-t;\n padding-bottom: $form-floating-input-padding-b;\n }\n\n > .form-control:focus,\n > .form-control:not(:placeholder-shown),\n > .form-select {\n ~ label {\n opacity: $form-floating-label-opacity;\n transform: $form-floating-label-transform;\n }\n }\n // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped\n > .form-control:-webkit-autofill {\n ~ label {\n opacity: $form-floating-label-opacity;\n transform: $form-floating-label-transform;\n }\n }\n // stylelint-enable no-duplicate-selectors\n}\n","//\n// Base styles\n//\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap; // For form validation feedback\n align-items: stretch;\n width: 100%;\n\n > .form-control,\n > .form-select {\n position: relative; // For focus state's z-index\n flex: 1 1 auto;\n width: 1%;\n min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size\n }\n\n // Bring the \"active\" form control to the top of surrounding elements\n > .form-control:focus,\n > .form-select:focus {\n z-index: 3;\n }\n\n // Ensure buttons are always above inputs for more visually pleasing borders.\n // This isn't needed for `.input-group-text` since it shares the same border-color\n // as our inputs.\n .btn {\n position: relative;\n z-index: 2;\n\n &:focus {\n z-index: 3;\n }\n }\n}\n\n\n// Textual addons\n//\n// Serves as a catch-all element for any text or radio/checkbox input you wish\n// to prepend or append to an input.\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: $input-group-addon-padding-y $input-group-addon-padding-x;\n @include font-size($input-font-size); // Match inputs\n font-weight: $input-group-addon-font-weight;\n line-height: $input-line-height;\n color: $input-group-addon-color;\n text-align: center;\n white-space: nowrap;\n background-color: $input-group-addon-bg;\n border: $input-border-width solid $input-group-addon-border-color;\n @include border-radius($input-border-radius);\n}\n\n\n// Sizing\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n @include border-radius($input-border-radius-lg);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n @include border-radius($input-border-radius-sm);\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: $form-select-padding-x + $form-select-indicator-padding;\n}\n\n\n// Rounded corners\n//\n// These rulesets must come after the sizing ones to properly override sm and lg\n// border-radius values when extending. They're more specific than we'd like\n// with the `.input-group >` part, but without it, we cannot override the sizing.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.input-group {\n &:not(.has-validation) {\n > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu),\n > .dropdown-toggle:nth-last-child(n + 3) {\n @include border-end-radius(0);\n }\n }\n\n &.has-validation {\n > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu),\n > .dropdown-toggle:nth-last-child(n + 4) {\n @include border-end-radius(0);\n }\n }\n\n $validation-messages: \"\";\n @each $state in map-keys($form-validation-states) {\n $validation-messages: $validation-messages + \":not(.\" + unquote($state) + \"-tooltip)\" + \":not(.\" + unquote($state) + \"-feedback)\";\n }\n\n > :not(:first-child):not(.dropdown-menu)#{$validation-messages} {\n margin-left: -$input-border-width;\n @include border-start-radius(0);\n }\n}\n","// This mixin uses an `if()` technique to be compatible with Dart Sass\n// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details\n\n// scss-docs-start form-validation-mixins\n@mixin form-validation-state-selector($state) {\n @if ($state == \"valid\" or $state == \"invalid\") {\n .was-validated #{if(&, \"&\", \"\")}:#{$state},\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n } @else {\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n }\n}\n\n@mixin form-validation-state(\n $state,\n $color,\n $icon,\n $tooltip-color: color-contrast($color),\n $tooltip-bg-color: rgba($color, $form-feedback-tooltip-opacity),\n $focus-box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity)\n) {\n .#{$state}-feedback {\n display: none;\n width: 100%;\n margin-top: $form-feedback-margin-top;\n @include font-size($form-feedback-font-size);\n font-style: $form-feedback-font-style;\n color: $color;\n }\n\n .#{$state}-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%; // Contain to parent when possible\n padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;\n margin-top: .1rem;\n @include font-size($form-feedback-tooltip-font-size);\n line-height: $form-feedback-tooltip-line-height;\n color: $tooltip-color;\n background-color: $tooltip-bg-color;\n @include border-radius($form-feedback-tooltip-border-radius);\n }\n\n @include form-validation-state-selector($state) {\n ~ .#{$state}-feedback,\n ~ .#{$state}-tooltip {\n display: block;\n }\n }\n\n .form-control {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-image: escape-svg($icon);\n background-repeat: no-repeat;\n background-position: right $input-height-inner-quarter center;\n background-size: $input-height-inner-half $input-height-inner-half;\n }\n\n &:focus {\n border-color: $color;\n box-shadow: $focus-box-shadow;\n }\n }\n }\n\n // stylelint-disable-next-line selector-no-qualifying-type\n textarea.form-control {\n @include form-validation-state-selector($state) {\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-position: top $input-height-inner-quarter right $input-height-inner-quarter;\n }\n }\n }\n\n .form-select {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n &:not([multiple]):not([size]),\n &:not([multiple])[size=\"1\"] {\n padding-right: $form-select-feedback-icon-padding-end;\n background-image: escape-svg($form-select-indicator), escape-svg($icon);\n background-position: $form-select-bg-position, $form-select-feedback-icon-position;\n background-size: $form-select-bg-size, $form-select-feedback-icon-size;\n }\n }\n\n &:focus {\n border-color: $color;\n box-shadow: $focus-box-shadow;\n }\n }\n }\n\n .form-check-input {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n &:checked {\n background-color: $color;\n }\n\n &:focus {\n box-shadow: $focus-box-shadow;\n }\n\n ~ .form-check-label {\n color: $color;\n }\n }\n }\n .form-check-inline .form-check-input {\n ~ .#{$state}-feedback {\n margin-left: .5em;\n }\n }\n\n .input-group .form-control,\n .input-group .form-select {\n @include form-validation-state-selector($state) {\n @if $state == \"valid\" {\n z-index: 1;\n } @else if $state == \"invalid\" {\n z-index: 2;\n }\n &:focus {\n z-index: 3;\n }\n }\n }\n}\n// scss-docs-end form-validation-mixins\n","//\n// Base styles\n//\n\n.btn {\n display: inline-block;\n font-family: $btn-font-family;\n font-weight: $btn-font-weight;\n line-height: $btn-line-height;\n color: $body-color;\n text-align: center;\n text-decoration: if($link-decoration == none, null, none);\n white-space: $btn-white-space;\n vertical-align: middle;\n cursor: if($enable-button-pointers, pointer, null);\n user-select: none;\n background-color: transparent;\n border: $btn-border-width solid transparent;\n @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-border-radius);\n @include transition($btn-transition);\n\n &:hover {\n color: $body-color;\n text-decoration: if($link-hover-decoration == underline, none, null);\n }\n\n .btn-check:focus + &,\n &:focus {\n outline: 0;\n box-shadow: $btn-focus-box-shadow;\n }\n\n .btn-check:checked + &,\n .btn-check:active + &,\n &:active,\n &.active {\n @include box-shadow($btn-active-box-shadow);\n\n &:focus {\n @include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow);\n }\n }\n\n &:disabled,\n &.disabled,\n fieldset:disabled & {\n pointer-events: none;\n opacity: $btn-disabled-opacity;\n @include box-shadow(none);\n }\n}\n\n\n//\n// Alternate buttons\n//\n\n// scss-docs-start btn-variant-loops\n@each $color, $value in $theme-colors {\n .btn-#{$color} {\n @include button-variant($value, $value);\n }\n}\n\n@each $color, $value in $theme-colors {\n .btn-outline-#{$color} {\n @include button-outline-variant($value);\n }\n}\n// scss-docs-end btn-variant-loops\n\n\n//\n// Link buttons\n//\n\n// Make a button look and behave like a link\n.btn-link {\n font-weight: $font-weight-normal;\n color: $btn-link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $btn-link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n\n &:focus {\n text-decoration: $link-hover-decoration;\n }\n\n &:disabled,\n &.disabled {\n color: $btn-link-disabled-color;\n }\n\n // No need for an active state here\n}\n\n\n//\n// Button Sizes\n//\n\n.btn-lg {\n @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);\n}\n\n.btn-sm {\n @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n// scss-docs-start btn-variant-mixin\n@mixin button-variant(\n $background,\n $border,\n $color: color-contrast($background),\n $hover-background: if($color == $color-contrast-light, shade-color($background, $btn-hover-bg-shade-amount), tint-color($background, $btn-hover-bg-tint-amount)),\n $hover-border: if($color == $color-contrast-light, shade-color($border, $btn-hover-border-shade-amount), tint-color($border, $btn-hover-border-tint-amount)),\n $hover-color: color-contrast($hover-background),\n $active-background: if($color == $color-contrast-light, shade-color($background, $btn-active-bg-shade-amount), tint-color($background, $btn-active-bg-tint-amount)),\n $active-border: if($color == $color-contrast-light, shade-color($border, $btn-active-border-shade-amount), tint-color($border, $btn-active-border-tint-amount)),\n $active-color: color-contrast($active-background),\n $disabled-background: $background,\n $disabled-border: $border,\n $disabled-color: color-contrast($disabled-background)\n) {\n color: $color;\n @include gradient-bg($background);\n border-color: $border;\n @include box-shadow($btn-box-shadow);\n\n &:hover {\n color: $hover-color;\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n }\n\n .btn-check:focus + &,\n &:focus {\n color: $hover-color;\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n @if $enable-shadows {\n @include box-shadow($btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix($color, $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $border, 15%), .5);\n }\n }\n\n .btn-check:checked + &,\n .btn-check:active + &,\n &:active,\n &.active,\n .show > &.dropdown-toggle {\n color: $active-color;\n background-color: $active-background;\n // Remove CSS gradients if they're enabled\n background-image: if($enable-gradients, none, null);\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix($color, $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix($color, $border, 15%), .5);\n }\n }\n }\n\n &:disabled,\n &.disabled {\n color: $disabled-color;\n background-color: $disabled-background;\n // Remove CSS gradients if they're enabled\n background-image: if($enable-gradients, none, null);\n border-color: $disabled-border;\n }\n}\n// scss-docs-end btn-variant-mixin\n\n// scss-docs-start btn-outline-variant-mixin\n@mixin button-outline-variant(\n $color,\n $color-hover: color-contrast($color),\n $active-background: $color,\n $active-border: $color,\n $active-color: color-contrast($active-background)\n) {\n color: $color;\n border-color: $color;\n\n &:hover {\n color: $color-hover;\n background-color: $active-background;\n border-color: $active-border;\n }\n\n .btn-check:focus + &,\n &:focus {\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n\n .btn-check:checked + &,\n .btn-check:active + &,\n &:active,\n &.active,\n &.dropdown-toggle.show {\n color: $active-color;\n background-color: $active-background;\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n }\n }\n\n &:disabled,\n &.disabled {\n color: $color;\n background-color: transparent;\n }\n}\n// scss-docs-end btn-outline-variant-mixin\n\n// scss-docs-start btn-size-mixin\n@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n // Manually declare to provide an override to the browser default\n @include border-radius($border-radius, 0);\n}\n// scss-docs-end btn-size-mixin\n",".fade {\n @include transition($transition-fade);\n\n &:not(.show) {\n opacity: 0;\n }\n}\n\n// scss-docs-start collapse-classes\n.collapse {\n &:not(.show) {\n display: none;\n }\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n @include transition($transition-collapse);\n\n &.collapse-horizontal {\n width: 0;\n height: auto;\n @include transition($transition-collapse-width);\n }\n}\n// scss-docs-end collapse-classes\n","// The dropdown wrapper (`<div>`)\n.dropup,\n.dropend,\n.dropdown,\n.dropstart {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n\n // Generate the caret automatically\n @include caret();\n}\n\n// The dropdown menu\n.dropdown-menu {\n position: absolute;\n z-index: $zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n min-width: $dropdown-min-width;\n padding: $dropdown-padding-y $dropdown-padding-x;\n margin: 0; // Override default margin of ul\n @include font-size($dropdown-font-size);\n color: $dropdown-color;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n list-style: none;\n background-color: $dropdown-bg;\n background-clip: padding-box;\n border: $dropdown-border-width solid $dropdown-border-color;\n @include border-radius($dropdown-border-radius);\n @include box-shadow($dropdown-box-shadow);\n\n &[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: $dropdown-spacer;\n }\n}\n\n// scss-docs-start responsive-breakpoints\n// We deliberately hardcode the `bs-` prefix because we check\n// this custom property in JS to determine Popper's positioning\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .dropdown-menu#{$infix}-start {\n --bs-position: start;\n\n &[data-bs-popper] {\n right: auto;\n left: 0;\n }\n }\n\n .dropdown-menu#{$infix}-end {\n --bs-position: end;\n\n &[data-bs-popper] {\n right: 0;\n left: auto;\n }\n }\n }\n}\n// scss-docs-end responsive-breakpoints\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n// Just add .dropup after the standard .dropdown class and you're set.\n.dropup {\n .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(up);\n }\n}\n\n.dropend {\n .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(end);\n &::after {\n vertical-align: 0;\n }\n }\n}\n\n.dropstart {\n .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(start);\n &::before {\n vertical-align: 0;\n }\n }\n}\n\n\n// Dividers (basically an `<hr>`) within the dropdown\n.dropdown-divider {\n height: 0;\n margin: $dropdown-divider-margin-y 0;\n overflow: hidden;\n border-top: 1px solid $dropdown-divider-bg;\n}\n\n// Links, buttons, and more within the dropdown menu\n//\n// `<button>`-specific styles are denoted with `// For <button>s`\n.dropdown-item {\n display: block;\n width: 100%; // For `<button>`s\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n clear: both;\n font-weight: $font-weight-normal;\n color: $dropdown-link-color;\n text-align: inherit; // For `<button>`s\n text-decoration: if($link-decoration == none, null, none);\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n background-color: transparent; // For `<button>`s\n border: 0; // For `<button>`s\n\n // Prevent dropdown overflow if there's no padding\n // See https://github.com/twbs/bootstrap/pull/27703\n @if $dropdown-padding-y == 0 {\n &:first-child {\n @include border-top-radius($dropdown-inner-border-radius);\n }\n\n &:last-child {\n @include border-bottom-radius($dropdown-inner-border-radius);\n }\n }\n\n &:hover,\n &:focus {\n color: $dropdown-link-hover-color;\n text-decoration: if($link-hover-decoration == underline, none, null);\n @include gradient-bg($dropdown-link-hover-bg);\n }\n\n &.active,\n &:active {\n color: $dropdown-link-active-color;\n text-decoration: none;\n @include gradient-bg($dropdown-link-active-bg);\n }\n\n &.disabled,\n &:disabled {\n color: $dropdown-link-disabled-color;\n pointer-events: none;\n background-color: transparent;\n // Remove CSS gradients if they're enabled\n background-image: if($enable-gradients, none, null);\n }\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: $dropdown-header-padding;\n margin-bottom: 0; // for use with heading elements\n @include font-size($font-size-sm);\n color: $dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Dropdown text\n.dropdown-item-text {\n display: block;\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n color: $dropdown-link-color;\n}\n\n// Dark dropdowns\n.dropdown-menu-dark {\n color: $dropdown-dark-color;\n background-color: $dropdown-dark-bg;\n border-color: $dropdown-dark-border-color;\n @include box-shadow($dropdown-dark-box-shadow);\n\n .dropdown-item {\n color: $dropdown-dark-link-color;\n\n &:hover,\n &:focus {\n color: $dropdown-dark-link-hover-color;\n @include gradient-bg($dropdown-dark-link-hover-bg);\n }\n\n &.active,\n &:active {\n color: $dropdown-dark-link-active-color;\n @include gradient-bg($dropdown-dark-link-active-bg);\n }\n\n &.disabled,\n &:disabled {\n color: $dropdown-dark-link-disabled-color;\n }\n }\n\n .dropdown-divider {\n border-color: $dropdown-dark-divider-bg;\n }\n\n .dropdown-item-text {\n color: $dropdown-dark-link-color;\n }\n\n .dropdown-header {\n color: $dropdown-dark-header-color;\n }\n}\n","// scss-docs-start caret-mixins\n@mixin caret-down {\n border-top: $caret-width solid;\n border-right: $caret-width solid transparent;\n border-bottom: 0;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-up {\n border-top: 0;\n border-right: $caret-width solid transparent;\n border-bottom: $caret-width solid;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-end {\n border-top: $caret-width solid transparent;\n border-right: 0;\n border-bottom: $caret-width solid transparent;\n border-left: $caret-width solid;\n}\n\n@mixin caret-start {\n border-top: $caret-width solid transparent;\n border-right: $caret-width solid;\n border-bottom: $caret-width solid transparent;\n}\n\n@mixin caret($direction: down) {\n @if $enable-caret {\n &::after {\n display: inline-block;\n margin-left: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @if $direction == down {\n @include caret-down();\n } @else if $direction == up {\n @include caret-up();\n } @else if $direction == end {\n @include caret-end();\n }\n }\n\n @if $direction == start {\n &::after {\n display: none;\n }\n\n &::before {\n display: inline-block;\n margin-right: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @include caret-start();\n }\n }\n\n &:empty::after {\n margin-left: 0;\n }\n }\n}\n// scss-docs-end caret-mixins\n","// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle; // match .btn alignment given font-size hack above\n\n > .btn {\n position: relative;\n flex: 1 1 auto;\n }\n\n // Bring the hover, focused, and \"active\" buttons to the front to overlay\n // the borders properly\n > .btn-check:checked + .btn,\n > .btn-check:focus + .btn,\n > .btn:hover,\n > .btn:focus,\n > .btn:active,\n > .btn.active {\n z-index: 1;\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n\n .input-group {\n width: auto;\n }\n}\n\n.btn-group {\n // Prevent double borders when buttons are next to each other\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-left: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-end-radius(0);\n }\n\n // The left radius should be 0 if the button is:\n // - the \"third or more\" child\n // - the second child and the previous element isn't `.btn-check` (making it the first child visually)\n // - part of a btn-group which isn't the first child\n > .btn:nth-child(n + 3),\n > :not(.btn-check) + .btn,\n > .btn-group:not(:first-child) > .btn {\n @include border-start-radius(0);\n }\n}\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-sm > .btn { @extend .btn-sm; }\n.btn-group-lg > .btn { @extend .btn-lg; }\n\n\n//\n// Split button dropdowns\n//\n\n.dropdown-toggle-split {\n padding-right: $btn-padding-x * .75;\n padding-left: $btn-padding-x * .75;\n\n &::after,\n .dropup &::after,\n .dropend &::after {\n margin-left: 0;\n }\n\n .dropstart &::before {\n margin-right: 0;\n }\n}\n\n.btn-sm + .dropdown-toggle-split {\n padding-right: $btn-padding-x-sm * .75;\n padding-left: $btn-padding-x-sm * .75;\n}\n\n.btn-lg + .dropdown-toggle-split {\n padding-right: $btn-padding-x-lg * .75;\n padding-left: $btn-padding-x-lg * .75;\n}\n\n\n// The clickable button for toggling the menu\n// Set the same inset shadow as the :active state\n.btn-group.show .dropdown-toggle {\n @include box-shadow($btn-active-box-shadow);\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n @include box-shadow(none);\n }\n}\n\n\n//\n// Vertical button groups\n//\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n\n > .btn,\n > .btn-group {\n width: 100%;\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-top: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-bottom-radius(0);\n }\n\n > .btn ~ .btn,\n > .btn-group:not(:first-child) > .btn {\n @include border-top-radius(0);\n }\n}\n","// Base class\n//\n// Kickstart any navigation component with a set of style resets. Works with\n// `<nav>`s, `<ul>`s or `<ol>`s.\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: $nav-link-padding-y $nav-link-padding-x;\n @include font-size($nav-link-font-size);\n font-weight: $nav-link-font-weight;\n color: $nav-link-color;\n text-decoration: if($link-decoration == none, null, none);\n @include transition($nav-link-transition);\n\n &:hover,\n &:focus {\n color: $nav-link-hover-color;\n text-decoration: if($link-hover-decoration == underline, none, null);\n }\n\n // Disabled state lightens text\n &.disabled {\n color: $nav-link-disabled-color;\n pointer-events: none;\n cursor: default;\n }\n}\n\n//\n// Tabs\n//\n\n.nav-tabs {\n border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;\n\n .nav-link {\n margin-bottom: -$nav-tabs-border-width;\n background: none;\n border: $nav-tabs-border-width solid transparent;\n @include border-top-radius($nav-tabs-border-radius);\n\n &:hover,\n &:focus {\n border-color: $nav-tabs-link-hover-border-color;\n // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link\n isolation: isolate;\n }\n\n &.disabled {\n color: $nav-link-disabled-color;\n background-color: transparent;\n border-color: transparent;\n }\n }\n\n .nav-link.active,\n .nav-item.show .nav-link {\n color: $nav-tabs-link-active-color;\n background-color: $nav-tabs-link-active-bg;\n border-color: $nav-tabs-link-active-border-color;\n }\n\n .dropdown-menu {\n // Make dropdown border overlap tab border\n margin-top: -$nav-tabs-border-width;\n // Remove the top rounded corners here since there is a hard edge above the menu\n @include border-top-radius(0);\n }\n}\n\n\n//\n// Pills\n//\n\n.nav-pills {\n .nav-link {\n background: none;\n border: 0;\n @include border-radius($nav-pills-border-radius);\n }\n\n .nav-link.active,\n .show > .nav-link {\n color: $nav-pills-link-active-color;\n @include gradient-bg($nav-pills-link-active-bg);\n }\n}\n\n\n//\n// Justified variants\n//\n\n.nav-fill {\n > .nav-link,\n .nav-item {\n flex: 1 1 auto;\n text-align: center;\n }\n}\n\n.nav-justified {\n > .nav-link,\n .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n }\n}\n\n.nav-fill,\n.nav-justified {\n .nav-item .nav-link {\n width: 100%; // Make sure button will grow\n }\n}\n\n\n// Tabbable tabs\n//\n// Hide tabbable panes to start, show them when `.active`\n\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n","// Contents\n//\n// Navbar\n// Navbar brand\n// Navbar nav\n// Navbar text\n// Responsive navbar\n// Navbar position\n// Navbar themes\n\n\n// Navbar\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap; // allow us to do the line break for collapsing content\n align-items: center;\n justify-content: space-between; // space out brand from logo\n padding-top: $navbar-padding-y;\n padding-right: $navbar-padding-x; // default: null\n padding-bottom: $navbar-padding-y;\n padding-left: $navbar-padding-x; // default: null\n @include gradient-bg();\n\n // Because flex properties aren't inherited, we need to redeclare these first\n // few properties so that content nested within behave properly.\n // The `flex-wrap` property is inherited to simplify the expanded navbars\n %container-flex-properties {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n }\n\n > .container,\n > .container-fluid {\n @extend %container-flex-properties;\n }\n\n @each $breakpoint, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($breakpoint, $container-max-widths)} {\n @extend %container-flex-properties;\n }\n }\n}\n\n\n// Navbar brand\n//\n// Used for brand, project, or site names.\n\n.navbar-brand {\n padding-top: $navbar-brand-padding-y;\n padding-bottom: $navbar-brand-padding-y;\n margin-right: $navbar-brand-margin-end;\n @include font-size($navbar-brand-font-size);\n text-decoration: if($link-decoration == none, null, none);\n white-space: nowrap;\n\n &:hover,\n &:focus {\n text-decoration: if($link-hover-decoration == underline, none, null);\n }\n}\n\n\n// Navbar nav\n//\n// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).\n\n.navbar-nav {\n display: flex;\n flex-direction: column; // cannot use `inherit` to get the `.navbar`s value\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n\n .nav-link {\n padding-right: 0;\n padding-left: 0;\n }\n\n .dropdown-menu {\n position: static;\n }\n}\n\n\n// Navbar text\n//\n//\n\n.navbar-text {\n padding-top: $nav-link-padding-y;\n padding-bottom: $nav-link-padding-y;\n}\n\n\n// Responsive navbar\n//\n// Custom styles for responsive collapsing and toggling of navbar contents.\n// Powered by the collapse Bootstrap JavaScript plugin.\n\n// When collapsed, prevent the toggleable navbar contents from appearing in\n// the default flexbox row orientation. Requires the use of `flex-wrap: wrap`\n// on the `.navbar` parent.\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n // For always expanded or extra full navbars, ensure content aligns itself\n // properly vertically. Can be easily overridden with flex utilities.\n align-items: center;\n}\n\n// Button for toggling the navbar when in its collapsed state\n.navbar-toggler {\n padding: $navbar-toggler-padding-y $navbar-toggler-padding-x;\n @include font-size($navbar-toggler-font-size);\n line-height: 1;\n background-color: transparent; // remove default button style\n border: $border-width solid transparent; // remove default button style\n @include border-radius($navbar-toggler-border-radius);\n @include transition($navbar-toggler-transition);\n\n &:hover {\n text-decoration: none;\n }\n\n &:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 $navbar-toggler-focus-width;\n }\n}\n\n// Keep as a separate element so folks can easily override it with another icon\n// or image file as needed.\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--#{$variable-prefix}scroll-height, 75vh);\n overflow-y: auto;\n}\n\n// scss-docs-start navbar-expand-loop\n// Generate series of `.navbar-expand-*` responsive classes for configuring\n// where your navbar collapses.\n.navbar-expand {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n // stylelint-disable-next-line scss/selector-no-union-class-name\n &#{$infix} {\n @include media-breakpoint-up($next) {\n flex-wrap: nowrap;\n justify-content: flex-start;\n\n .navbar-nav {\n flex-direction: row;\n\n .dropdown-menu {\n position: absolute;\n }\n\n .nav-link {\n padding-right: $navbar-nav-link-padding-x;\n padding-left: $navbar-nav-link-padding-x;\n }\n }\n\n .navbar-nav-scroll {\n overflow: visible;\n }\n\n .navbar-collapse {\n display: flex !important; // stylelint-disable-line declaration-no-important\n flex-basis: auto;\n }\n\n .navbar-toggler {\n display: none;\n }\n\n .offcanvas-header {\n display: none;\n }\n\n .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important; // stylelint-disable-line declaration-no-important\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n @include transition(none);\n transform: none;\n }\n .offcanvas-top,\n .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n\n .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n }\n }\n }\n}\n// scss-docs-end navbar-expand-loop\n\n// Navbar themes\n//\n// Styles for switching between navbars with light or dark background.\n\n// Dark links against a light background\n.navbar-light {\n .navbar-brand {\n color: $navbar-light-brand-color;\n\n &:hover,\n &:focus {\n color: $navbar-light-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-light-color;\n\n &:hover,\n &:focus {\n color: $navbar-light-hover-color;\n }\n\n &.disabled {\n color: $navbar-light-disabled-color;\n }\n }\n\n .show > .nav-link,\n .nav-link.active {\n color: $navbar-light-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-light-color;\n border-color: $navbar-light-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-light-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-light-color;\n\n a,\n a:hover,\n a:focus {\n color: $navbar-light-active-color;\n }\n }\n}\n\n// White links against a dark background\n.navbar-dark {\n .navbar-brand {\n color: $navbar-dark-brand-color;\n\n &:hover,\n &:focus {\n color: $navbar-dark-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-dark-color;\n\n &:hover,\n &:focus {\n color: $navbar-dark-hover-color;\n }\n\n &.disabled {\n color: $navbar-dark-disabled-color;\n }\n }\n\n .show > .nav-link,\n .nav-link.active {\n color: $navbar-dark-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-dark-color;\n border-color: $navbar-dark-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-dark-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-dark-color;\n a,\n a:hover,\n a:focus {\n color: $navbar-dark-active-color;\n }\n }\n}\n","//\n// Base styles\n//\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106\n height: $card-height;\n word-wrap: break-word;\n background-color: $card-bg;\n background-clip: border-box;\n border: $card-border-width solid $card-border-color;\n @include border-radius($card-border-radius);\n @include box-shadow($card-box-shadow);\n\n > hr {\n margin-right: 0;\n margin-left: 0;\n }\n\n > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n\n &:first-child {\n border-top-width: 0;\n @include border-top-radius($card-inner-border-radius);\n }\n\n &:last-child {\n border-bottom-width: 0;\n @include border-bottom-radius($card-inner-border-radius);\n }\n }\n\n // Due to specificity of the above selector (`.card > .list-group`), we must\n // use a child selector here to prevent double borders.\n > .card-header + .list-group,\n > .list-group + .card-footer {\n border-top: 0;\n }\n}\n\n.card-body {\n // Enable `flex-grow: 1` for decks and groups so that card blocks take up\n // as much space as possible, ensuring footers are aligned to the bottom.\n flex: 1 1 auto;\n padding: $card-spacer-y $card-spacer-x;\n color: $card-color;\n}\n\n.card-title {\n margin-bottom: $card-title-spacer-y;\n}\n\n.card-subtitle {\n margin-top: -$card-title-spacer-y * .5;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link {\n &:hover {\n text-decoration: if($link-hover-decoration == underline, none, null);\n }\n\n + .card-link {\n margin-left: $card-spacer-x;\n }\n}\n\n//\n// Optional textual caps\n//\n\n.card-header {\n padding: $card-cap-padding-y $card-cap-padding-x;\n margin-bottom: 0; // Removes the default margin-bottom of <hN>\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-bottom: $card-border-width solid $card-border-color;\n\n &:first-child {\n @include border-radius($card-inner-border-radius $card-inner-border-radius 0 0);\n }\n}\n\n.card-footer {\n padding: $card-cap-padding-y $card-cap-padding-x;\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-top: $card-border-width solid $card-border-color;\n\n &:last-child {\n @include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius);\n }\n}\n\n\n//\n// Header navs\n//\n\n.card-header-tabs {\n margin-right: -$card-cap-padding-x * .5;\n margin-bottom: -$card-cap-padding-y;\n margin-left: -$card-cap-padding-x * .5;\n border-bottom: 0;\n\n @if $nav-tabs-link-active-bg != $card-bg {\n .nav-link.active {\n background-color: $card-bg;\n border-bottom-color: $card-bg;\n }\n }\n}\n\n.card-header-pills {\n margin-right: -$card-cap-padding-x * .5;\n margin-left: -$card-cap-padding-x * .5;\n}\n\n// Card image\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: $card-img-overlay-padding;\n @include border-radius($card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch\n}\n\n.card-img,\n.card-img-top {\n @include border-top-radius($card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n @include border-bottom-radius($card-inner-border-radius);\n}\n\n\n//\n// Card groups\n//\n\n.card-group {\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n margin-bottom: $card-group-margin;\n }\n\n @include media-breakpoint-up(sm) {\n display: flex;\n flex-flow: row wrap;\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n flex: 1 0 0%;\n margin-bottom: 0;\n\n + .card {\n margin-left: 0;\n border-left: 0;\n }\n\n // Handle rounded corners\n @if $enable-rounded {\n &:not(:last-child) {\n @include border-end-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-disallowed-list\n border-top-right-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-disallowed-list\n border-bottom-right-radius: 0;\n }\n }\n\n &:not(:first-child) {\n @include border-start-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-disallowed-list\n border-top-left-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-disallowed-list\n border-bottom-left-radius: 0;\n }\n }\n }\n }\n }\n}\n","//\n// Base styles\n//\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: $accordion-button-padding-y $accordion-button-padding-x;\n @include font-size($font-size-base);\n color: $accordion-button-color;\n text-align: left; // Reset button style\n background-color: $accordion-button-bg;\n border: 0;\n @include border-radius(0);\n overflow-anchor: none;\n @include transition($accordion-transition);\n\n &:not(.collapsed) {\n color: $accordion-button-active-color;\n background-color: $accordion-button-active-bg;\n box-shadow: inset 0 ($accordion-border-width * -1) 0 $accordion-border-color;\n\n &::after {\n background-image: escape-svg($accordion-button-active-icon);\n transform: $accordion-icon-transform;\n }\n }\n\n // Accordion icon\n &::after {\n flex-shrink: 0;\n width: $accordion-icon-width;\n height: $accordion-icon-width;\n margin-left: auto;\n content: \"\";\n background-image: escape-svg($accordion-button-icon);\n background-repeat: no-repeat;\n background-size: $accordion-icon-width;\n @include transition($accordion-icon-transition);\n }\n\n &:hover {\n z-index: 2;\n }\n\n &:focus {\n z-index: 3;\n border-color: $accordion-button-focus-border-color;\n outline: 0;\n box-shadow: $accordion-button-focus-box-shadow;\n }\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n background-color: $accordion-bg;\n border: $accordion-border-width solid $accordion-border-color;\n\n &:first-of-type {\n @include border-top-radius($accordion-border-radius);\n\n .accordion-button {\n @include border-top-radius($accordion-inner-border-radius);\n }\n }\n\n &:not(:first-of-type) {\n border-top: 0;\n }\n\n // Only set a border-radius on the last item if the accordion is collapsed\n &:last-of-type {\n @include border-bottom-radius($accordion-border-radius);\n\n .accordion-button {\n &.collapsed {\n @include border-bottom-radius($accordion-inner-border-radius);\n }\n }\n\n .accordion-collapse {\n @include border-bottom-radius($accordion-border-radius);\n }\n }\n}\n\n.accordion-body {\n padding: $accordion-body-padding-y $accordion-body-padding-x;\n}\n\n\n// Flush accordion items\n//\n// Remove borders and border-radius to keep accordion items edge-to-edge.\n\n.accordion-flush {\n .accordion-collapse {\n border-width: 0;\n }\n\n .accordion-item {\n border-right: 0;\n border-left: 0;\n @include border-radius(0);\n\n &:first-child { border-top: 0; }\n &:last-child { border-bottom: 0; }\n\n .accordion-button {\n @include border-radius(0);\n }\n }\n}\n",".breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: $breadcrumb-padding-y $breadcrumb-padding-x;\n margin-bottom: $breadcrumb-margin-bottom;\n @include font-size($breadcrumb-font-size);\n list-style: none;\n background-color: $breadcrumb-bg;\n @include border-radius($breadcrumb-border-radius);\n}\n\n.breadcrumb-item {\n // The separator between breadcrumbs (by default, a forward-slash: \"/\")\n + .breadcrumb-item {\n padding-left: $breadcrumb-item-padding-x;\n\n &::before {\n float: left; // Suppress inline spacings and underlining of the separator\n padding-right: $breadcrumb-item-padding-x;\n color: $breadcrumb-divider-color;\n content: var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{\"/* rtl:\"} var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{\"*/\"};\n }\n }\n\n &.active {\n color: $breadcrumb-active-color;\n }\n}\n",".pagination {\n display: flex;\n @include list-unstyled();\n}\n\n.page-link {\n position: relative;\n display: block;\n color: $pagination-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $pagination-bg;\n border: $pagination-border-width solid $pagination-border-color;\n @include transition($pagination-transition);\n\n &:hover {\n z-index: 2;\n color: $pagination-hover-color;\n text-decoration: if($link-hover-decoration == underline, none, null);\n background-color: $pagination-hover-bg;\n border-color: $pagination-hover-border-color;\n }\n\n &:focus {\n z-index: 3;\n color: $pagination-focus-color;\n background-color: $pagination-focus-bg;\n outline: $pagination-focus-outline;\n box-shadow: $pagination-focus-box-shadow;\n }\n}\n\n.page-item {\n &:not(:first-child) .page-link {\n margin-left: $pagination-margin-start;\n }\n\n &.active .page-link {\n z-index: 3;\n color: $pagination-active-color;\n @include gradient-bg($pagination-active-bg);\n border-color: $pagination-active-border-color;\n }\n\n &.disabled .page-link {\n color: $pagination-disabled-color;\n pointer-events: none;\n background-color: $pagination-disabled-bg;\n border-color: $pagination-disabled-border-color;\n }\n}\n\n\n//\n// Sizing\n//\n@include pagination-size($pagination-padding-y, $pagination-padding-x, null, $pagination-border-radius);\n\n.pagination-lg {\n @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $pagination-border-radius-lg);\n}\n\n.pagination-sm {\n @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $pagination-border-radius-sm);\n}\n","// Pagination\n\n// scss-docs-start pagination-mixin\n@mixin pagination-size($padding-y, $padding-x, $font-size, $border-radius) {\n .page-link {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n }\n\n .page-item {\n @if $pagination-margin-start == (-$pagination-border-width) {\n &:first-child {\n .page-link {\n @include border-start-radius($border-radius);\n }\n }\n\n &:last-child {\n .page-link {\n @include border-end-radius($border-radius);\n }\n }\n } @else {\n //Add border-radius to all pageLinks in case they have left margin\n .page-link {\n @include border-radius($border-radius);\n }\n }\n }\n}\n// scss-docs-end pagination-mixin\n","// Base class\n//\n// Requires one of the contextual, color modifier classes for `color` and\n// `background-color`.\n\n.badge {\n display: inline-block;\n padding: $badge-padding-y $badge-padding-x;\n @include font-size($badge-font-size);\n font-weight: $badge-font-weight;\n line-height: 1;\n color: $badge-color;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n @include border-radius($badge-border-radius);\n @include gradient-bg();\n\n // Empty badges collapse automatically\n &:empty {\n display: none;\n }\n}\n\n// Quick fix for badges in buttons\n.btn .badge {\n position: relative;\n top: -1px;\n}\n","//\n// Base styles\n//\n\n.alert {\n position: relative;\n padding: $alert-padding-y $alert-padding-x;\n margin-bottom: $alert-margin-bottom;\n border: $alert-border-width solid transparent;\n @include border-radius($alert-border-radius);\n}\n\n// Headings for larger alerts\n.alert-heading {\n // Specified to prevent conflicts of changing $headings-color\n color: inherit;\n}\n\n// Provide class for links that match alerts\n.alert-link {\n font-weight: $alert-link-font-weight;\n}\n\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissible {\n padding-right: $alert-dismissible-padding-r;\n\n // Adjust close link position\n .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: $stretched-link-z-index + 1;\n padding: $alert-padding-y * 1.25 $alert-padding-x;\n }\n}\n\n\n// scss-docs-start alert-modifiers\n// Generate contextual modifier classes for colorizing the alert.\n\n@each $state, $value in $theme-colors {\n $alert-background: shift-color($value, $alert-bg-scale);\n $alert-border: shift-color($value, $alert-border-scale);\n $alert-color: shift-color($value, $alert-color-scale);\n @if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) {\n $alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale));\n }\n .alert-#{$state} {\n @include alert-variant($alert-background, $alert-border, $alert-color);\n }\n}\n// scss-docs-end alert-modifiers\n","// scss-docs-start alert-variant-mixin\n@mixin alert-variant($background, $border, $color) {\n color: $color;\n @include gradient-bg($background);\n border-color: $border;\n\n .alert-link {\n color: shade-color($color, 20%);\n }\n}\n// scss-docs-end alert-variant-mixin\n","// Disable animation if transitions are disabled\n\n// scss-docs-start progress-keyframes\n@if $enable-transitions {\n @keyframes progress-bar-stripes {\n 0% { background-position-x: $progress-height; }\n }\n}\n// scss-docs-end progress-keyframes\n\n.progress {\n display: flex;\n height: $progress-height;\n overflow: hidden; // force rounded corners by cropping it\n @include font-size($progress-font-size);\n background-color: $progress-bg;\n @include border-radius($progress-border-radius);\n @include box-shadow($progress-box-shadow);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: $progress-bar-color;\n text-align: center;\n white-space: nowrap;\n background-color: $progress-bar-bg;\n @include transition($progress-bar-transition);\n}\n\n.progress-bar-striped {\n @include gradient-striped();\n background-size: $progress-height $progress-height;\n}\n\n@if $enable-transitions {\n .progress-bar-animated {\n animation: $progress-bar-animation-timing progress-bar-stripes;\n\n @if $enable-reduced-motion {\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n }\n }\n}\n","// Base class\n//\n// Easily usable on <ul>, <ol>, or <div>.\n\n.list-group {\n display: flex;\n flex-direction: column;\n\n // No need to set list-style: none; since .list-group-item is block level\n padding-left: 0; // reset padding because ul and ol\n margin-bottom: 0;\n @include border-radius($list-group-border-radius);\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n\n > li::before {\n // Increments only this instance of the section counter\n content: counters(section, \".\") \". \";\n counter-increment: section;\n }\n}\n\n\n// Interactive list items\n//\n// Use anchor or button elements instead of `li`s or `div`s to create interactive\n// list items. Includes an extra `.active` modifier class for selected items.\n\n.list-group-item-action {\n width: 100%; // For `<button>`s (anchors become 100% by default though)\n color: $list-group-action-color;\n text-align: inherit; // For `<button>`s (anchors inherit)\n\n // Hover state\n &:hover,\n &:focus {\n z-index: 1; // Place hover/focus items above their siblings for proper border styling\n color: $list-group-action-hover-color;\n text-decoration: none;\n background-color: $list-group-hover-bg;\n }\n\n &:active {\n color: $list-group-action-active-color;\n background-color: $list-group-action-active-bg;\n }\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: $list-group-item-padding-y $list-group-item-padding-x;\n color: $list-group-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $list-group-bg;\n border: $list-group-border-width solid $list-group-border-color;\n\n &:first-child {\n @include border-top-radius(inherit);\n }\n\n &:last-child {\n @include border-bottom-radius(inherit);\n }\n\n &.disabled,\n &:disabled {\n color: $list-group-disabled-color;\n pointer-events: none;\n background-color: $list-group-disabled-bg;\n }\n\n // Include both here for `<a>`s and `<button>`s\n &.active {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: $list-group-active-color;\n background-color: $list-group-active-bg;\n border-color: $list-group-active-border-color;\n }\n\n & + & {\n border-top-width: 0;\n\n &.active {\n margin-top: -$list-group-border-width;\n border-top-width: $list-group-border-width;\n }\n }\n}\n\n\n// Horizontal\n//\n// Change the layout of list group items from vertical (default) to horizontal.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .list-group-horizontal#{$infix} {\n flex-direction: row;\n\n > .list-group-item {\n &:first-child {\n @include border-bottom-start-radius($list-group-border-radius);\n @include border-top-end-radius(0);\n }\n\n &:last-child {\n @include border-top-end-radius($list-group-border-radius);\n @include border-bottom-start-radius(0);\n }\n\n &.active {\n margin-top: 0;\n }\n\n + .list-group-item {\n border-top-width: $list-group-border-width;\n border-left-width: 0;\n\n &.active {\n margin-left: -$list-group-border-width;\n border-left-width: $list-group-border-width;\n }\n }\n }\n }\n }\n}\n\n\n// Flush list items\n//\n// Remove borders and border-radius to keep list group items edge-to-edge. Most\n// useful within other components (e.g., cards).\n\n.list-group-flush {\n @include border-radius(0);\n\n > .list-group-item {\n border-width: 0 0 $list-group-border-width;\n\n &:last-child {\n border-bottom-width: 0;\n }\n }\n}\n\n\n// scss-docs-start list-group-modifiers\n// List group contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n@each $state, $value in $theme-colors {\n $list-group-variant-bg: shift-color($value, $list-group-item-bg-scale);\n $list-group-variant-color: shift-color($value, $list-group-item-color-scale);\n @if (contrast-ratio($list-group-variant-bg, $list-group-variant-color) < $min-contrast-ratio) {\n $list-group-variant-color: mix($value, color-contrast($list-group-variant-bg), abs($list-group-item-color-scale));\n }\n\n @include list-group-item-variant($state, $list-group-variant-bg, $list-group-variant-color);\n}\n// scss-docs-end list-group-modifiers\n","// List Groups\n\n// scss-docs-start list-group-mixin\n@mixin list-group-item-variant($state, $background, $color) {\n .list-group-item-#{$state} {\n color: $color;\n background-color: $background;\n\n &.list-group-item-action {\n &:hover,\n &:focus {\n color: $color;\n background-color: shade-color($background, 10%);\n }\n\n &.active {\n color: $white;\n background-color: $color;\n border-color: $color;\n }\n }\n }\n}\n// scss-docs-end list-group-mixin\n","// transparent background and border properties included for button version.\n// iOS requires the button element instead of an anchor tag.\n// If you want the anchor version, it requires `href=\"#\"`.\n// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n.btn-close {\n box-sizing: content-box;\n width: $btn-close-width;\n height: $btn-close-height;\n padding: $btn-close-padding-y $btn-close-padding-x;\n color: $btn-close-color;\n background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements\n border: 0; // for button elements\n @include border-radius();\n opacity: $btn-close-opacity;\n\n // Override <a>'s hover style\n &:hover {\n color: $btn-close-color;\n text-decoration: none;\n opacity: $btn-close-hover-opacity;\n }\n\n &:focus {\n outline: 0;\n box-shadow: $btn-close-focus-shadow;\n opacity: $btn-close-focus-opacity;\n }\n\n &:disabled,\n &.disabled {\n pointer-events: none;\n user-select: none;\n opacity: $btn-close-disabled-opacity;\n }\n}\n\n.btn-close-white {\n filter: $btn-close-white-filter;\n}\n",".toast {\n width: $toast-max-width;\n max-width: 100%;\n @include font-size($toast-font-size);\n color: $toast-color;\n pointer-events: auto;\n background-color: $toast-background-color;\n background-clip: padding-box;\n border: $toast-border-width solid $toast-border-color;\n box-shadow: $toast-box-shadow;\n @include border-radius($toast-border-radius);\n\n &.showing {\n opacity: 0;\n }\n\n &:not(.show) {\n display: none;\n }\n}\n\n.toast-container {\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n\n > :not(:last-child) {\n margin-bottom: $toast-spacing;\n }\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: $toast-padding-y $toast-padding-x;\n color: $toast-header-color;\n background-color: $toast-header-background-color;\n background-clip: padding-box;\n border-bottom: $toast-border-width solid $toast-header-border-color;\n @include border-top-radius(subtract($toast-border-radius, $toast-border-width));\n\n .btn-close {\n margin-right: $toast-padding-x * -.5;\n margin-left: $toast-padding-x;\n }\n}\n\n.toast-body {\n padding: $toast-padding-x; // apply to both vertical and horizontal\n word-wrap: break-word;\n}\n","// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and stuff\n\n\n// Container that the modal scrolls within\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex-modal;\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a\n // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342\n // See also https://github.com/twbs/bootstrap/issues/17695\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: $modal-dialog-margin;\n // allow clicks to pass through for custom click handling to close modal\n pointer-events: none;\n\n // When fading in the modal, animate it to slide down\n .modal.fade & {\n @include transition($modal-transition);\n transform: $modal-fade-transform;\n }\n .modal.show & {\n transform: $modal-show-transform;\n }\n\n // When trying to close, animate focus to scale\n .modal.modal-static & {\n transform: $modal-scale-transform;\n }\n}\n\n.modal-dialog-scrollable {\n height: subtract(100%, $modal-dialog-margin * 2);\n\n .modal-content {\n max-height: 100%;\n overflow: hidden;\n }\n\n .modal-body {\n overflow-y: auto;\n }\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: subtract(100%, $modal-dialog-margin * 2);\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`\n // counteract the pointer-events: none; in the .modal-dialog\n color: $modal-content-color;\n pointer-events: auto;\n background-color: $modal-content-bg;\n background-clip: padding-box;\n border: $modal-content-border-width solid $modal-content-border-color;\n @include border-radius($modal-content-border-radius);\n @include box-shadow($modal-content-box-shadow-xs);\n // Remove focus outline from opened modal\n outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n @include overlay-backdrop($zindex-modal-backdrop, $modal-backdrop-bg, $modal-backdrop-opacity);\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends\n padding: $modal-header-padding;\n border-bottom: $modal-header-border-width solid $modal-header-border-color;\n @include border-top-radius($modal-content-inner-border-radius);\n\n .btn-close {\n padding: ($modal-header-padding-y * .5) ($modal-header-padding-x * .5);\n margin: ($modal-header-padding-y * -.5) ($modal-header-padding-x * -.5) ($modal-header-padding-y * -.5) auto;\n }\n}\n\n// Title text within header\n.modal-title {\n margin-bottom: 0;\n line-height: $modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n // Enable `flex-grow: 1` so that the body take up as much space as possible\n // when there should be a fixed height on `.modal-dialog`.\n flex: 1 1 auto;\n padding: $modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n flex-shrink: 0;\n align-items: center; // vertically center\n justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items\n padding: $modal-inner-padding - $modal-footer-margin-between * .5;\n border-top: $modal-footer-border-width solid $modal-footer-border-color;\n @include border-bottom-radius($modal-content-inner-border-radius);\n\n // Place margin between footer elements\n // This solution is far from ideal because of the universal selector usage,\n // but is needed to fix https://github.com/twbs/bootstrap/issues/24800\n > * {\n margin: $modal-footer-margin-between * .5;\n }\n}\n\n// Scale up the modal\n@include media-breakpoint-up(sm) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n max-width: $modal-md;\n margin: $modal-dialog-margin-y-sm-up auto;\n }\n\n .modal-dialog-scrollable {\n height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n }\n\n .modal-dialog-centered {\n min-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n }\n\n .modal-content {\n @include box-shadow($modal-content-box-shadow-sm-up);\n }\n\n .modal-sm { max-width: $modal-sm; }\n}\n\n@include media-breakpoint-up(lg) {\n .modal-lg,\n .modal-xl {\n max-width: $modal-lg;\n }\n}\n\n@include media-breakpoint-up(xl) {\n .modal-xl { max-width: $modal-xl; }\n}\n\n// scss-docs-start modal-fullscreen-loop\n@each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n $postfix: if($infix != \"\", $infix + \"-down\", \"\");\n\n @include media-breakpoint-down($breakpoint) {\n .modal-fullscreen#{$postfix} {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n\n .modal-content {\n height: 100%;\n border: 0;\n @include border-radius(0);\n }\n\n .modal-header {\n @include border-radius(0);\n }\n\n .modal-body {\n overflow-y: auto;\n }\n\n .modal-footer {\n @include border-radius(0);\n }\n }\n }\n}\n// scss-docs-end modal-fullscreen-loop\n","// Shared between modals and offcanvases\n@mixin overlay-backdrop($zindex, $backdrop-bg, $backdrop-opacity) {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex;\n width: 100vw;\n height: 100vh;\n background-color: $backdrop-bg;\n\n // Fade for backdrop\n &.fade { opacity: 0; }\n &.show { opacity: $backdrop-opacity; }\n}\n","// Base class\n.tooltip {\n position: absolute;\n z-index: $zindex-tooltip;\n display: block;\n margin: $tooltip-margin;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($tooltip-font-size);\n // Allow breaking very long words so they don't overflow the tooltip's bounds\n word-wrap: break-word;\n opacity: 0;\n\n &.show { opacity: $tooltip-opacity; }\n\n .tooltip-arrow {\n position: absolute;\n display: block;\n width: $tooltip-arrow-width;\n height: $tooltip-arrow-height;\n\n &::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-tooltip-top {\n padding: $tooltip-arrow-height 0;\n\n .tooltip-arrow {\n bottom: 0;\n\n &::before {\n top: -1px;\n border-width: $tooltip-arrow-height ($tooltip-arrow-width * .5) 0;\n border-top-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-end {\n padding: 0 $tooltip-arrow-height;\n\n .tooltip-arrow {\n left: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n right: -1px;\n border-width: ($tooltip-arrow-width * .5) $tooltip-arrow-height ($tooltip-arrow-width * .5) 0;\n border-right-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-bottom {\n padding: $tooltip-arrow-height 0;\n\n .tooltip-arrow {\n top: 0;\n\n &::before {\n bottom: -1px;\n border-width: 0 ($tooltip-arrow-width * .5) $tooltip-arrow-height;\n border-bottom-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-start {\n padding: 0 $tooltip-arrow-height;\n\n .tooltip-arrow {\n right: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n left: -1px;\n border-width: ($tooltip-arrow-width * .5) 0 ($tooltip-arrow-width * .5) $tooltip-arrow-height;\n border-left-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-auto {\n &[data-popper-placement^=\"top\"] {\n @extend .bs-tooltip-top;\n }\n &[data-popper-placement^=\"right\"] {\n @extend .bs-tooltip-end;\n }\n &[data-popper-placement^=\"bottom\"] {\n @extend .bs-tooltip-bottom;\n }\n &[data-popper-placement^=\"left\"] {\n @extend .bs-tooltip-start;\n }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: $tooltip-max-width;\n padding: $tooltip-padding-y $tooltip-padding-x;\n color: $tooltip-color;\n text-align: center;\n background-color: $tooltip-bg;\n @include border-radius($tooltip-border-radius);\n}\n","@mixin reset-text {\n font-family: $font-family-base;\n // We deliberately do NOT reset font-size or overflow-wrap / word-wrap.\n font-style: normal;\n font-weight: $font-weight-normal;\n line-height: $line-height-base;\n text-align: left; // Fallback for where `start` is not supported\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n}\n",".popover {\n position: absolute;\n top: 0;\n left: 0 #{\"/* rtl:ignore */\"};\n z-index: $zindex-popover;\n display: block;\n max-width: $popover-max-width;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($popover-font-size);\n // Allow breaking very long words so they don't overflow the popover's bounds\n word-wrap: break-word;\n background-color: $popover-bg;\n background-clip: padding-box;\n border: $popover-border-width solid $popover-border-color;\n @include border-radius($popover-border-radius);\n @include box-shadow($popover-box-shadow);\n\n .popover-arrow {\n position: absolute;\n display: block;\n width: $popover-arrow-width;\n height: $popover-arrow-height;\n\n &::before,\n &::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-popover-top {\n > .popover-arrow {\n bottom: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n bottom: 0;\n border-width: $popover-arrow-height ($popover-arrow-width * .5) 0;\n border-top-color: $popover-arrow-outer-color;\n }\n\n &::after {\n bottom: $popover-border-width;\n border-width: $popover-arrow-height ($popover-arrow-width * .5) 0;\n border-top-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-end {\n > .popover-arrow {\n left: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n\n &::before {\n left: 0;\n border-width: ($popover-arrow-width * .5) $popover-arrow-height ($popover-arrow-width * .5) 0;\n border-right-color: $popover-arrow-outer-color;\n }\n\n &::after {\n left: $popover-border-width;\n border-width: ($popover-arrow-width * .5) $popover-arrow-height ($popover-arrow-width * .5) 0;\n border-right-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-bottom {\n > .popover-arrow {\n top: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n top: 0;\n border-width: 0 ($popover-arrow-width * .5) $popover-arrow-height ($popover-arrow-width * .5);\n border-bottom-color: $popover-arrow-outer-color;\n }\n\n &::after {\n top: $popover-border-width;\n border-width: 0 ($popover-arrow-width * .5) $popover-arrow-height ($popover-arrow-width * .5);\n border-bottom-color: $popover-arrow-color;\n }\n }\n\n // This will remove the popover-header's border just below the arrow\n .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: $popover-arrow-width;\n margin-left: -$popover-arrow-width * .5;\n content: \"\";\n border-bottom: $popover-border-width solid $popover-header-bg;\n }\n}\n\n.bs-popover-start {\n > .popover-arrow {\n right: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n\n &::before {\n right: 0;\n border-width: ($popover-arrow-width * .5) 0 ($popover-arrow-width * .5) $popover-arrow-height;\n border-left-color: $popover-arrow-outer-color;\n }\n\n &::after {\n right: $popover-border-width;\n border-width: ($popover-arrow-width * .5) 0 ($popover-arrow-width * .5) $popover-arrow-height;\n border-left-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-auto {\n &[data-popper-placement^=\"top\"] {\n @extend .bs-popover-top;\n }\n &[data-popper-placement^=\"right\"] {\n @extend .bs-popover-end;\n }\n &[data-popper-placement^=\"bottom\"] {\n @extend .bs-popover-bottom;\n }\n &[data-popper-placement^=\"left\"] {\n @extend .bs-popover-start;\n }\n}\n\n// Offset the popover to account for the popover arrow\n.popover-header {\n padding: $popover-header-padding-y $popover-header-padding-x;\n margin-bottom: 0; // Reset the default from Reboot\n @include font-size($font-size-base);\n color: $popover-header-color;\n background-color: $popover-header-bg;\n border-bottom: $popover-border-width solid $popover-border-color;\n @include border-top-radius($popover-inner-border-radius);\n\n &:empty {\n display: none;\n }\n}\n\n.popover-body {\n padding: $popover-body-padding-y $popover-body-padding-x;\n color: $popover-body-color;\n}\n","// Notes on the classes:\n//\n// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)\n// even when their scroll action started on a carousel, but for compatibility (with Firefox)\n// we're preventing all actions instead\n// 2. The .carousel-item-start and .carousel-item-end is used to indicate where\n// the active slide is heading.\n// 3. .active.carousel-item is the current slide.\n// 4. .active.carousel-item-start and .active.carousel-item-end is the current\n// slide in its in-transition state. Only one of these occurs at a time.\n// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end\n// is the upcoming slide in transition.\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n @include clearfix();\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n @include transition($carousel-transition);\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n/* rtl:begin:ignore */\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n/* rtl:end:ignore */\n\n\n//\n// Alternate transitions\n//\n\n.carousel-fade {\n .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n }\n\n .carousel-item.active,\n .carousel-item-next.carousel-item-start,\n .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n }\n\n .active.carousel-item-start,\n .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n @include transition(opacity 0s $carousel-transition-duration);\n }\n}\n\n\n//\n// Left/right controls for nav\n//\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n // Use flex for alignment (1-3)\n display: flex; // 1. allow flex styles\n align-items: center; // 2. vertically center contents\n justify-content: center; // 3. horizontally center contents\n width: $carousel-control-width;\n padding: 0;\n color: $carousel-control-color;\n text-align: center;\n background: none;\n border: 0;\n opacity: $carousel-control-opacity;\n @include transition($carousel-control-transition);\n\n // Hover/focus state\n &:hover,\n &:focus {\n color: $carousel-control-color;\n text-decoration: none;\n outline: 0;\n opacity: $carousel-control-hover-opacity;\n }\n}\n.carousel-control-prev {\n left: 0;\n background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null);\n}\n.carousel-control-next {\n right: 0;\n background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null);\n}\n\n// Icons for within\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: $carousel-control-icon-width;\n height: $carousel-control-icon-width;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n/* rtl:options: {\n \"autoRename\": true,\n \"stringMap\":[ {\n \"name\" : \"prev-next\",\n \"search\" : \"prev\",\n \"replace\" : \"next\"\n } ]\n} */\n.carousel-control-prev-icon {\n background-image: escape-svg($carousel-control-prev-icon-bg);\n}\n.carousel-control-next-icon {\n background-image: escape-svg($carousel-control-next-icon-bg);\n}\n\n// Optional indicator pips/controls\n//\n// Add a container (such as a list) with the following class and add an item (ideally a focusable control,\n// like a button) with data-bs-target for each slide your carousel holds.\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n // Use the .carousel-control's width as margin so we don't overlay those\n margin-right: $carousel-control-width;\n margin-bottom: 1rem;\n margin-left: $carousel-control-width;\n list-style: none;\n\n [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: $carousel-indicator-width;\n height: $carousel-indicator-height;\n padding: 0;\n margin-right: $carousel-indicator-spacer;\n margin-left: $carousel-indicator-spacer;\n text-indent: -999px;\n cursor: pointer;\n background-color: $carousel-indicator-active-bg;\n background-clip: padding-box;\n border: 0;\n // Use transparent borders to increase the hit area by 10px on top and bottom.\n border-top: $carousel-indicator-hit-area-height solid transparent;\n border-bottom: $carousel-indicator-hit-area-height solid transparent;\n opacity: $carousel-indicator-opacity;\n @include transition($carousel-indicator-transition);\n }\n\n .active {\n opacity: $carousel-indicator-active-opacity;\n }\n}\n\n\n// Optional captions\n//\n//\n\n.carousel-caption {\n position: absolute;\n right: (100% - $carousel-caption-width) * .5;\n bottom: $carousel-caption-spacer;\n left: (100% - $carousel-caption-width) * .5;\n padding-top: $carousel-caption-padding-y;\n padding-bottom: $carousel-caption-padding-y;\n color: $carousel-caption-color;\n text-align: center;\n}\n\n// Dark mode carousel\n\n.carousel-dark {\n .carousel-control-prev-icon,\n .carousel-control-next-icon {\n filter: $carousel-dark-control-icon-filter;\n }\n\n .carousel-indicators [data-bs-target] {\n background-color: $carousel-dark-indicator-active-bg;\n }\n\n .carousel-caption {\n color: $carousel-dark-caption-color;\n }\n}\n","// scss-docs-start clearfix\n@mixin clearfix() {\n &::after {\n display: block;\n clear: both;\n content: \"\";\n }\n}\n// scss-docs-end clearfix\n","//\n// Rotating border\n//\n\n// scss-docs-start spinner-border-keyframes\n@keyframes spinner-border {\n to { transform: rotate(360deg) #{\"/* rtl:ignore */\"}; }\n}\n// scss-docs-end spinner-border-keyframes\n\n.spinner-border {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: $spinner-vertical-align;\n border: $spinner-border-width solid currentColor;\n border-right-color: transparent;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 50%;\n animation: $spinner-animation-speed linear infinite spinner-border;\n}\n\n.spinner-border-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n border-width: $spinner-border-width-sm;\n}\n\n//\n// Growing circle\n//\n\n// scss-docs-start spinner-grow-keyframes\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n// scss-docs-end spinner-grow-keyframes\n\n.spinner-grow {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: $spinner-vertical-align;\n background-color: currentColor;\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 50%;\n opacity: 0;\n animation: $spinner-animation-speed linear infinite spinner-grow;\n}\n\n.spinner-grow-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n}\n\n@if $enable-reduced-motion {\n @media (prefers-reduced-motion: reduce) {\n .spinner-border,\n .spinner-grow {\n animation-duration: $spinner-animation-speed * 2;\n }\n }\n}\n",".offcanvas {\n position: fixed;\n bottom: 0;\n z-index: $zindex-offcanvas;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: $offcanvas-color;\n visibility: hidden;\n background-color: $offcanvas-bg-color;\n background-clip: padding-box;\n outline: 0;\n @include box-shadow($offcanvas-box-shadow);\n @include transition(transform $offcanvas-transition-duration ease-in-out);\n}\n\n.offcanvas-backdrop {\n @include overlay-backdrop($zindex-offcanvas-backdrop, $offcanvas-backdrop-bg, $offcanvas-backdrop-opacity);\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: $offcanvas-padding-y $offcanvas-padding-x;\n\n .btn-close {\n padding: ($offcanvas-padding-y * .5) ($offcanvas-padding-x * .5);\n margin-top: $offcanvas-padding-y * -.5;\n margin-right: $offcanvas-padding-x * -.5;\n margin-bottom: $offcanvas-padding-y * -.5;\n }\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: $offcanvas-title-line-height;\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: $offcanvas-padding-y $offcanvas-padding-x;\n overflow-y: auto;\n}\n\n.offcanvas-start {\n top: 0;\n left: 0;\n width: $offcanvas-horizontal-width;\n border-right: $offcanvas-border-width solid $offcanvas-border-color;\n transform: translateX(-100%);\n}\n\n.offcanvas-end {\n top: 0;\n right: 0;\n width: $offcanvas-horizontal-width;\n border-left: $offcanvas-border-width solid $offcanvas-border-color;\n transform: translateX(100%);\n}\n\n.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: $offcanvas-vertical-height;\n max-height: 100%;\n border-bottom: $offcanvas-border-width solid $offcanvas-border-color;\n transform: translateY(-100%);\n}\n\n.offcanvas-bottom {\n right: 0;\n left: 0;\n height: $offcanvas-vertical-height;\n max-height: 100%;\n border-top: $offcanvas-border-width solid $offcanvas-border-color;\n transform: translateY(100%);\n}\n\n.offcanvas.show {\n transform: none;\n}\n",".placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentColor;\n opacity: $placeholder-opacity-max;\n\n &.btn::before {\n display: inline-block;\n content: \"\";\n }\n}\n\n// Sizing\n.placeholder-xs {\n min-height: .6em;\n}\n\n.placeholder-sm {\n min-height: .8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n// Animation\n.placeholder-glow {\n .placeholder {\n animation: placeholder-glow 2s ease-in-out infinite;\n }\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: $placeholder-opacity-min;\n }\n}\n\n.placeholder-wave {\n mask-image: linear-gradient(130deg, $black 55%, rgba(0, 0, 0, (1 - $placeholder-opacity-min)) 75%, $black 95%);\n mask-size: 200% 100%;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n 100% {\n mask-position: -200% 0%;\n }\n}\n","@each $color, $value in $theme-colors {\n .link-#{$color} {\n color: $value;\n\n @if $link-shade-percentage != 0 {\n &:hover,\n &:focus {\n color: if(color-contrast($value) == $color-contrast-light, shade-color($value, $link-shade-percentage), tint-color($value, $link-shade-percentage));\n }\n }\n }\n}\n","// Credit: Nicolas Gallagher and SUIT CSS.\n\n.ratio {\n position: relative;\n width: 100%;\n\n &::before {\n display: block;\n padding-top: var(--#{$variable-prefix}aspect-ratio);\n content: \"\";\n }\n\n > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n }\n}\n\n@each $key, $ratio in $aspect-ratios {\n .ratio-#{$key} {\n --#{$variable-prefix}aspect-ratio: #{$ratio};\n }\n}\n","// Shorthand\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n// Responsive sticky top\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .sticky#{$infix}-top {\n position: sticky;\n top: 0;\n z-index: $zindex-sticky;\n }\n }\n}\n","// scss-docs-start stacks\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n// scss-docs-end stacks\n","//\n// Visually hidden\n//\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n @include visually-hidden();\n}\n","// stylelint-disable declaration-no-important\n\n// Hide content visually while keeping it accessible to assistive technologies\n//\n// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/\n// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/\n\n@mixin visually-hidden() {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important; // Fix for https://github.com/twbs/bootstrap/issues/25686\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n// Use to only display content when it's focused, or one of its child elements is focused\n// (i.e. when focus is within the element/container that the class was applied to)\n//\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n\n@mixin visually-hidden-focusable() {\n &:not(:focus):not(:focus-within) {\n @include visually-hidden();\n }\n}\n","//\n// Stretched link\n//\n\n.stretched-link {\n &::#{$stretched-link-pseudo-element} {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: $stretched-link-z-index;\n content: \"\";\n }\n}\n","//\n// Text truncation\n//\n\n.text-truncate {\n @include text-truncate();\n}\n","// Text truncate\n// Requires inline-block or block for proper styling\n\n@mixin text-truncate() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n",".vr {\n display: inline-block;\n align-self: stretch;\n width: 1px;\n min-height: 1em;\n background-color: currentColor;\n opacity: $hr-opacity;\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix, $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (eg. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$variable-prefix}#{$property-class}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $value in $is-local-vars {\n --#{$variable-prefix}#{$local-var}: #{$value};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/favicon.png b/Utilities/LibMatrix.E2eeTestKit/wwwroot/favicon.png new file mode 100644
index 0000000..8422b59 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/favicon.png
Binary files differdiff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-192.png b/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-192.png new file mode 100644
index 0000000..166f56d --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-192.png
Binary files differdiff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-512.png b/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-512.png new file mode 100644
index 0000000..c2dd484 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/icon-512.png
Binary files differdiff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/index.html b/Utilities/LibMatrix.E2eeTestKit/wwwroot/index.html new file mode 100644
index 0000000..df84658 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/index.html
@@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>LibMatrix.E2eeTestKit</title> + <base href="/" /> + <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> + <link rel="stylesheet" href="css/app.css" /> + <link rel="icon" type="image/png" href="favicon.png" /> + <link href="LibMatrix.E2eeTestKit.styles.css" rel="stylesheet" /> + <link href="manifest.webmanifest" rel="manifest" /> + <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> + <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" /> +</head> + +<body> + <div id="app"> + <svg class="loading-progress"> + <circle r="40%" cx="50%" cy="50%" /> + <circle r="40%" cx="50%" cy="50%" /> + </svg> + <div class="loading-progress-text"></div> + </div> + + <div id="blazor-error-ui"> + An unhandled error has occurred. + <a href="" class="reload">Reload</a> + <a class="dismiss">🗙</a> + </div> + <script src="_framework/blazor.webassembly.js"></script> + <script>navigator.serviceWorker.register('service-worker.js');</script> +</body> + +</html> diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/manifest.webmanifest b/Utilities/LibMatrix.E2eeTestKit/wwwroot/manifest.webmanifest new file mode 100644
index 0000000..1a55bfe --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/manifest.webmanifest
@@ -0,0 +1,22 @@ +{ + "name": "LibMatrix.E2eeTestKit", + "short_name": "LibMatrix.E2eeTestKit", + "id": "./", + "start_url": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#03173d", + "prefer_related_applications": false, + "icons": [ + { + "src": "icon-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "icon-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.js b/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.js new file mode 100644
index 0000000..c6d0085 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.js
@@ -0,0 +1,4 @@ +// In development, always fetch from the network and do not enable offline support. +// This is because caching would make development more difficult (changes would not +// be reflected on the first load after each change). +self.addEventListener('fetch', () => { }); diff --git a/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.published.js b/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.published.js new file mode 100644
index 0000000..003e3e7 --- /dev/null +++ b/Utilities/LibMatrix.E2eeTestKit/wwwroot/service-worker.published.js
@@ -0,0 +1,55 @@ +// Caution! Be sure you understand the caveats before publishing an application with +// offline support. See https://aka.ms/blazor-offline-considerations + +self.importScripts('./service-worker-assets.js'); +self.addEventListener('install', event => event.waitUntil(onInstall(event))); +self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +const cacheNamePrefix = 'offline-cache-'; +const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; +const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. +const base = "/"; +const baseUrl = new URL(base, self.origin); +const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href); + +async function onInstall(event) { + console.info('Service worker: Install'); + + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) + .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) + .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +} + +async function onActivate(event) { + console.info('Service worker: Activate'); + + // Delete unused caches + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys + .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) + .map(key => caches.delete(key))); +} + +async function onFetch(event) { + let cachedResponse = null; + if (event.request.method === 'GET') { + // For all navigation requests, try to serve index.html from cache, + // unless that request is for an offline resource. + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = event.request.mode === 'navigate' + && !manifestUrlList.some(url => url === event.request.url); + + const request = shouldServeIndexHtml ? 'index.html' : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } + + return cachedResponse || fetch(event.request); +} diff --git a/Utilities/LibMatrix.FederationTest/.gitignore b/Utilities/LibMatrix.FederationTest/.gitignore new file mode 100644
index 0000000..4a96773 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/.gitignore
@@ -0,0 +1 @@ +.keys/ diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs b/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs new file mode 100644
index 0000000..33d0b99 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs
@@ -0,0 +1,41 @@ +using LibMatrix.Federation.Extensions; +using LibMatrix.Federation.Utilities; +using LibMatrix.FederationTest.Services; +using LibMatrix.Homeservers; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers; + +[ApiController] +[Route("_matrix/key/v2/")] +public class FederationKeysController(FederationTestConfiguration config, FederationKeyStore keyStore) { + static FederationKeysController() { + Console.WriteLine("INFO | FederationKeysController initialized."); + } + + private static SignedObject<ServerKeysResponse>? _cachedServerKeysResponse; + private static SemaphoreSlim _serverKeyCacheLock = new SemaphoreSlim(1, 1); + + [HttpGet("server")] + public async Task<SignedObject<ServerKeysResponse>> GetServerKeys() { + await _serverKeyCacheLock.WaitAsync(); + if (_cachedServerKeysResponse == null || _cachedServerKeysResponse.TypedContent.ValidUntil < DateTime.Now + TimeSpan.FromSeconds(30)) { + var keys = keyStore.GetCurrentSigningKey(); + _cachedServerKeysResponse = new ServerKeysResponse() { + ValidUntil = DateTime.Now + TimeSpan.FromMinutes(1), + ServerName = config.ServerName, + OldVerifyKeys = [], + VerifyKeysById = new() { + { + new() { Algorithm = "ed25519", KeyId = "0" }, new ServerKeysResponse.CurrentVerifyKey() { + Key = keys.publicKey.ToUnpaddedBase64(), + } + } + } + }.Sign(config.ServerName, new ServerKeysResponse.VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey); + } + _serverKeyCacheLock.Release(); + + return _cachedServerKeysResponse; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs b/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs new file mode 100644
index 0000000..2c3aaa3 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs
@@ -0,0 +1,18 @@ +using LibMatrix.Homeservers; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers; + +[ApiController] +[Route("_matrix/federation/v1/")] +public class FederationVersionController : ControllerBase { + [HttpGet("version")] + public ServerVersionResponse GetVersion() { + return new ServerVersionResponse { + Server = new() { + Name = "LibMatrix.Federation", + Version = "0.0.0", + } + }; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs new file mode 100644
index 0000000..4a6bc87 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
@@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using LibMatrix.Extensions; +using LibMatrix.Federation; +using LibMatrix.Federation.Utilities; +using LibMatrix.FederationTest.Services; +using LibMatrix.Homeservers; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers; + +[ApiController] +public class TestController(FederationTestConfiguration config, FederationKeyStore keyStore) : ControllerBase { + static TestController() { + Console.WriteLine("INFO | TestController initialized."); + } + + [HttpGet("/test")] + public async Task<JsonObject> GetTest() { + var hc = new MatrixHttpClient() { + BaseAddress = new Uri("https://matrix.rory.gay") + }; + + var keyId = new ServerKeysResponse.VersionedKeyId() { + Algorithm = "ed25519", + KeyId = "0" + }; + + var signatureData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() { + Method = "GET", + Uri = "/_matrix/federation/v1/user/devices/@emma:rory.gay", + OriginServerName = config.ServerName, + DestinationServerName = "rory.gay" + } + .Sign(config.ServerName, keyId, keyStore.GetCurrentSigningKey().privateKey); + + var signature = signatureData.Signatures[config.ServerName][keyId]; + var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() { + Origin = config.ServerName, + Destination = "rory.gay", + Key = keyId, + Signature = signature + }.ToHeaderValue(); + + var req = new HttpRequestMessage(HttpMethod.Get, "/_matrix/federation/v1/user/devices/@emma:rory.gay"); + req.Headers.Add("Authorization", headerValue); + + var response = await hc.SendAsync(req); + var content = await response.Content.ReadFromJsonAsync<JsonObject>(); + return content!; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs b/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs new file mode 100644
index 0000000..28fca8d --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs
@@ -0,0 +1,19 @@ +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.FederationTest.Controllers; + +[ApiController] +[Route(".well-known/")] +public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase { + static WellKnownController() { + Console.WriteLine("INFO | WellKnownController initialized."); + } + [HttpGet("matrix/server")] + public ServerWellKnown GetMatrixServerWellKnown() { + // {Request.Headers["X-Forwarded-Proto"].FirstOrDefault(Request.Scheme)}:// + return new() { + Homeserver = $"{Request.Headers["X-Forwarded-Host"].FirstOrDefault(Request.Host.Host)}:{Request.Headers["X-Forwarded-Port"].FirstOrDefault("443")}", + }; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj new file mode 100644
index 0000000..c54ba8d --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
@@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\LibMatrix.Federation\LibMatrix.Federation.csproj" /> + </ItemGroup> + +</Project> diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs new file mode 100644
index 0000000..adc809f --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -0,0 +1,34 @@ +using LibMatrix.FederationTest.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); +builder.Services.AddHttpLogging(options => { + options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; + options.RequestHeaders.Add("X-Forwarded-Proto"); + options.RequestHeaders.Add("X-Forwarded-Host"); + options.RequestHeaders.Add("X-Forwarded-Port"); +}); + +builder.Services.AddSingleton<FederationTestConfiguration>(); +builder.Services.AddSingleton<FederationKeyStore>(); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (true || app.Environment.IsDevelopment()) { + app.MapOpenApi(); +} + +app.UseAuthorization(); + +app.MapControllers(); +// app.UseHttpLogging(); + + +app.Run(); \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs new file mode 100644
index 0000000..f24d14e --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -0,0 +1,31 @@ +using LibMatrix.FederationTest.Utilities; +using Org.BouncyCastle.Crypto.Parameters; + +namespace LibMatrix.FederationTest.Services; + +public class FederationKeyStore(FederationTestConfiguration config) { + static FederationKeyStore() { + Console.WriteLine("INFO | FederationKeyStore initialized."); + } + + private static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) currentKeyPair = default; + public (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GetCurrentSigningKey() { + if (currentKeyPair != default) { + return currentKeyPair; + } + + if(!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath); + + var privateKeyPath = Path.Combine(config.KeyStorePath, "signing.key"); + if (!File.Exists(privateKeyPath)) { + var keyPair = Ed25519Utils.GenerateKeyPair(); + File.WriteAllBytes(privateKeyPath, keyPair.privateKey.GetEncoded()); + return keyPair; + } + + var privateKeyBytes = File.ReadAllBytes(privateKeyPath); + var privateKey = Ed25519Utils.LoadPrivateKeyFromEncoded(privateKeyBytes); + var publicKey = privateKey.GeneratePublicKey(); + return currentKeyPair = (privateKey, publicKey); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs new file mode 100644
index 0000000..353ddf5 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
@@ -0,0 +1,10 @@ +namespace LibMatrix.FederationTest.Services; + +public class FederationTestConfiguration { + public FederationTestConfiguration(IConfiguration configurationSection) { + configurationSection.GetRequiredSection("FederationTest").Bind(this); + } + + public string ServerName { get; set; } = "localhost"; + public string KeyStorePath { get; set; } = "./.keys"; +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs new file mode 100644
index 0000000..bb57d51 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
@@ -0,0 +1,28 @@ +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace LibMatrix.FederationTest.Utilities; + +public class Ed25519Utils { + public static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GenerateKeyPair() { + Console.WriteLine("Generating new Ed25519 key pair!"); + var keyPairGen = new Ed25519KeyPairGenerator(); + keyPairGen.Init(new Ed25519KeyGenerationParameters(new SecureRandom())); + var keyPair = keyPairGen.GenerateKeyPair(); + + var privateKey = (Ed25519PrivateKeyParameters)keyPair.Private; + var publicKey = (Ed25519PublicKeyParameters)keyPair.Public; + + return (privateKey, publicKey); + } + + public static Ed25519PublicKeyParameters LoadPublicKeyFromEncoded(string key) { + var keyBytes = Convert.FromBase64String(key); + return new Ed25519PublicKeyParameters(keyBytes, 0); + } + + public static Ed25519PrivateKeyParameters LoadPrivateKeyFromEncoded(byte[] key) { + return new Ed25519PrivateKeyParameters(key, 0); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.FederationTest/appsettings.Development.json b/Utilities/LibMatrix.FederationTest/appsettings.Development.json new file mode 100644
index 0000000..b6c6151 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/appsettings.Development.json
@@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.AspNetCore.Routing": "Warning" + } + }, + "FederationTest": { + "ServerName": "libmatrix-fed-test.rory.gay", + "KeyStorePath": "./.keys" + } +} diff --git a/Utilities/LibMatrix.FederationTest/appsettings.json b/Utilities/LibMatrix.FederationTest/appsettings.json new file mode 100644
index 0000000..10f68b8 --- /dev/null +++ b/Utilities/LibMatrix.FederationTest/appsettings.json
@@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tests/LibMatrix.HomeserverEmulator/.gitignore b/Utilities/LibMatrix.HomeserverEmulator/.gitignore
index 8fce603..8fce603 100644 --- a/Tests/LibMatrix.HomeserverEmulator/.gitignore +++ b/Utilities/LibMatrix.HomeserverEmulator/.gitignore
diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs new file mode 100644
index 0000000..d0eaed4 --- /dev/null +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/AuthController.cs
@@ -0,0 +1,138 @@ +using System.Text.Json.Nodes; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/client/{version}/")] +public class AuthController(ILogger<AuthController> logger, UserStore userStore, TokenService tokenService, HseConfiguration config) : ControllerBase { + [HttpPost("login")] + public async Task<LoginResponse> Login(LoginRequest request) { + if (!request.Identifier.User.StartsWith('@')) + request.Identifier.User = $"@{request.Identifier.User}:{tokenService.GenerateServerName(HttpContext)}"; + // if (request.Identifier.User.EndsWith("localhost")) + // request.Identifier.User = request.Identifier.User.Replace("localhost", tokenService.GenerateServerName(HttpContext)); + + var user = await userStore.GetUserById(request.Identifier.User) ?? await userStore.CreateUser(request.Identifier.User); + + return user.Login(); + } + + [HttpGet("login")] + public async Task<LoginFlowsResponse> GetLoginFlows() { + return new LoginFlowsResponse { + Flows = ((string[]) [ + "m.login.password", + "m.login.recaptcha", + "m.login.sso", + "m.login.email.identity", + "m.login.msisdn", + "m.login.dummy", + "m.login.registration_token", + ]).Select(x => new LoginFlowsResponse.LoginFlow { Type = x }).ToList() + }; + } + + [HttpPost("logout")] + public async Task<object> Logout() { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + if (user == null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "No such user" + }; + + if (!user.AccessTokens.ContainsKey(token)) + throw new MatrixException() { + ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND, + Error = "Token not found" + }; + + user.AccessTokens.Remove(token); + return new { }; + } + + [HttpPost("register")] + public async Task<object> Register(JsonObject request, [FromQuery] string kind = "user") { + if (kind == "guest") { + var user = await userStore.CreateUser(Random.Shared.NextInt64(long.MaxValue).ToString(), kind: "guest"); + return user.Login(); + } + + if (request.Count == 0) { + return new { + session = Guid.NewGuid().ToString(), + flows = new { + stages = new[] { + "m.login.dummy", + } + } + }; + } + + if (request.ContainsKey("password")) { + var parts = request["username"].ToString().Split(':'); + var localpart = parts[0].TrimStart('@'); + var user = await userStore.CreateUser($"@{localpart}:{config.ServerName}"); + var login = user.Login(); + + if (request.ContainsKey("initial_device_display_name")) + user.AccessTokens[login.AccessToken].DeviceName = request["initial_device_display_name"]!.ToString(); + + return login; + } + + return new { }; + } + + [HttpGet("register/available")] + public async Task<object> IsUsernameAvailable([FromQuery] string username) { + return new { + available = await userStore.GetUserById($"@{username}:{config.ServerName}") is null + }; + } + + // [HttpPost("account/deactivate")] + // public async Task<object> DeactivateAccount() { + // var token = tokenService.GetAccessToken(HttpContext); + // var user = await userStore.GetUserByToken(token); + // if (user == null) + // throw new MatrixException() { + // ErrorCode = "M_UNKNOWN_TOKEN", + // Error = "No such user" + // }; + // + // + // return new { }; + // } + + #region 3PID + + [HttpGet("account/3pid")] + public async Task<object> Get3pid() { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + if (user == null) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN_TOKEN", + Error = "No such user" + }; + + return new { + threepids = (object[])[] + }; + } + + #endregion +} + +public class LoginFlowsResponse { + public required List<LoginFlow> Flows { get; set; } + + public class LoginFlow { + public required string Type { get; set; } + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs new file mode 100644
index 0000000..d497ca6 --- /dev/null +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/DirectoryController.cs
@@ -0,0 +1,154 @@ +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_matrix/")] +public class DirectoryController(ILogger<DirectoryController> logger, RoomStore roomStore) : ControllerBase { +#region Room directory + + [HttpGet("client/v3/directory/room/{alias}")] + public async Task<AliasResult> GetRoomAliasV3(string alias) { + var match = roomStore._rooms.FirstOrDefault(x => + x.State.Any(y => y.Type == RoomCanonicalAliasEventContent.EventId && y.StateKey == "" && y.RawContent?["alias"]?.ToString() == alias)); + + if (match == null) + throw new MatrixException() { + ErrorCode = "M_NOT_FOUND", + Error = "Room not found" + }; + + var servers = match.State.Where(x => x.Type == RoomMemberEventContent.EventId && x.RawContent?["membership"]?.ToString() == "join") + .Select(x => x.StateKey!.Split(':', 2)[1]).ToList(); + + return new() { + RoomId = match.RoomId, + Servers = servers + }; + } + + [HttpGet("client/v3/publicRooms")] + public async Task<PublicRoomDirectoryResult> GetPublicRooms(int limit = 100, string? server = null, string? since = null) { + var rooms = roomStore._rooms.OrderByDescending(x => x.JoinedMembers.Count).AsEnumerable(); + + if (since != null) { + rooms = rooms.SkipWhile(x => x.RoomId != since).Skip(1); + } + + if (server != null) { + rooms = rooms.Where(x => x.State.Any(y => y.Type == RoomMemberEventContent.EventId && y.StateKey!.EndsWith(server))); + } + + var count = rooms.Count(); + rooms = rooms.Take(limit); + + return new PublicRoomDirectoryResult() { + Chunk = rooms.Select(x => new PublicRoomDirectoryResult.PublicRoomListItem() { + RoomId = x.RoomId, + Name = x.State.FirstOrDefault(y => y.Type == RoomNameEventContent.EventId)?.RawContent?["name"]?.ToString(), + Topic = x.State.FirstOrDefault(y => y.Type == RoomTopicEventContent.EventId)?.RawContent?["topic"]?.ToString(), + AvatarUrl = x.State.FirstOrDefault(y => y.Type == RoomAvatarEventContent.EventId)?.RawContent?["url"]?.ToString(), + GuestCanJoin = x.State.Any(y => y.Type == RoomGuestAccessEventContent.EventId && y.RawContent?["guest_access"]?.ToString() == "can_join"), + NumJoinedMembers = x.JoinedMembers.Count, + WorldReadable = x.State.Any(y => y.Type == RoomHistoryVisibilityEventContent.EventId && y.RawContent?["history_visibility"]?.ToString() == "world_readable"), + JoinRule = x.State.FirstOrDefault(y => y.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.ToString(), + CanonicalAlias = x.State.FirstOrDefault(y => y.Type == RoomCanonicalAliasEventContent.EventId)?.RawContent?["alias"]?.ToString() + }).ToList(), + NextBatch = count > limit ? rooms.Last().RoomId : null, + TotalRoomCountEstimate = count + }; + } + + [HttpPost("client/v3/publicRooms")] + public async Task<PublicRoomDirectoryResult> GetFilteredPublicRooms([FromBody] PublicRoomDirectoryRequest request, [FromQuery] string? server = null) { + var rooms = roomStore._rooms.OrderByDescending(x => x.JoinedMembers.Count).AsEnumerable(); + + if (request.Since != null) { + rooms = rooms.SkipWhile(x => x.RoomId != request.Since).Skip(1); + } + + if (server != null) { + rooms = rooms.Where(x => x.State.Any(y => y.Type == RoomMemberEventContent.EventId && y.StateKey!.EndsWith(server))); + } + + var count = rooms.Count(); + rooms = rooms.Take(request.Limit ?? 100); + + return new PublicRoomDirectoryResult() { + Chunk = rooms.Select(x => new PublicRoomDirectoryResult.PublicRoomListItem() { + RoomId = x.RoomId, + Name = x.State.FirstOrDefault(y => y.Type == RoomNameEventContent.EventId)?.RawContent?["name"]?.ToString(), + Topic = x.State.FirstOrDefault(y => y.Type == RoomTopicEventContent.EventId)?.RawContent?["topic"]?.ToString(), + AvatarUrl = x.State.FirstOrDefault(y => y.Type == RoomAvatarEventContent.EventId)?.RawContent?["url"]?.ToString(), + GuestCanJoin = x.State.Any(y => y.Type == RoomGuestAccessEventContent.EventId && y.RawContent?["guest_access"]?.ToString() == "can_join"), + NumJoinedMembers = x.JoinedMembers.Count, + WorldReadable = x.State.Any(y => y.Type == RoomHistoryVisibilityEventContent.EventId && y.RawContent?["history_visibility"]?.ToString() == "world_readable"), + JoinRule = x.State.FirstOrDefault(y => y.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.ToString(), + CanonicalAlias = x.State.FirstOrDefault(y => y.Type == RoomCanonicalAliasEventContent.EventId)?.RawContent?["alias"]?.ToString() + }).ToList(), + NextBatch = count > request.Limit ? rooms.Last().RoomId : null, + TotalRoomCountEstimate = count + }; + } + +#endregion + +#region User directory + + [HttpPost("client/v3/user_directory/search")] + public async Task<UserDirectoryResponse> SearchUserDirectory([FromBody] UserDirectoryRequest request) { + var users = roomStore._rooms + .SelectMany(x => x.State.Where(y => + y.Type == RoomMemberEventContent.EventId + && y.RawContent?["membership"]?.ToString() == "join" + && (y.StateKey!.ContainsAnyCase(request.SearchTerm) || y.RawContent?["displayname"]?.ToString()?.ContainsAnyCase(request.SearchTerm) == true) + ) + ) + .DistinctBy(x => x.StateKey) + .ToList(); + + request.Limit ??= 10; + + return new() { + Results = users.Select(x => new UserDirectoryResponse.UserDirectoryResult { + UserId = x.StateKey!, + DisplayName = x.RawContent?["displayname"]?.ToString(), + AvatarUrl = x.RawContent?["avatar_url"]?.ToString() + }).ToList(), + Limited = users.Count > request.Limit + }; + } + +#endregion +} + +public class PublicRoomDirectoryRequest { + [JsonPropertyName("filter")] + public PublicRoomDirectoryFilter Filter { get; set; } + + [JsonPropertyName("include_all_networks")] + public bool IncludeAllNetworks { get; set; } + + [JsonPropertyName("limit")] + public int? Limit { get; set; } + + [JsonPropertyName("since")] + public string? Since { get; set; } + + [JsonPropertyName("third_party_instance_id")] + public string? ThirdPartyInstanceId { get; set; } + + public class PublicRoomDirectoryFilter { + [JsonPropertyName("generic_search_term")] + public string? GenericSearchTerm { get; set; } + + [JsonPropertyName("room_types")] + public List<string>? RoomTypes { get; set; } + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs new file mode 100644
index 0000000..1fb3251 --- /dev/null +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEAdmin/HEAdminController.cs
@@ -0,0 +1,18 @@ +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_hse/admin")] +public class HEAdminController(ILogger<HEAdminController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("users")] + public async Task<List<UserStore.User>> GetUsers() { + return userStore._users.ToList(); + } + + [HttpGet("rooms")] + public async Task<List<RoomStore.Room>> GetRooms() { + return roomStore._rooms.ToList(); + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs new file mode 100644
index 0000000..85e4ddb --- /dev/null +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEClient/HEClientController.cs
@@ -0,0 +1,34 @@ +using ArcaneLibs.Collections; +using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_hse/client/v1/external_profiles")] +public class HEClientController(ILogger<HEClientController> logger, UserStore userStore, TokenService tokenService) : ControllerBase { + [HttpGet] + public async Task<ObservableDictionary<string, LoginResponse>> GetExternalProfiles() { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + + return user.AuthorizedSessions; + } + + [HttpPut("{name}")] + public async Task PutExternalProfile(string name, [FromBody] LoginResponse sessionData) { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + + user.AuthorizedSessions[name] = sessionData; + } + + [HttpDelete("{name}")] + public async Task DeleteExternalProfile(string name) { + var token = tokenService.GetAccessToken(HttpContext); + var user = await userStore.GetUserByToken(token); + + user.AuthorizedSessions.Remove(name); + } +} \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
index 9e0c17c..ce47245 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/HEDebug/HEDebugController.cs
@@ -1,18 +1,18 @@ -using LibMatrix.HomeserverEmulator.Services; -using Microsoft.AspNetCore.Mvc; - -namespace LibMatrix.HomeserverEmulator.Controllers; - -[ApiController] -[Route("/_hsEmulator")] -public class HEDebugController(ILogger<HEDebugController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase { - [HttpGet("users")] - public async Task<List<UserStore.User>> GetUsers() { - return userStore._users.ToList(); - } - - [HttpGet("rooms")] - public async Task<List<RoomStore.Room>> GetRooms() { - return roomStore._rooms.ToList(); - } +using LibMatrix.HomeserverEmulator.Services; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/_hsEmulator")] +public class HEDebugController(ILogger<HEDebugController> logger, UserStore userStore, RoomStore roomStore) : ControllerBase { + [HttpGet("users")] + public async Task<List<UserStore.User>> GetUsers() { + return userStore._users.ToList(); + } + + [HttpGet("rooms")] + public async Task<List<RoomStore.Room>> GetRooms() { + return roomStore._rooms.ToList(); + } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
index 7898a8c..18cccf6 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/KeysController.cs
@@ -11,6 +11,25 @@ // [ApiController] // [Route("/_matrix/client/{version}/")] // public class KeysController(ILogger<KeysController> logger, TokenService tokenService, UserStore userStore) : ControllerBase { +// [HttpPost("keys/upload")] +// public async Task<object> UploadKeys(DeviceKeysUploadRequest request) { +// var token = tokenService.GetAccessToken(HttpContext); +// if (token == null) +// throw new MatrixException() { +// ErrorCode = "M_MISSING_TOKEN", +// Error = "Missing token" +// }; +// +// var user = await userStore.GetUserByToken(token); +// if (user == null) +// throw new MatrixException() { +// ErrorCode = "M_UNKNOWN_TOKEN", +// Error = "No such user" +// }; +// +// return new { }; +// } +// // [HttpGet("room_keys/version")] // public async Task<RoomKeysResponse> GetRoomKeys() { // var token = tokenService.GetAccessToken(HttpContext); diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs
index 1fb427e..245770e 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/LegacyController.cs
@@ -1,12 +1,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Text.Json.Nodes; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; -using LibMatrix.Services; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
index 7899ada..6048bbb 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Media/MediaController.cs
@@ -1,9 +1,9 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using ArcaneLibs.Collections; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Services; using Microsoft.AspNetCore.Mvc; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace LibMatrix.HomeserverEmulator.Controllers.Media; @@ -13,12 +13,12 @@ public class MediaController( ILogger<MediaController> logger, TokenService tokenService, UserStore userStore, - HSEConfiguration cfg, + HseConfiguration cfg, HomeserverResolverService hsResolver, MediaStore mediaStore) : ControllerBase { [HttpPost("upload")] - public async Task<object> UploadMedia([FromHeader(Name = "Content-Type")] string ContentType, [FromQuery] string filename, [FromBody] Stream file) { + public async Task<object> UploadMedia([FromHeader(Name = "Content-Type")] string contentType, [FromQuery] string filename, [FromBody] Stream file) { var token = tokenService.GetAccessTokenOrNull(HttpContext); if (token == null) throw new MatrixException() { @@ -42,6 +42,9 @@ public class MediaController( [HttpGet("download/{serverName}/{mediaId}")] public async Task DownloadMedia(string serverName, string mediaId) { + Response.Headers["Access-Control-Allow-Origin"] = "*"; + Response.Headers["Access-Control-Allow-Methods"] = "GET"; + var stream = await DownloadRemoteMedia(serverName, mediaId); await stream.CopyToAsync(Response.Body); } @@ -56,6 +59,7 @@ public class MediaController( JsonObject data = new(); using var hc = new HttpClient(); + logger.LogInformation("Getting URL preview for {}", url); using var response = await hc.GetAsync(url); var doc = await response.Content.ReadAsStringAsync(); var match = Regex.Match(doc, "<meta property=\"(.*?)\" content=\"(.*?)\""); @@ -72,7 +76,9 @@ public class MediaController( if (cfg.StoreData) { var path = Path.Combine(cfg.DataStoragePath, "media", serverName, mediaId); if (!System.IO.File.Exists(path)) { - var mediaUrl = await hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + // var mediaUrl = await hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + var homeserver = (await hsResolver.ResolveHomeserverFromWellKnown(serverName)).Client; + var mediaUrl = homeserver is null ? null : $"{homeserver}/_matrix/media/v3/download/"; if (mediaUrl is null) throw new MatrixException() { ErrorCode = "M_NOT_FOUND", @@ -80,13 +86,16 @@ public class MediaController( }; using var client = new HttpClient(); var stream = await client.GetStreamAsync(mediaUrl); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); await using var fs = System.IO.File.Create(path); await stream.CopyToAsync(fs); } return new FileStream(path, FileMode.Open); } else { - var mediaUrl = await hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + // var mediaUrl = await hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + var homeserver = (await hsResolver.ResolveHomeserverFromWellKnown(serverName)).Client; + var mediaUrl = homeserver is null ? null : $"{homeserver}/_matrix/media/v3/download/"; if (mediaUrl is null) throw new MatrixException() { ErrorCode = "M_NOT_FOUND", diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs
index bac803f..bac803f 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomAccountDataController.cs
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs
index 7d735f7..6c57cc4 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomMembersController.cs
@@ -1,4 +1,4 @@ -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Services; using Microsoft.AspNetCore.Mvc; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
index 74c70a3..485c028 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomStateController.cs
@@ -2,6 +2,7 @@ using System.Collections.Frozen; using System.Text.Json.Nodes; using LibMatrix.HomeserverEmulator.Extensions; using LibMatrix.HomeserverEmulator.Services; +using LibMatrix.Responses; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers.Rooms; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
index afd69d1..61195b8 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomTimelineController.cs
@@ -1,9 +1,10 @@ -using System.Collections.Immutable; using System.Diagnostics; using System.Text.Json.Nodes; using ArcaneLibs; +using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.EventTypes.Spec.State.Space; using LibMatrix.Helpers; using LibMatrix.HomeserverEmulator.Extensions; using LibMatrix.HomeserverEmulator.Services; @@ -20,6 +21,7 @@ public class RoomTimelineController( TokenService tokenService, UserStore userStore, RoomStore roomStore, + HseConfiguration hseConfig, HomeserverProviderService hsProvider) : ControllerBase { [HttpPut("send/{eventType}/{txnId}")] public async Task<EventIdResponse> SendMessage(string roomId, string eventType, string txnId, [FromBody] JsonObject content) { @@ -33,21 +35,21 @@ public class RoomTimelineController( Error = "Room not found" }; + var evt = new StateEvent() { + RawContent = content, + Type = eventType + }.ToStateEvent(user, room); + + if (evt.Type == RoomMessageEventContent.EventId && (evt.TypedContent as RoomMessageEventContent).Body.StartsWith("!hse")) + _ = Task.Run(() => HandleHseCommand(evt, room, user)); + if (!room.JoinedMembers.Any(x => x.StateKey == user.UserId)) throw new MatrixException() { ErrorCode = "M_FORBIDDEN", Error = "User is not in the room" }; - var evt = new StateEvent() { - RawContent = content, - Type = eventType - }.ToStateEvent(user, room); - room.Timeline.Add(evt); - if (evt.Type == RoomMessageEventContent.EventId && (evt.TypedContent as RoomMessageEventContent).Body.StartsWith("!hse")) - await HandleHseCommand(evt, room, user); - // else return new() { EventId = evt.EventId @@ -124,9 +126,10 @@ public class RoomTimelineController( return evt; } - + [HttpGet("relations/{eventId}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, [FromQuery] string? dir = "b", [FromQuery] string? from = null, + [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -156,9 +159,10 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + [HttpGet("relations/{eventId}/{relationType}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, [FromQuery] string? dir = "b", + [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -188,9 +192,10 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + [HttpGet("relations/{eventId}/{relationType}/{eventType}")] - public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b", [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { + public async Task<RecursedBatchedChunkedStateEventResponse> GetRelations(string roomId, string eventId, string relationType, string eventType, [FromQuery] string? dir = "b", + [FromQuery] string? from = null, [FromQuery] int? limit = 100, [FromQuery] bool? recurse = false, [FromQuery] string? to = null) { var token = tokenService.GetAccessToken(HttpContext); var user = await userStore.GetUserByToken(token); @@ -220,7 +225,7 @@ public class RoomTimelineController( Chunk = matchingEvents.ToList() }; } - + private async Task<IEnumerable<StateEventResponse>> GetRelationsInternal(string roomId, string eventId, string dir, string? from, int? limit, bool? recurse, string? to) { var room = roomStore.GetRoomById(roomId); var evt = room.Timeline.SingleOrDefault(x => x.EventId == eventId); @@ -237,7 +242,7 @@ public class RoomTimelineController( else if (dir == "f") { relatedEvents = relatedEvents.Take(limit ?? 100); } - + return relatedEvents; } @@ -252,7 +257,8 @@ public class RoomTimelineController( room.Timeline.Add(new StateEventResponse() { Type = RoomMessageEventContent.EventId, TypedContent = content, - Sender = $"@hse:{tokenService.GenerateServerName(HttpContext)}", + // Sender = $"@hse:{tokenService.GenerateServerName(HttpContext)}", + Sender = $"@hse:{hseConfig.ServerName}", RoomId = room.RoomId, EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100)), OriginServerTs = DateTimeOffset.Now.ToUnixTimeMilliseconds() @@ -260,12 +266,14 @@ public class RoomTimelineController( } private async Task HandleHseCommand(StateEventResponse evt, RoomStore.Room room, UserStore.User user) { + logger.LogWarning("Handling HSE command for {0}: {1}", user.UserId, evt.RawContent.ToJson(false, true)); try { var msgContent = evt.TypedContent as RoomMessageEventContent; var parts = msgContent.Body.Split('\n')[0].Split(" "); if (parts.Length < 2) return; var command = parts[1]; + Console.WriteLine($"Handling command {command}"); switch (command) { case "import": await HandleImportCommand(parts[2..], evt, room, user); @@ -293,7 +301,7 @@ public class RoomTimelineController( InternalSendMessage(room, url + "&i=" + i); if (i % 5000 == 0 || i == 9999) { - Thread.Sleep(5000); + // Thread.Sleep(1000); do { InternalSendMessage(room, @@ -306,6 +314,7 @@ public class RoomTimelineController( } while (Process.GetCurrentProcess().WorkingSet64 >= 1_024_000_000); } } + break; } case "genrooms": { @@ -313,7 +322,7 @@ public class RoomTimelineController( var count = 1000; for (int i = 0; i < count; i++) { var crq = new CreateRoomRequest() { - Name = "Test room", + Name = $"Test room {i}", CreationContent = new() { ["version"] = "11" }, @@ -334,9 +343,11 @@ public class RoomTimelineController( }.ToStateEvent(user, room)); } } + var newRoom = roomStore.CreateRoom(crq); newRoom.AddUser(user.UserId); } + InternalSendMessage(room, $"Generated {count} new rooms in {sw.Elapsed}!"); break; } @@ -348,6 +359,21 @@ public class RoomTimelineController( InternalSendMessage(room, $"GC memory: {Util.BytesToString(GC.GetTotalMemory(false))}, total process memory: {Util.BytesToString(Process.GetCurrentProcess().WorkingSet64)}"); break; + case "leave-all-rooms": { + var rooms = roomStore.GetRoomsByMember(user.UserId); + foreach (var memberEvt in rooms) { + var roomObj = roomStore.GetRoomById(memberEvt.RoomId); + roomObj.SetStateInternal(new() { + Type = RoomMemberEventContent.EventId, + StateKey = user.UserId, + TypedContent = new RoomMemberEventContent() { + Membership = "leave" + }, + }, senderId: user.UserId); + } + + break; + } default: InternalSendMessage(room, $"Command {command} not found!"); break; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs
index c24e6e9..9dae2e5 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Rooms/RoomsController.cs
@@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Responses; @@ -62,7 +61,7 @@ public class RoomsController(ILogger<RoomsController> logger, TokenService token var room = new RoomStore.Room($"!{Guid.NewGuid()}:{tokenService.GenerateServerName(HttpContext)}"); var eventTypesToTransfer = new[] { - RoomServerACLEventContent.EventId, + RoomServerAclEventContent.EventId, RoomEncryptionEventContent.EventId, RoomNameEventContent.EventId, RoomAvatarEventContent.EventId, diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
index 024d071..86c9f6a 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/SyncController.cs
@@ -1,7 +1,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Extensions; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Responses; @@ -11,7 +11,7 @@ namespace LibMatrix.HomeserverEmulator.Controllers; [ApiController] [Route("/_matrix/client/{version}/")] -public class SyncController(ILogger<SyncController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore, HSEConfiguration cfg) : ControllerBase { +public class SyncController(ILogger<SyncController> logger, TokenService tokenService, UserStore userStore, RoomStore roomStore, HseConfiguration cfg) : ControllerBase { [HttpGet("sync")] [SuppressMessage("ReSharper.DPA", "DPA0011: High execution time of MVC action", Justification = "Endpoint is expected to wait until data is available or timeout.")] public async Task<SyncResponse> Sync([FromQuery] string? since = null, [FromQuery] int? timeout = 5000) { @@ -28,17 +28,24 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe UserStore.User.SessionInfo.UserSyncState newSyncState = new(); SyncResponse syncResp; - if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.ContainsKey(since)) + if (string.IsNullOrWhiteSpace(since) || !session.SyncStates.TryGetValue(since, out var syncState)) syncResp = InitialSync(user, session); else { - var syncState = session.SyncStates[since]; newSyncState = syncState.Clone(); var newSyncToken = Guid.NewGuid().ToString(); + long stallTime = 100; do { syncResp = IncrementalSync(user, session, syncState); syncResp.NextBatch = newSyncToken; - } while (!await HasDataOrStall(syncResp) && sw.ElapsedMilliseconds < timeout); + + var hasData = await HasDataOrStall(syncResp); + if (!hasData) { + await Task.Delay((int)Math.Min(stallTime, Math.Max(0, (timeout ?? 10) - sw.ElapsedMilliseconds))); + stallTime *= 2; + } + else break; + } while (sw.ElapsedMilliseconds < timeout); if (sw.ElapsedMilliseconds > timeout) { logger.LogTrace("Sync timed out after {Elapsed}", sw.Elapsed); @@ -50,6 +57,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe session.SyncStates[syncResp.NextBatch] = RecalculateSyncStates(newSyncState, syncResp); logger.LogTrace("Responding to sync after {totalElapsed}", sw.Elapsed); + // logger.LogTrace(syncResp.ToJson(ignoreNull: true)); return syncResp; } @@ -136,6 +144,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe }; // step 1: check previously synced rooms + int updatedRooms = 0; foreach (var (roomId, roomPosition) in syncState.RoomPositions) { var room = roomStore.GetRoomById(roomId); if (room == null) { @@ -148,21 +157,26 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe var newTimelineEvents = room.Timeline.Skip(roomPosition.TimelinePosition).ToList(); var newAccountDataEvents = room.AccountData[user.UserId].Skip(roomPosition.AccountDataPosition).ToList(); if (newTimelineEvents.Count == 0 && newAccountDataEvents.Count == 0) continue; + if (updatedRooms++ >= 50) break; // performance cap data.Join[room.RoomId] = new() { State = new(newTimelineEvents.GetCalculatedState()), - Timeline = new(newTimelineEvents, false) + Timeline = new(newTimelineEvents, false), + AccountData = new(newAccountDataEvents) }; } } - if (data.Join.Count > 0) return data; + if (data.Join.Count > 0) { + logger.LogTrace("Found {count} updated rooms", data.Join.Count); + return data; + } // step 2: check newly joined rooms - var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList(); + // var untrackedRooms = roomStore._rooms.Where(r => !syncState.RoomPositions.ContainsKey(r.RoomId)).ToList(); var allJoinedRooms = roomStore.GetRoomsByMember(user.UserId).ToArray(); if (allJoinedRooms.Length == 0) return data; - var rooms = Random.Shared.GetItems(allJoinedRooms, Math.Min(allJoinedRooms.Length, 50)); + var rooms = Random.Shared.GetItems(allJoinedRooms, Math.Min(allJoinedRooms.Length, 1)); foreach (var membership in rooms) { var membershipContent = membership.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Membership event content is not RoomMemberEventContent"); @@ -206,6 +220,10 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe #endregion + private bool HasData(SyncResponse resp) { + return resp.Rooms?.Invite?.Count > 0 || resp.Rooms?.Join?.Count > 0 || resp.Rooms?.Leave?.Count > 0; + } + private async Task<bool> HasDataOrStall(SyncResponse resp) { // logger.LogTrace("Checking if sync response has data: {resp}", resp.ToJson(indent: false, ignoreNull: true)); // if (resp.AccountData?.Events?.Count > 0) return true; @@ -240,21 +258,23 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe // }; var hasData = resp is { - AccountData: { - Events: { Count: > 0 } - } + AccountData.Events.Count: > 0 } or { - Presence: { - Events: { Count: > 0 } - } + Presence.Events.Count: > 0 } or { DeviceLists: { - Changed: { Count: > 0 }, - Left: { Count: > 0 } + Changed.Count: > 0, + Left.Count: > 0 } } or { - ToDevice: { - Events: { Count: > 0 } + ToDevice.Events.Count: > 0 + } or { + Rooms: { + Invite.Count: > 0 + } or { + Join.Count: > 0 + } or { + Leave.Count: > 0 } }; @@ -264,7 +284,7 @@ public class SyncController(ILogger<SyncController> logger, TokenService tokenSe if (!hasData) { // logger.LogDebug($"Sync response has no data, stalling for 1000ms: {resp.ToJson(indent: false, ignoreNull: true)}"); - await Task.Delay(10); + // await Task.Delay(100); } return hasData; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs
index ff006df..ff006df 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/ThirdParty/ThirdPartyController.cs
diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs
index a32d283..41cf62a 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/AccountDataController.cs
@@ -1,9 +1,5 @@ using System.Text.Json.Nodes; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Filters; using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs
index bdee7bc..f163a8e 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/FilterController.cs
@@ -1,9 +1,5 @@ -using System.Text.Json.Nodes; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.Filters; using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs
index 98c41da..7afdcb5 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/ProfileController.cs
@@ -1,9 +1,5 @@ using System.Text.Json.Nodes; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Filters; using LibMatrix.HomeserverEmulator.Services; -using LibMatrix.Responses; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
index 2be3896..339e686 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/Users/UserController.cs
@@ -1,8 +1,5 @@ -using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Filters; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Responses; using Microsoft.AspNetCore.Mvc; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
index 0c3bde6..93e4b4f 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
@@ -1,8 +1,6 @@ -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using LibMatrix.Homeservers; using LibMatrix.Responses; -using LibMatrix.Services; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers; @@ -30,6 +28,11 @@ public class VersionsController(ILogger<WellKnownController> logger) : Controlle "v1.6", "v1.7", "v1.8", + "v1.9", + "v1.10", + "v1.11", + "v1.12", + "v1.13" }, UnstableFeatures = new() }; diff --git a/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs
index 97e460d..3cec712 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/WellKnownController.cs
@@ -1,32 +1,34 @@ -using System.Text.Json.Nodes; -using LibMatrix.Services; -using Microsoft.AspNetCore.Mvc; - -namespace LibMatrix.HomeserverEmulator.Controllers; - -[ApiController] -[Route("/.well-known/matrix/")] -public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase { - [HttpGet("client")] - public JsonObject GetClientWellKnown() { - var obj = new JsonObject() { - ["m.homeserver"] = new JsonObject() { - ["base_url"] = $"{Request.Scheme}://{Request.Host}" - } - }; - - logger.LogInformation("Serving client well-known: {}", obj); - - return obj; - } - [HttpGet("server")] - public JsonObject GetServerWellKnown() { - var obj = new JsonObject() { - ["m.server"] = $"{Request.Scheme}://{Request.Host}" - }; - - logger.LogInformation("Serving server well-known: {}", obj); - - return obj; - } +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; + +namespace LibMatrix.HomeserverEmulator.Controllers; + +[ApiController] +[Route("/.well-known/matrix/")] +public class WellKnownController(ILogger<WellKnownController> logger) : ControllerBase { + [HttpGet("client")] + public JsonObject GetClientWellKnown() { + var obj = new JsonObject() { + ["m.homeserver"] = new JsonObject() { + // ["base_url"] = $"{Request.Scheme}://{Request.Host}" + ["base_url"] = $"https://{Request.Host}" + } + }; + + logger.LogInformation("Serving client well-known: {}", obj); + + return obj; + } + + [HttpGet("server")] + public JsonObject GetServerWellKnown() { + var obj = new JsonObject() { + // ["m.server"] = $"{Request.Scheme}://{Request.Host}" + ["m.server"] = $"https://{Request.Host}" + }; + + logger.LogInformation("Serving server well-known: {}", obj); + + return obj; + } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs b/Utilities/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs
index d938b1b..d938b1b 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Extensions/EventExtensions.cs
diff --git a/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj new file mode 100644
index 0000000..5178012 --- /dev/null +++ b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj
@@ -0,0 +1,1034 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <InvariantGlobalization>true</InvariantGlobalization> + <LangVersion>preview</LangVersion> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="EasyCompressor.LZMA" Version="2.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj"/> +<!-- <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj"/>--> + </ItemGroup> + + <ItemGroup> + <Folder Include="data\users\" /> + </ItemGroup> + + <ItemGroup> + <_ContentIncludedByDefault Remove="data\rooms\!009948f3-9034-4a24-a748-a6391dd886bd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!00e1caa3-d6bf-4a0a-9793-0cebe26d4a96.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!011ed14b-f495-4c52-9f0f-b0fd0bd72f26.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0151df7a-3b06-4048-8b1c-11ea88987ceb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!017179e2-c121-49c2-ad06-197f4cf39155.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!01830986-341e-4622-a51e-6368a6064c2b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!018a12f5-8ed7-4dbc-9285-2165ae1ca39a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!01951dc5-d87a-4b27-b444-809f4907755b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!01d8efb7-52b3-461d-a95e-f16ca6ca5888.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!01f2c6dd-1bab-40e6-ac62-fcb5abf15e73.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!025c937f-13fd-445f-af46-69bda3d08d02.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!02d2dd0c-e57b-4454-a197-046d81f8d7bb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!02f00a13-d09a-4b42-888f-f6651d7882b8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!031b3eb4-d25d-47ef-b3ce-e06f6f4f154a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!032dd423-f2d3-4f3f-9b8d-10febd9de20b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!03ba6761-0bb4-4bfa-b895-6e4428795410.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!03ce824c-450b-4d13-a8a5-43a6ba4f1da6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!03cfb867-3155-4ef4-8e2a-0939e6837ec2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!03e4b54c-c41e-46e0-b47d-a6486fe524bc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!044df1bc-13a1-4496-9ba3-bcf60d45fbb4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0482a9e8-a7a1-4e66-a98d-1735758f8013.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!04ae0100-c074-4a0b-bb29-5c88b3199fb4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0511d862-353b-4cb9-9392-04eb3d6b756a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!054b1613-1757-438b-bfe9-73b501390617.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!05877fed-59a3-430b-9b04-10d70ca9aef1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!05d2ac50-d014-4e72-93b1-d8e18ee4cf8c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!05e4883f-4784-4a25-9748-d9d8ce4d314e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!06475f48-8175-45e9-8cf0-15741f27ad8f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!06b3a783-949c-42d3-a4d4-813273e3bab0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!07b2ed26-caba-440e-8cb7-172e311c1d78.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!07e9491b-f799-4552-90e7-242413ea69cf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!08602758-2108-45e5-8f08-baf57f8e90df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!086b0484-a1ed-4c04-93f7-2f73009570fa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!08fc4a55-9107-442b-961e-5353203623e0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!091cf52d-7932-4510-974b-c41ef0d05787.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!093a2862-3c0f-487a-bf43-6ae4d7ab9278.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!09925e93-ec19-49bd-ba19-e7b0fd0bd029.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!09f14b3a-21d0-41ed-bb86-1a602b07370e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0a34a93d-db6a-47cf-a5cc-8f9354435ea2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0a71f415-ec9a-40df-b300-c3fb79d4dd13.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0ab184df-be0f-4877-80f8-0aa34d5ad00b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0ad9691a-d4e9-4f4a-8f48-86a8adad7f43.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0afbbfb2-caf0-4829-a7f0-06472c16cb4b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0b6f3cb8-37ed-4a7a-b95c-5d299ac73755.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0ba81dce-f4d6-40fb-84fd-0df52347eb99.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0bd9becb-401f-465a-8b55-f2f20b4cdb78.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0c1f5b9c-4b80-4c83-989d-3dbcffa6f74a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0d7ece98-e347-4082-a9a4-d5e4cd594bf0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0d877be3-7545-49ca-9941-191e3299b7fe.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0dd1c03a-0455-4ffd-9ecb-5777039d4165.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0e3ca29e-0270-4ab9-8b07-c76f4662dcd0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0ea8696f-f527-4e11-916a-69dd9e086d9e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0f08c6a1-47aa-436f-9a44-3fdbd5aff348.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0f6a7cc3-2f2e-49fd-a953-401c9a16c38d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0f9fcf53-412d-452c-bcea-43d6dbb3d705.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0fb1c6b5-8bb9-47e8-8d9b-a126cb5eff7d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0fb79121-1e83-4fe9-8bf9-f8a5ed364efa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!0fc9886d-ecbf-4525-8f5c-a43fc4dc1b58.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1000acc2-a562-41f0-9f40-6eaf73b99f66.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!100204b5-f690-42b9-a49b-3fc3df91bdb2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!101265f1-eacb-4185-9470-269bf51b1829.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!108535e1-632b-489a-a9e8-e2fcf60d8c83.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!109f5a13-3a9f-4ced-a4a4-b0191a84b7d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!10c5ff39-6ac2-42a8-8fae-292f01a5cb32.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!111e8be3-72e1-4200-83aa-241c6e98764d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!11381a2e-0b1e-4a56-b53f-0dd2cf65a187.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!114243be-a9f3-4909-bb03-8171eacdf1ee.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!116a3988-1763-45ec-b24c-ebb9e57faf4e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!11a149c7-ee5c-4ada-9145-052976d3ab18.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1214a2eb-57ea-4662-8b32-742678cf130f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!13207273-2393-45a6-8cc9-7c4fbbbc9736.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!133f8b66-d9d5-44e4-95e9-dde76cceee0d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!13ed7c78-7160-4ebd-a84f-7124479d0b3b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!14586508-6e3a-4bc3-a8ac-dbcb177310de.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1459a8ab-3aa5-4619-bb94-7d636f9a4576.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!14cb8826-1b94-4106-b530-0f9609d93679.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1524fc2d-6985-44d4-9d77-08f02e2df93a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1528dc79-229f-4a0a-9433-ca0d8fe81fdc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!15d52bea-102a-4352-92a5-f2817e879083.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!15f05e3c-4694-403a-a95e-10def22cb013.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!161bbd52-f6e8-4ade-a139-27209aab5ca7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!164a1783-6218-4335-aa82-b9db00aa4cf9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1680277b-9540-43b3-813c-83bce9c2581d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!169d4aca-5385-4df3-afc4-5cc3f94708d3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!16a88afc-09dd-45f8-8202-98bb7a88cef2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!16c80534-e338-45d3-beaf-0e4e93695abb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!16d4b0c1-9bd3-408c-bfb9-48bb8ef7b679.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1709e1bd-5539-40fd-9124-0bfb5c62341f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!17308b64-da60-4966-bf85-409df16d5ec4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1749eb5f-f7ea-4eb0-8a77-7243123bbbf8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1761367d-a128-4ad9-9c7a-acb5a4e5a1b6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!176f30db-9b87-4a06-b3f3-895bd8f41448.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!178abf28-9ac4-432e-bac8-9c75f800fbfa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!17e1e888-b9e2-4aca-85a2-48e5d1d0b35c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!186cbe4b-c6b2-4e2d-ad85-931c8d078185.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!189c6c4a-fa84-4fb4-bdfc-ab25e959e243.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!18c0440c-5c3e-4c80-8397-95658769c20c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1909d1c2-18e6-4f41-b6d4-01a0350ecb4c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!191141f8-0556-43e3-b982-142854e40385.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1940357b-f2b8-45b9-a830-d35e7f4f46f1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!19ad183e-18f5-41a2-837f-5c440047a0c7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!19b0fc7b-0359-4aa3-9597-29a97145e7cd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!19cb0696-af13-4036-b000-7aa03bef9541.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!19e9f51d-278c-412f-9ecf-cb96a83282c7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1a0767ef-4897-4a87-8d95-9c1446b07780.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1a1e71ce-5f6a-4a9f-a3cf-8f95189eae24.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1a995531-85bc-4f90-b36e-29a1e8620e7e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1b2eb324-6682-4ea3-8377-2e17da2fd996.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1b3c2d02-b76a-449d-a458-15baf8254298.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1b60b136-19a4-4551-b682-b02a6cea94d4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1bc97814-3491-455b-8fc2-ca2916f77813.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1bdf6ed0-cac5-48d2-84ed-83b7f5ef3a42.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1c0450bd-8cb2-487f-9160-3ca95543410f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1c18b32b-c3b2-48ee-8562-bdbc1697adab.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1c273675-9267-46e1-901e-50460328d95a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1ce174af-bd5e-4ae5-ac1e-b71eb4b44594.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1cff0104-9ffa-460c-ae4a-4a4c17e2784f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1d3e4977-6df2-4f69-b8b8-e13ccffdec71.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1d4821a3-f202-46f4-aabf-7fa88387283c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1d6dc82d-978f-4f7f-8514-12f099d4e699.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1e3463cd-408a-4f85-b79f-5f1f9a2f4160.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1e96e1e2-e2d9-4547-90c2-c9311fdc1ffc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1ed2a1ee-ed46-4444-967a-3ad686fee01f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1ee34f83-373b-4508-94a9-3ab61873ecd6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1f1e1492-eef4-4dc3-a5bc-ff2aac6fd10c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1fd36873-80cc-4d2d-95e3-11e3df524dac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1ff577ed-8eed-41be-a839-2e9985d4ee5b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!1ffe979c-e5c4-4db4-b0d1-c0e615cb76cc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!202568b3-39a9-41a2-a040-bc8845e50449.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!20da7437-06fd-43b3-b5ed-e2f87198daad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!20f5dbc1-e343-428a-8a76-781193037198.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!210b0c40-0cb3-49bd-8ef5-028cf01499de.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!216fa2ed-abd6-4d1c-b87a-4dba953da630.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2170b28b-1c11-411c-ad8e-1b4d884b509b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!221ecfe5-5d11-44e5-a7f3-ab71ce8c9774.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!231333d5-f5c4-4df4-9d09-814338ff0b36.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!235e6b94-0c1b-409b-881f-f293d356bcd8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2363d819-977b-465a-81a5-d16ab55a2e22.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!23766876-4d3e-421f-a26c-abb822473a1f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!23a77580-70a5-4f71-bf88-383caa4edaba.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!23c45ba3-cd4f-4fa2-b129-948a350d28a9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!23faa183-1aed-4a7e-937a-6b46768c9e16.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2449949d-7cfe-4680-99d7-97167eea6b52.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!24b54660-10aa-44be-a54b-c7b571d0427c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!24e064e4-b388-4bc9-b379-88e485deab8a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!253ea427-a360-46fa-a1bd-e319055a9bf5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!25f695b3-ee00-4161-ad85-209101aa600c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!25f6ff1e-e9bb-47b9-a9ca-f72f7c5ac797.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!26089b3d-c318-4d35-af3e-255eef32bc7e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!267a5f9e-1dac-4b5f-89cf-d0f1741aedf1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!26879a0f-d760-440b-be38-c70ba711e016.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!26a15ed6-2da6-4796-8526-d63385e62189.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!270af0c6-1496-46af-91f6-9977aeefeef1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!27897886-242b-4f28-a79c-9c71b787a956.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!27adc88d-bda5-4888-b6bd-9691f55f3a3b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2830fdb4-947f-4f7a-aac1-c793f84c2d7b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!285a2bfa-8e59-4613-bc4f-f0300ed990a7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!293d9933-de29-455f-9a89-aa1b131afc2b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!296bf7a9-68fc-47b7-9c6c-eab873a25565.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2987c88d-23fe-43bc-99bf-a57fd0f13675.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2987f9ed-ae51-42f2-b0c6-6ae736646360.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!29ecae78-49d0-4b07-98ab-ab75527f68be.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2a763c96-c63b-4332-9ed6-dc7e842a0440.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2af2278f-aa89-43d8-88bb-f20480ff9c58.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2b1e1b17-babd-4e87-a119-16ceefa61be3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2bbf11fc-ff3d-476a-beb6-4f0b515c93ea.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2bd0be39-ea9f-4c56-9faf-cdf5b1f3c079.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2bec4090-07ec-440a-908a-a722168fb4e7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2c2e3e2e-50d8-4440-86a9-9047ff752cdf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ca6f22d-34b9-4ca5-ad14-c96abc6c17e8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ce029f3-f023-44af-94dd-06390e68198d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ce31ed9-b818-4b70-86bc-2e41a7d71b3d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ce9adde-2a0b-4a87-bfee-74f262cc73fc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2cfe6d51-ddea-4915-8e5f-21a74df2bfac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2d00f9bc-0fd3-4da9-a254-a90e61c15067.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2d87b8b1-0ca6-416c-acba-3349b6a2a514.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2dac26f3-01e8-4aa6-817e-2a89cc59290d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2dc32058-e0b1-4e46-8c5f-a46a60fe85ad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2e0a9a01-5bc5-432e-81c7-378a9b35ead4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2e1facae-b5c7-4a96-ad59-a15a28da15a4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2e5640d9-8e1a-42e9-b630-a869ea6c44c1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2eaf153a-d5a2-40b3-9d59-a0424133003a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ee4350b-006e-4d2b-9b71-87b8ac202e6e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ee9c481-dbbf-4230-8d4c-d8dc11bec46c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2f61cf24-2da9-4050-8d71-ff96cfd478c8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2f709dd0-2acf-4553-9d3f-1b3e7d7e9a3b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2fe91768-c1ff-4360-9dc0-ea052da98d4a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!2ffad0b7-fa7f-4b88-97c9-54830b4f460c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3047fc05-26c5-4649-8fc5-6d83afd9d42c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!31197c74-10be-46d2-b36a-e2b96d22d57b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3142892e-219c-470d-80f3-0d7e5c34150f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!31a3f8cf-614b-4b08-a61e-52290f3a197b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!31cf85c8-3c6d-492e-b43f-4d0a8c2a055a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!31d505c2-817f-4e4a-88bb-710ca54869ed.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!31f9f8d9-e3d5-4c77-805c-3cdcd4d1a140.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!321a0118-d05e-4d95-901f-b7929e65628c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!324aac37-6820-45e3-8b14-21a21334a718.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3288ee08-56ae-4371-8fcc-fae09bb29053.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!33d6862e-cf55-499d-963f-93a9e393cc5d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!33ed93c1-91f9-4a3c-b55d-35c927a034d1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!344ceb77-d29b-49b0-a328-0104c35f233f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!34a06da6-fecf-401e-9dbc-1a072b0da618.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!34b6371c-977d-410d-b3c8-920b1fda6b84.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!34c9fb4b-912e-4d0c-a593-cc072b07376d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!34f7fd18-78f4-47fe-b0e9-28f0181aef28.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!35334b1b-0748-4ae6-b290-2b9d952a0cc6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!353ec49e-5261-49d9-b701-0073818dd29a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!35d3f72b-29c9-42ca-adb0-89302521cc58.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!35dc7a95-211f-4383-a123-466555ca192d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!35ebd4e9-69a9-44d2-a152-426330bac118.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!36132c8e-a7eb-4a04-866c-b97d95fd51d6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!362d1839-0159-465d-9a81-faf0f4b9a223.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!365e92dc-d8ea-4e86-8d42-0aacb460e98f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3780fe5d-358d-4b32-8f29-fba7cdada78c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!379a776d-54c8-4eee-bef4-0492e1b05e3a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3866c302-ce01-4ffb-8b7b-4d83bab72ca4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!38cfc692-cd7b-43b0-b242-4d4445b750d8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!391d087a-62d9-4a38-ab28-d58aa9c6e970.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!39392d41-09c3-4a2f-90c2-52a8805ea39c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!394c788f-640f-4ac9-9d54-10607998b486.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3964e3d1-7f92-45e8-b24b-7ab15013891d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!39bccce2-b78c-4cf9-8428-308793b32e71.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3a446fce-8d08-4ce7-819a-f2f316dc0153.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3a5b7b5e-a7d1-4515-977c-59f968f7c132.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3a6a92be-939f-4820-9366-dc485d46dc4c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3b08bd24-dd41-4fe6-8a73-5e696201f5ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3baa7569-6cd3-4a38-a9d9-be6bda89f5ef.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3be0678a-feb2-47ab-8b1c-bbc4d78dc171.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3c0a0580-8152-4bdf-a511-26ba9f32857b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3c19888e-ab9d-408d-90ad-4bb92b2bdbf2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3c353341-5051-40b5-b97c-ad7284dfda13.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3c6b7be3-d937-4f2b-a251-14b0814af9ee.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3c95c9ef-6ca0-4f22-a4c2-82581f5ce566.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3cf1cd63-6e1d-41f8-b38b-99919923445a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3d69d242-b996-4912-b637-b2ff675b019d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3d69d419-4f1f-420f-a86d-8be471e522e5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3dd5ace5-70f3-443f-a77b-f2b3efc8b6a4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3ddd3d20-ae93-4448-8b65-38ce0cdf64b3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3ead0dc7-f117-4478-a8e1-28bd33969d65.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3fd47b87-b7f0-4aac-87f5-7370282cadca.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!3fe3728d-6531-4b01-ab58-bbd17cc8edca.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!401b32b9-cd18-4da1-ab56-02470a6ca453.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!40cf93b8-ac5e-47a1-ae71-998ad27ca189.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!40e14dc3-d16b-400d-8c13-dbf2d5ca90ee.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!40ef037d-369d-4e0b-9dfe-582bc9f73525.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!40f5792b-73a0-48fa-82cf-c13e0b8469d2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!41a0a70a-a5e1-41cd-bad4-dc1b371172fb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!41a7775c-cf14-4060-b897-ebba44d643ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!41bf7a42-68a4-4df3-8006-cf8524d3dae6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!41fb0617-119f-4fa3-b813-ff80a5c6b817.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!42a66b22-60c7-45db-a17d-f78822e758b0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!42dbf858-050f-49c6-82f9-5bfecbe77cfb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!43510534-019c-43e9-a9fe-ae868411967c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4383cd84-9f31-4779-a798-a1a237d3efc9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!43d322d3-42a7-4f27-ade1-7886fe5c955e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!442e840f-fc88-4fd3-8932-4943c8753360.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!44684574-21b6-4ca1-a8cc-2fe11e459ca5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!44b00d92-c007-493a-85c8-ee0dd43fbfe0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!44d3ca18-311a-44a8-9fff-8912c7ecac60.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!451aff08-e181-45b6-a78a-c594f0fa056c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!452cbd9e-1d72-4e84-83dd-b42449f05baf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!45600fdd-f784-42f6-a514-7f1e34e5c745.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!45afbb30-3ab4-43ab-90ac-f5b902c70557.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!45daabc6-cd56-4052-b6eb-8663f8b8dbe2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!45ff7d79-4e27-40d2-93ee-8b24f78e89ec.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!461b5b97-1d25-4171-bb20-aa925d72bce6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!462b0be0-065f-44e3-96a6-3e67179d6674.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!46430d1f-b496-48d0-b5bc-5de79cadf175.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!46962d6e-92b2-4245-8a03-b10d38546911.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!46efb743-ab87-4ac7-9682-c3fcd15ed92c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!47199b5c-57c0-4aa4-bbfb-20d5c70212af.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4719ae4d-008f-4191-89e8-6c15f31c1fb7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4723262f-5574-4a31-bb74-57ec1e0651ab.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4776dea5-e0fd-4421-be38-c671999842e3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!478f85ae-4468-49ef-8d22-4d4d05570fae.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!47ef9d8c-24ad-44a4-ba40-526716184725.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!48f5f5eb-6fd5-480c-9658-c23e2952b81c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4909420b-44e9-4a00-9c97-3b8d61a0a1ef.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4917c167-f8bc-4416-bafc-37d2fa05537d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!491dcbbb-5398-4520-a6a8-71ff45dfbb49.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!49636e8f-be76-4105-ab9e-90e9bc6855d3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!496f12b0-20f6-4388-a078-74e31c9c8db8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!49a0ed75-a6ab-4005-ace4-a9cf6351cbcd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!49a133d9-a891-4d85-b46d-da7b7160c1f6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4a43c56b-93f7-4ce2-9cc9-eadb58f38487.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4a65073e-7651-45d6-b501-e9437434fcac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4a850743-1c6a-4f08-a580-a2ec5d91845c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b1244f7-ca4e-4833-b500-c9a377dbb7ca.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b15fb46-8496-49c5-8f6d-bf9c7daa6793.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b33efd9-b324-40c9-bfdc-3c1970b86195.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b358236-7bd5-4fe1-be79-5b6d0985e112.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b466e87-444d-437b-a830-14c0cb6fe2f4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b6f50dd-a934-44a5-9925-004e6a89a779.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4b82a600-3b76-4865-a422-517b6155034c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4bf9b456-2028-42d5-bec8-53f046eae7a7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4c0351ae-e696-40a6-857d-45b82fc4f9a6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4c262d60-7c86-47ad-b6b7-17e3ee5d2b65.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4c2c29be-4591-4ff7-acd0-bb5f13225315.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4c65cfdc-3c2c-469f-b204-a71f50ee3a11.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4c6d07db-ed39-41a5-b489-89f77ae4c67f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4cd1a8da-c3fa-4feb-ba5e-b93131f33729.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4d180222-335e-4c40-8e61-abd709ddbfa0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4d528a08-13d1-4d0a-8f69-d870f1245a9a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4da181a0-9cee-44ce-a2db-9acdb2154fac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4dbdaa18-bf2a-46a7-9fa5-272a503fcd57.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4e0cc5d6-eca3-4cd2-af5a-0a5e472a9a40.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4e42d7d0-b511-42ec-8888-858bd10b4413.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4e478383-798e-4f22-b4a9-919b0609267f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4eec4377-98a0-40fa-ba06-be2777d6a463.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4f68be5d-749f-4ae2-bf17-ff248c6c47b0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4f863192-0991-4618-975e-55772df49772.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!4ff69980-0724-49c8-b885-ee5c86aee9f1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!500d38f4-f55e-4091-8bbe-806c9bbec2df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!50340cb9-b71e-4748-9dbd-80d5e2ab4533.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!508ade04-1815-49b3-bd31-81a9da598277.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!50a458ad-1ab0-4da8-8175-b90e1f987b09.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5107d457-b1a4-464a-93ce-452a5aaab204.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!514dbafe-7499-410b-aa6b-9a3cfc39ed94.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!527b3e69-4a28-4cac-a358-c204444f499d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!528ca094-7692-4a01-b1a3-0790926d5544.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!528ce4fb-8249-4d3f-9743-a1dc9bba8829.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5296546d-7aab-4e5d-9d4a-027966246645.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!529c7af7-691e-4480-b88d-f4c424c30610.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!52a3ae7e-0653-46b1-8052-8cf1cf17da95.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!52a3e47b-0213-4872-9bfa-94ac62c92698.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!52c624d8-2ed5-40eb-b3ca-cfc19cdc8e71.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!52d1bfcd-3d9a-4469-8fa1-413491ca75ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!52d49ea6-a9b2-4332-b712-46a9fe47e7cf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!530618be-416e-4281-bdf4-432cca2ba154.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!53205a54-7175-4adf-9362-9e83d27a1414.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!53219c8d-ab96-486c-8d72-32c08d8d1e14.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!536e266b-8b25-4a1b-a5f5-6778399c90b4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!53869421-9475-4607-9ef9-efd4af0d3b98.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!53ed8360-33f8-4539-9f27-ebee01d3cfdc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!541ac3af-938d-4d2d-87ec-d40a7d51f949.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!54385af9-0e23-44dd-bc4d-2bf6362da26c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5457ed85-c48c-4ccf-8905-44751b42225f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!545e67e1-1744-4937-9bf1-a9666a903a0f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!54cf9e45-e833-4e90-9fef-7b49da74dd8a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5540a57e-b315-407a-ba5a-e41be9436b3b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!555a5486-947b-414e-a500-5248395bcd97.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!55cc236a-7120-4b8d-b990-afb7f6386e2d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!56624f0c-b9d6-4107-8c1b-e238ab6d1182.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!56805a47-39e7-4b6f-9f1c-d6762a70e655.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5691c070-0e79-44c6-8c01-2fb0031df8af.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!56a13e14-6a26-443e-ad58-84c7f8703ca5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!56eea411-09d9-4339-b21e-dcbceca22a8b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!570f1dfe-8829-4593-bef0-f42230b519ef.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!580a90be-62cd-4679-bdd7-1b28f9934b0d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!580f5147-ea97-432d-ad40-42cfcb3cf2e6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5890f4c0-88ad-4800-8232-d67b3da9b9d8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!58e03112-fd39-4e87-9b8b-fe8e9a92cc94.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!590ae097-0b16-4a6c-8a3d-fbfe6d56224a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!593c6e1e-de58-4cbc-a933-9eeedad35cc4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5994b78b-a10e-4a43-87b4-9c2c313f3c9d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!59e04135-5a7e-4c38-91e8-6e3213c384df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5a165c73-27a7-449a-9a8a-94c377e78d92.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5a176a47-1bae-411f-86c4-5ce9a434878c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5a6465c4-ca0a-4ff5-a3c9-60be0e2485ee.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5a8bf716-15e5-40a6-8d4e-d4c6cdef49cd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5a9e4fd6-0206-4fcd-85e2-aeb0414c223a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5ab45d5f-316c-4519-8552-30222f670430.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5b777c93-d41b-448c-b14c-4f8e6bdf0956.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5be6fb36-dffc-4ffe-a553-f164714f6370.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5c0c8af0-e2e6-436f-867c-ea652a0c8a4d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5c191dd2-a708-4bec-be43-12a392460ffc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5c6d36dd-07fc-4421-bcf5-48008c49b592.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5cb2c113-40fe-45c2-97db-786ac0003a69.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5cd92388-abf1-40ff-82c8-f8b448680114.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5cf9dec3-bc79-4d2b-b12d-49132108abc2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5d022dfa-9ac2-4396-9683-f6f32c8afffd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5d65c2b7-a08d-40d2-ac79-2675933e0594.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5d743f7c-884e-4ab5-ab60-caa95ab2a935.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5db3bc2b-39d2-4ecf-8c70-e009243c0e1e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5ddf0dd3-5782-444e-9610-093b736ddfc8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5dfb804d-d608-4b1f-9cd5-d3bd8a4d767c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5e439239-b615-4827-822c-d2c16adf909f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5e61773b-5593-4752-867e-7c0555d89b35.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5e79c9b9-c28a-4cd4-a1e5-29c32987a56e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5e97d736-2d04-4c28-9692-bc22b6a2fcfc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5e9eb177-ead9-4476-8418-567c01b4ab87.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5ea34f5c-48ba-4100-b56b-0592cb44a691.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5f3f177c-9e7a-4471-8c2b-b9c82196ac29.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5f64dd66-fec7-4e6a-a356-ed786534481c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5f975c45-c190-4aa1-9f04-20301c3ce827.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5fed32d8-3433-4fef-a31a-d07dd279f505.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!5ff2de80-5117-4132-be8e-fb640c017658.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!608b89e8-29cc-408c-94a0-332f4d5fa220.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!60b22989-525c-43d6-a2e5-d4763c7c35cd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!60b51119-3f74-4ee7-8382-022b876627dc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!60c64de3-a206-4ec7-a471-6231e2a87ad8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6103ef62-4d3f-4c45-b8c5-917479681fe4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6114533a-571b-4678-bd70-282e33652305.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!61334577-4c4b-4f15-a044-4e5b37ca99fb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6161938b-41a6-490b-9fc8-80549584b3c3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6162d9db-7522-44e7-aa6b-5cf919d931c7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6163f333-883f-47e6-b9cd-2ab8f58253ec.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!616f742e-7a02-4b87-9ca8-ad1ab6424ada.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!61a27993-72d2-4f6b-9f33-fc05b83b9a92.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!61b1bd3c-6cb4-43ba-850c-4f008020cf75.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!61b2b409-7935-4a49-a77b-b8385651afb1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!623b5349-f07d-4124-a018-ea4203f0f584.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!623c857d-300e-4b82-a7c9-841ad3ff5720.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!625b9cb6-df51-4d54-953c-f0d903286af9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!627ac2fa-8384-4c2d-b14a-57cadffa9307.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!629fa0a6-d0de-46cb-8670-c3b78b5b026c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!62ecec36-777b-4e75-a8e2-ecc1b5acc989.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!636119e3-4107-441a-bb92-ac39aa04e28a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!63821479-398e-4f03-bef1-e050ee0cc7e5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!63d0ca04-40c0-465e-b31f-52c008241771.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!63d4aa81-eadf-4d81-913b-64e26851d093.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!63f904f6-a1cc-474a-a43b-06ec3d07191e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6443867d-f5b3-4a85-8df1-57aaac003dd8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!648fe408-e54a-40e1-b148-b97162fd6c61.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!64954fab-f87f-4cb7-9bf0-88829f91b25f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!649e9af2-3160-4a44-9bd7-ecc909f51f69.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!64a42e40-2bd3-4b35-915c-2034abd3fb37.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!64fd2ab6-fdad-4299-8278-61769de3c386.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!655e242b-68d7-4c73-b053-5a398ef9daa0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!66097526-6373-42e8-8d7f-9930899d42ad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6613b5ce-70b0-4286-824e-6cdd5504e5d2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!663e92df-0e24-483d-aad0-7f8745b7eedc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!66d37076-cdc3-4a7d-82fb-2d53dd9a083d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!66ea76e7-2fec-4397-bada-48783193a2f0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!67345f81-971a-481d-92a5-c93a89168247.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!674e4ac2-fa73-4707-b8ae-5c179c076c51.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6794ef03-6d4d-4c47-8285-a37cdba4d64e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!67cd0076-0a88-470a-be4f-796239108eef.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!67d57362-ce3e-496b-929b-a8da87cd3862.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!68422c19-788b-4dea-acfb-5e49f4800990.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!68467b77-4a4b-408a-ada2-41ac4613dffc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!68b476dd-95a5-4e4e-b202-35b81e5a3731.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!694b53f0-000e-4c58-8f80-0ef9076cbeb8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!69906978-ce3e-4f70-b89a-380c69b13982.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!69bb2ff8-6db7-40d8-b45a-ef101cd969fa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!69e5c80f-58f2-4615-8044-86af98bf3086.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!69eef5e8-ab54-4eb0-8e69-5198a4dd45d1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6a3216f7-5afd-42a4-b021-02dfb397321e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6a54ee0a-5567-4e89-ac6d-c2902bc4fdf8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6a5ecdeb-e183-45b3-8a30-7bbc3545aa27.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6a9002da-8c11-4e6f-8ad3-15d79111ddcd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6b81c752-01e9-458f-b261-bf8d4688166b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6b8a143f-ccc9-472e-b077-bcc0e044fe36.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6b9cc525-e7ea-4fe0-aa3a-5ffd31d5faad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6bcb38c1-4958-4e3a-bdd5-32f52b58a3f7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6bf138cc-5bfa-4fd0-9c04-c2d5f072ccd5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6d840bfb-0045-463a-b226-727c278f155f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6d93ea42-f79c-4385-bd9b-5e0fba1a5909.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6de3378e-baae-45a3-8a84-03ec11148bc3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6e49ac34-422c-42d2-9c77-305b68a0ac54.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6e7c7434-fd9f-42ae-8cc4-aba1e2ff18aa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6e9d7e45-865c-4867-9f4d-0e96f3921c35.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6ee08cd7-3540-489f-9c6b-674b298737d6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6f7a879e-b2cf-4a20-bec5-5c09de93c4ad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!6f829ba4-b4e3-433c-a860-8e186e30ac85.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!70c4999f-08e3-4aea-8973-ebc3389c5e35.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!70ddde73-376f-4562-a097-b1424f2cd70e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!70edd37a-7220-437c-9619-26748a492a3a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!714e6e75-7832-45ef-af06-bf66039166f8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!719db094-4cb1-4cb7-be57-3bbbf6e34ec7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!72315498-eb3b-44a3-a008-c2d46463daeb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!724a70f3-b99a-457c-a074-b5899daaa4d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7285b815-f6cf-4cb8-904a-1fcee299d77e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7295f5f1-1342-408f-99d8-d2c10bc86f23.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!736868b2-3f5a-4688-b391-790b7d5fc5e3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!73ad57ab-b839-4fec-9af8-814115c27e68.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!73fbafd6-ca4d-4f7d-bf00-b2c878f36891.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!73ff3e24-d56f-4bd4-91d1-e95725bb5139.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!74eb5bd8-4757-4263-9a6c-e77c9c2ae6f7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!756e9840-5e66-4d10-b93b-7a7b6cf38832.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7571dd3c-e54a-4c39-87c0-33cbaeaf9648.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!75d61410-1198-40d6-9a2b-3c5da0c3ac92.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!75e6c3a8-5e47-491b-82f3-0c7000df0f2c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!760b828c-374b-466c-b27d-39b04a2af257.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!761ee42c-bb77-43bd-ac52-46a1a00a833e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!76fa27b9-c343-4c79-b500-7821f787592b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!76fb2de5-6c22-45ad-a984-e15fd2f4b93c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7767a63d-7556-4b2d-a91b-485df33a6889.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7784e064-fdab-429a-8505-042653461c7a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!77dc5e61-c8e0-4be7-b604-4e1e2ed5fafc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!77ebb6f7-97d9-4846-91b4-208396eb89e5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!781a5078-228d-4a04-8371-0071970a913f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!783efa63-029a-48bc-9d7f-789811a1c43e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!786de287-e4ac-4269-90ee-0f8d1c99e2c3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!787d6c56-a077-447d-958a-1183108265af.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!794406d4-3ad0-4436-af85-3320115799fb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!79519d6d-83b9-4bd8-bbe9-f85296401068.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!79da8d0b-947a-44f4-8b63-213d4a276eba.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a2d5031-ff86-41f2-bb18-204ddfd631aa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a452301-05b9-4114-b5dd-ed8de24c97ad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a48212d-d1a2-4680-9bff-f6dc2e322c03.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a4d1809-bf26-4ee3-8c9b-8de0470fb883.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a67a249-01d7-4cd2-b885-c18781d1b2a0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7a9dfabc-f519-4adb-ba00-2dd6c2827e08.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7ab35026-dfda-471f-baee-3c0c8ea03eeb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7ae7061a-0931-438a-b905-c1e54f48c474.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7afb9033-4f15-421a-981e-f4161d006e85.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7b59eec6-53fc-4b9d-8c5b-6f3ecc5a4ebe.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7b7d15a8-aa1c-4044-a759-2947b9131324.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7bc0b3a0-f830-43b7-81a7-a31c52fafe16.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7bd17e98-bf49-45ab-b1a7-bd6d85035a07.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7bd5547f-aac6-469d-892a-53f3f9f71d0b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7c4f45e1-02d4-4e48-ba3e-4a637f3790db.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7ccaf774-35b4-4116-ba95-69316b85dd35.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d27f50f-1d49-4293-834f-3663e16a98cb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d42d75a-7c2b-4fa7-9031-c7cb005f1967.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d488e6f-f188-46aa-a140-22fd49e22f0b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d7f7373-0ad2-460d-9b46-c2d57eca8e04.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d823240-f3bc-40b8-9e7f-c6f1b77918e7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7d980a7a-6d46-4034-8f1a-d119b4350f05.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7df9eacf-941a-4828-a262-6d132c095dcd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7e4d28ed-c284-4354-a018-78072c374052.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7e79d4b3-fbee-487a-93bb-786d259d8c10.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7e8eda49-46ee-435e-9a90-717192068160.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7f3ec66a-08ea-4461-ad43-d5309502ad88.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7f515865-7563-459f-9f89-ec4649ccf6da.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7f9e76ad-7803-44fc-ad43-24c1fd7f2914.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7fe13852-666c-49eb-a91f-da36122a1794.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7feecbb9-b6db-40a7-9e06-05a025a39d7c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!7ff3ec0a-f857-467f-bf42-242056982142.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!80213af7-e194-429b-9a88-043c0c556e4d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!80336cab-19d9-4cc6-a81c-b34063b8b87d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!809abe8b-7091-4df1-9229-1749ceda56c1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!80e7ff58-53e1-473a-9a29-ff5e4f033f1d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!80f697d5-8f52-467b-9526-f4709689c5a2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!810287ef-4020-42c4-9a00-2c22552b409f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8179f1be-f633-4867-b554-055946a9c9fe.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!819c4318-5c18-41ad-8bf7-7148cbdc94ca.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!81bf0e44-2afc-4adb-8906-b912d7324dea.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!81c501b2-21cc-43d6-bc92-a4619ebc3d02.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8240981d-9292-4c82-b263-3d478bff5064.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!826ff981-e71a-42c8-97b2-95e36dcf2b5b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!828c8579-9c75-43b7-aef3-3436516361df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!82aca1f3-e6a0-4eb0-9850-0b4225383cdf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!82e303df-50e3-4d96-a685-ff4f8b4b2ebd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!82e9aa2e-4cec-4325-8901-5db37fdfd0c7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!82f61d00-9565-412d-8dbd-71677ce2b9bd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!82fd4e98-e5f6-4b4e-a63e-778f37d95e70.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8309da6c-6419-4d42-9700-c510705f6611.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!830e080b-c885-44af-ad2f-06ca60fcbf8d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!83531791-e6c8-46cb-81b5-474e0a2aa330.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!83ec3f44-cd1f-4300-8af7-d2b5c6aa1379.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!846d6318-3637-49f6-ad26-3ad5f7a72e6b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!848aede3-f8aa-4f24-a4d7-6ee8f5862ea3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!84baf477-3e1d-4af9-afcd-a1c6480dd897.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!84cba453-dc0a-4db0-8663-bebe9b729d42.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!84d82be2-4f5a-4a4b-9709-e81253736f47.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!851fef2f-538f-4b64-ad62-46d399c0b9fe.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!85529642-880e-43e6-b628-5d83ee0a3499.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8569d782-7a72-4a08-ac99-48364a0664ee.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!85724c05-e2bf-45b8-859e-c27ba22d6c06.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8610348f-6220-48be-af80-c2e3e3bcaadb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8614e7a3-8af0-45f8-bab1-f3c5b3c988aa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8666776a-5c94-4480-aba5-fa03c9610b60.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!86aab75b-c414-4ee9-a248-69481d8970d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!86c30f5a-174a-4ead-900a-01b8f6549833.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!86c3b3e4-dd53-4362-97b0-f813406b7e93.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8708eb0d-45fa-44a1-a933-daa7f037d6fb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8769b30e-93fd-4878-8630-6cd79bf37e3f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!87b12842-e6cf-4edb-adeb-d04d9052ffbc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!87bf5cff-f62e-4f7f-9bcb-f095bd027ad5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!880f5cac-8b76-4be0-b6a4-9a5b1a76d7cb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8863ecac-3c8f-4c49-9651-9daf581c3661.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!887df1bd-70a8-4ee7-acaa-14944486a0f9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!888fadf9-58d1-4147-bac8-e192d87728db.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!88c986fd-f3dc-4fb1-a36b-2c31aac6e82e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!893de46f-fd7c-45b4-b161-64875029bed7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!897dfd06-82e4-46a4-9534-05f09078c8f0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!89824cb3-e607-486b-aed9-a56ae41b6a28.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!89a36790-14d5-47a8-954e-cd8df0cc735f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8a4ce707-c4d8-4416-8111-03670b92da13.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8a5b653f-2bc6-416e-ba2d-881b9f891e32.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8a5e4f44-5a35-464c-b465-5310afbb21be.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8ae39bc3-8ce8-4e10-9baf-34771e6af458.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8af94adb-44cd-44f4-8d94-c6d9a495c826.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8b371195-6702-439f-950f-f71e2ee233bd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8b7bd3b0-5063-4881-88bc-5dbdd029202e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8bc1e90e-c2ea-4a08-ac4a-c1bbf7421f30.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8c74a87c-03ad-4f75-b928-b7c9e01c0cea.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8d00edda-9300-4834-acb9-b4fccb581886.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8d3461c2-0ba7-41d6-bdcc-3b1f37109334.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8d8a7616-affe-4ed2-9d41-d5b9a3a5ad4b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8dc2a80b-7e5f-4eaf-9b7a-4b59b5979078.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8e345f62-0a2b-4399-9815-4420681fdcd6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8e583211-dd76-438e-b980-c64e99a3db2d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8e6157b9-51dc-467e-be1a-ed2ccc50cb7d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8e8a5d26-9841-491d-80dc-9a1ed1a03ca4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8edc03ed-f541-4147-b780-dc5d771bb2db.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8f913ee0-31a1-439c-9f91-b476b6cc8595.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8f9a93d3-3222-469a-8c25-1ca5bc7e98e1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!8fb8ac48-f2b6-45b8-98bd-daa519d242dc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!918fb788-49a9-45ff-afed-7e5a274a00dd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!91a34035-db1e-471a-b69c-ad3c1e7f86ab.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!91c7d204-3bae-4d5b-ac03-015206ff86df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!91da77d8-35ec-4894-b17c-dd9b787abcfd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!91fcd66a-a3d5-4452-a7c4-5bc8c7634727.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!933fc143-21d1-41a1-b2e4-1259485693e2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!93407142-ad96-4e20-a7b0-23781ea069b5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!93579f05-47b3-428e-954f-637ca1a635cf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!93c6717d-79a9-4f5a-b447-a4d148ce55e3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!940c863f-ca7a-418d-86ad-0f561a7a5cc7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!958ce060-b2f6-49dc-96cc-7203cb6e4048.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!95c24965-59d9-4197-b203-9d28fef41c32.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!95c24d4a-510b-40f9-8703-e82ce2aacf17.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!95ce6440-c5a3-4c78-a40f-0a6bdbe8f11c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9640253d-cbe6-4e63-832f-c8c5862646d3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!96b66bf2-632e-499b-b51a-dffe3d74b7cc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!976989b0-3036-499e-ae81-1614a027db98.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!976fd366-5337-46ce-b252-cf7afa0c3368.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!978060e7-a3b5-48c8-b2cd-d186fcd1c9b6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9799cb4e-dcba-465f-9c75-49f3ef5f1acd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!97abdb0a-a490-4528-8b7c-0f3dcf7b4325.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!984d09f3-a2f6-46b6-9fa8-61860a74f85a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!98c77d4e-fccb-45c2-8a02-6ee2a5415fa2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!98f30fc1-da53-403e-9e2d-6034b50c2f9c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9903fc3e-0984-4dce-aa04-3f4dac0f541f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9918eae5-18bb-4aca-8963-a0fe59d9e781.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!991f85e0-6f1e-4d5c-8019-d856349e14b2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!99770653-f825-43f4-b0f5-edf87aa4023d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!999bd22f-b7b0-494d-b5e9-4b7c824d3382.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9a0f8e15-86e1-4e7a-8b9e-961cafc45629.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9a66a615-7ae6-4952-ae92-efaafb819cbc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9b54e996-fda5-4e27-af77-d876eb7f9f7e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9b56b840-1819-409e-b6fa-66919471c651.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9b70d17b-8976-4368-afb3-51df6e632860.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9bd82993-74a9-453f-9764-1b3ad5ac7f9c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9c385667-6e21-4ee9-b010-f5dfbce33062.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9c38ece0-a2f3-4ad8-a171-5e8ffc161350.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9c5a1e44-8cdc-426a-bc9b-aa6f6c74e3da.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9c719a62-7841-4e25-8b93-ee59f7c20233.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9c7fac0d-445e-4a84-b7d5-45ff59c027e8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9cacdd4f-d708-4357-9ac3-175f64b7b163.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9d64cdc2-c11a-4185-af80-377fd2ffde6f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9d72af86-5de2-457e-b03c-8e8c9acf1d72.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9d991242-911f-4b3d-9d3e-657c09dabd11.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9ddb6a8c-45a9-41e6-99da-eff96797c186.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9de8324c-ce97-4a13-9962-0057c558bd19.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9df30072-b3c9-404d-b773-bf9ef89858ef.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9df59639-d274-42c3-bac2-93809e5baa22.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9e2765f1-9e81-4164-926a-776c17efcee8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9e5359b0-024f-4ebe-969f-672a199c5b73.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9e62b6f9-23ca-47dc-adef-22c908593a21.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9e806174-f27a-4fd0-b419-73a20fb1bc17.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9f04c67d-e38f-4ab3-8cf6-4dcc2d4dcd7f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9f52feb2-65ae-4652-9fb9-d7efdd78a259.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!9f95db47-94b6-43b9-9094-f523295f14a4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a0574842-95ef-4402-b003-514936196fc6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a07c9e8a-27ad-4a53-9aa1-b4cc17a031f3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a0d73922-5fe4-4631-b0f3-0b6724d66e17.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a0d9e00f-87e7-4168-ae85-472f7e162dda.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a1025da4-a811-4068-8416-16202f7e899c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a1dc84cb-462f-4183-95f8-5203c4080be5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a2116151-c4ee-4f0e-8d25-cd582c2a2751.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a2248fa0-e9a8-4742-94ee-00b919cd5368.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a25a6e7a-e7fa-467b-b26e-0b80d16e9617.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a2e8690c-17ac-492e-a0e0-b56fa854f91c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a31148c2-7b4c-467f-91fc-0362b8a340f2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a339bd4d-dec7-44b9-873f-a28fc6b451bc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a3555a97-c6cc-45ed-bb72-b20061cd7e58.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a36b9b6c-f204-42fb-9cdb-e71c6207e82e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a38286fb-fbc1-45a7-ac24-1b30f49c2f9e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a3d2b5e3-2de6-42e3-bcfb-67dfe8e5ae1b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a3d2d4f0-d27a-4dff-a16e-cf8f6ab078c6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a440aa7e-13c3-47b5-a707-7c825260ab2a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a45a3767-fa8f-4450-9d3f-4b620878039d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a4a49991-8cff-4045-8cd2-c9fa5ccc6ed7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a4e3366d-7408-4a05-825b-4510bce62b4a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a5194fad-bbaa-4f01-b5b4-78f3347398b1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a5274826-976e-42ba-9cb5-439c4f780b99.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a5abe2dd-6ba4-4c54-9401-9b88fa83cf39.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a5c46de7-eeef-431a-8b35-b71fcf81d12e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a5fa0d25-4d76-444f-81e8-00028f2c07f4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a6a4b783-b5c5-40b0-8237-052c81bb7c38.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a6bdf3c4-5b6a-4eeb-8eca-bdc14058a8ae.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a6c5b19f-560b-4e98-acbf-e6646194fe2a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a6dfa64d-5f6d-4d3e-b92a-bb78fc41db89.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a6f4c62c-1ae7-4bb9-902d-8ee9b282c72d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a70a38d9-cf3e-4e5b-8501-c964ab7cf20d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a76461bb-23c8-4344-a932-34f602d873bf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a764ac61-393f-4d3d-a45d-314768f99380.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a7710298-8c29-421e-a758-2de643aec29f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a77ec1c5-4d57-439c-921d-8e116de97214.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a7b1cfec-7d6a-4a88-9c21-db876816ab34.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a7cb2ce6-4777-4e26-b420-deff47f05fdc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a7d07d48-5235-45ef-b673-a38f2289db4b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a7d3ed16-2c24-4b3e-8cdc-d36826460fae.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!a8fd042e-ce3c-446e-beca-00aef4418597.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!aa2d7528-a27a-47ca-ba4b-72fd12946f17.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!aa45208c-ebc2-428d-8af7-e893f7378169.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!aaffe828-6a01-4a5f-a081-04f7606df81a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ab61fbf0-ccfe-4ada-8772-dff5c3a02247.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ab90ed9b-50e3-4031-9393-24fe8e992fd7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!acad28d3-9f1b-442e-bc4e-dd7622f91fae.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!acca7ef4-f7d3-4b7f-8c9d-ab00dd4b6645.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ad0c5018-db28-4851-a03f-d94497e059f1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ad258585-b8b1-4a5d-a67c-c2e6d7d95be2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ad38071e-4ff5-4fbc-bb94-3f22963b44c1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ae865b74-d384-47cf-bda0-e30c42c99d2e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ae8d8426-24c3-4465-822f-8abe89e9712d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!aee0f24c-933d-459c-9a3a-a6e2bb506e05.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!af3abfac-532e-4a28-8214-3468dfa95e5d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!af40349f-0b62-474e-90e8-d5f6142245fc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!aff2e91e-3b3c-4cac-bba9-6f2876dbf441.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b078e4f2-a8f1-4792-9fa3-03f2ffbb1fc2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b0c2ebf1-94c8-449c-9c0c-ebb237de45e2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b111a966-160c-480f-b0d9-a17e14777e4a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b12bbc43-f6c0-4115-89c1-69635bc76306.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b1e32c37-a825-4f3a-9c2b-766cc0ce82d3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b241102d-a7c9-40ab-adbb-ed4b3d80e489.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b25b1552-cf53-4079-9710-20163fb0a69a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b2767de4-2c85-4923-aa2d-57997602c958.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b27f17da-3788-477e-a9d9-666316736922.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b2a5072b-65f8-4e01-acdf-4575d9707dd3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b2e0de4b-42c8-4e71-a4f3-141e77a9700c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b2eb4fdb-5e67-49ed-b181-ebc9cad9418c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b34de8cd-f3c8-4478-ace1-c10f9bff4e96.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b3acf88b-80d0-44c0-abee-6dec4fbdedbe.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b3c3ed1c-8b2a-4884-96c9-ad76bf6e7735.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b3f6d482-5e18-42de-a0f2-baa60d02ab8c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b441a095-ab08-4c8d-8771-1a678336e46d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b554a46a-e88b-411d-8362-e4b63783a693.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b5772f29-ab76-4fd8-90b6-8841dcc0c4b4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b5f6dc35-fdf4-465b-bc91-3d4dda471752.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b6df2f90-8cf9-4bb1-8ec9-71a0c89f6943.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b7bb67df-d7b5-413c-9483-34b3547127ad.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b7ffcc98-4fca-4103-8c68-8020671345f2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b800cf2f-bd31-45ce-90ef-11044a266ccb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b8d90e48-18fb-4764-b31b-4773c77ce0b3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b922d766-d56f-4800-b6fb-3c81caf0a38b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b96639ae-9710-4803-8b39-9df0ba604ee2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b972ff9e-0f0d-4ec4-9846-3a09ab769eea.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b9760753-53dc-4463-827f-7ab25328d939.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b9a089be-25ca-40b5-8a57-a69956ebd8a1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!b9fb2acf-d5f8-49f4-bcf4-3bcc576a3b62.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ba401d49-e243-43b7-9f45-c82b6ce3814a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ba4f8e33-4721-4f51-ba69-41bef6d7d8a6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ba53504e-701f-4420-9c34-75998e4f512e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bb7ba32c-d61e-42b7-b8ae-e8c4386847ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bb7de660-24fc-4d72-88b1-03203e16fc38.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bb813719-a991-4ad4-b6c4-54ea1121a0fa.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bc138419-ee0e-4ba8-ad06-7b229e104dc8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bc3ab790-1d1a-498f-b870-668c925a54de.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bc66e53d-231b-4888-a46a-4ce01002dede.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bc7bc0bc-1144-4c19-a084-1d30057d9a83.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bcc03393-28e7-43db-9650-2b79bdcef35e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bd070c4c-3406-435a-8c86-c4ec56075fff.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bd79c444-2083-490e-85f9-95f701276c1c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bd946a3d-2de3-4d86-9c7d-3250c3120271.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bdf8a01d-b98d-4e22-9035-8ef6cf97f755.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!be1db8f7-31a2-48f8-9696-af290e33c783.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!be4cf1c9-df85-40c1-868e-aaee070ee786.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!be610b60-599b-492e-8ec5-f8f3e6586c7c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf0d5113-12a6-4428-ad9f-ef42cab9fbb2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf5f197d-ff0d-49ac-b23a-71a9f24002a0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf6a8ef8-9dc8-4e6e-b373-d2823c1891f8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf7194ed-55ae-4e1a-92bd-8157c6996fb6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf7b453f-f0d3-4ebe-bd90-a9d65386a9e8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bf85b825-183a-458e-9fd3-eaf4a415fc9d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!bfda8075-aef9-4372-8c0e-971b78f31c3e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c046ab99-345e-4da0-a3c8-0b8e28c8413e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c0471a4b-bafd-4a59-9d5b-9733bae2570d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c06eea48-21ea-4ff8-9e0e-4f65c2ca27be.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c09d2c8f-3484-4711-aa25-83072d1c3e14.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c0a19744-d67b-4a63-a084-45a654dbd375.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c134c4a0-ffd1-43ad-a9c9-bf5f4a2d53cd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c1436235-896b-4e82-b146-5b5e592c497d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c18984db-4fde-4f92-bdb2-3dd6b3999e6d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c195e1d3-1885-4ebe-894a-e0904ebe926e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c1a8707b-63d7-4936-b36c-d05ac2ebaa2c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c250d917-ee64-4e90-a6d2-651f2b2a2d88.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c26c228e-7235-4c28-a843-5813a313708b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c2aca3c6-d019-4b90-a7ae-bf52243a87be.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c3001c06-6095-4a91-949e-7679ea3dc2b1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c3348754-0a43-49fc-9c6a-9d3d5919bc38.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c3621a39-9518-4245-a40e-0bc8301bccd3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c4a6bd63-fd2e-4e1a-8f4c-bffff882cfc5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c594a37d-d3ff-425c-bacd-ec27840e072f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c5bd5714-fd0a-408a-8e35-fd915039e3f8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c5d427c8-f341-498b-b223-a3181e4d063a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c5f3ce90-7bc9-4566-9122-26b76f7a5b82.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c60655c1-3c28-4abe-acbf-4a1f75d0c2b1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c60e70cb-b5d3-4a55-b901-c0674e5eeb2b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c6368aa1-bd5e-4079-bcf0-b703ef01efb7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c6afec49-f553-42a7-a1e5-649f03c1a9c4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c6d378ad-cfa5-46f5-971d-2a5c4f98a58b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c78375ba-9d68-4232-855a-dc2caff25dff.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c7902733-21c6-4766-bb64-eaeeecd4db89.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c7dccfe0-bf72-41a0-b1d6-90c2f19fecc1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c7e9ef5c-6997-4cbf-83a0-3f4989843881.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c7f047f0-2e9f-4ecb-8162-63fc1896df62.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c85ad497-2928-448e-85d7-5b79822db861.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c87c6ea4-9637-48ad-9e42-0c7ba695d282.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c8d11a3f-4490-48c9-be9d-676feae9eb93.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c8e6fc0c-3c97-44f2-9a28-ddd498ae66cf.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!c9c8f194-886f-47ba-9fe6-9ff098a4f4d6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ca867141-5304-46a8-a2b4-c067b725af37.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ca89c627-c193-4f39-8364-4238cf3cc840.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cb3632d2-c7aa-4600-a10a-b7a16fce03fb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cb82ab3d-591d-494a-9979-a2727bc5626f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cb95e28e-7611-4243-98eb-bcb5d91a1440.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cb9659d4-0c9a-442d-b61d-ae5d6f068dc3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cbb66134-f183-47d2-8147-530f332ef71e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cc4f6155-951c-4d35-ab65-a1aaef2f4a18.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cc710b9d-e9c6-43c3-8199-3a1d1565cb85.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cc781b54-415b-4ced-8fc1-83dadb298354.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ccbdcb78-905b-4a3f-bf84-b574d700d827.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cd044782-4b57-4b87-a422-69263d7f8265.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cd518557-08a0-4081-83f5-d97655d92682.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cd8acfbf-98e3-4c8e-b357-ed2e91f6dab3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cde74e59-5080-4b65-8e79-01dfba4c6d18.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ce0216dd-9102-4f47-8846-d7ebeb0dcd00.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ce6c2e13-491c-4382-a3e8-6c4c05100ac3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ce7014c2-83e0-4e99-8bb2-046bdf39f048.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ce711e5a-ff55-46ab-81ae-38e10a068802.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cec78c8d-372b-4d8e-8c8e-a57272f65076.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cf510c51-11a0-4849-a72d-fbd19eed48bd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cf5f7944-dfdc-4e66-9e9c-37efe31eb558.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cfa0fb00-d325-48af-9b8e-0b6b4511b870.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!cfc61302-0225-45d3-aaab-97482ea12090.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d045de20-8e6f-4bc4-bd08-55a26d7c7389.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d053e218-2768-4ddf-bede-ad442f09b640.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d0ed4630-bb46-4a2f-adc4-e80b0b5fff96.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d10775bf-c0b8-44b2-8f66-b08087d04a2b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d18be333-3e3e-435b-89dd-427bc6363062.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d22bee42-784c-4514-9cf8-8deb26d59a47.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d23be372-cb1b-4c05-8bd2-0ee06410b4b2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d265564e-e623-4abb-a4d8-190370fb3b38.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d26ddf63-75fc-48d2-ac42-07d671ebce7f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d30ca369-1e7d-40bb-9d0d-1c8c3660e954.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d3273daf-a9b0-4959-b260-01432187448c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d33bb1c7-ac15-4216-a505-2d76262518c4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d383a4e1-6299-46cc-8663-02a9023cf9c1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d3be721c-bac2-4e28-bfdb-08aff335c0a6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d438f303-39d9-4192-9086-9f80d2758a95.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d49393c0-b749-4021-9014-e1547d92b706.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d4a89799-91c9-48db-ad06-f3f9596be9ce.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d4bf9709-af5b-4911-803c-044c4fd55e2b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d4c36ca5-37b3-4524-88d2-8dccfb78fdce.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d539f896-49b2-4f3c-91f4-a09f3880a084.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d5ad52e6-adef-4978-ad2e-9e8752b50862.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d5dce4bf-1f4b-4749-84d8-b810cc8dd696.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d61b5f7c-12e5-4bb1-a68a-7ef989aa2c4c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d6a53b5d-bddf-4ae8-a209-886e16e72ce9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d6bacf38-0765-4261-95a2-71c10352de6c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d6c44000-0ed2-41d3-9ffa-9f15695279b1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d6d26b7e-be16-4927-acfd-e7caf6eb21bb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d6d4a170-d644-4302-80d1-8f6131713b1c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d74867ce-7807-45a8-b030-35d6e64cfbe0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d74bca5a-1521-442e-87f3-3752bce676d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d7d39ea5-a57a-4c36-99a6-8dc9a6cf3997.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d8039263-c790-42e0-9259-10161305b10c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d8c19a32-07b5-4616-bf6c-0664d2c6023d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d9118d53-0acb-47f9-9475-d5ec4f8d3bc1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d933cf65-9047-4d4f-81bc-8abcd6c338ea.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d95e5f97-1a22-4ae2-b554-5dee707b4895.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!d983e058-2c7e-4711-ad92-6331e6b822c3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!da61e076-280d-4532-b76b-df3e49a99c39.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dac02dbb-8cc7-4792-b96a-de2f6bc5375c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dae742c7-dfb0-446a-a509-58e99e25c836.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!db4f2d65-2d09-4d2e-a25b-f25b4419e87a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!db9b1115-3dc3-4144-849f-f460b51d9d75.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dbd402bd-9985-4b34-98ff-221fe63786f6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dbf45ed5-7b39-4a55-9a32-3647a375b3ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dcf54a40-5ee2-45ec-9466-6acceda82493.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ddf1bd0b-814b-4282-86b3-9be7f428218d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ddfed76b-dd25-46e4-a375-dcba0138c06f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!de5b1570-b28e-494b-83d1-416e5cdea3e3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!de5bb326-f9d5-4b47-b72b-c242dc2001f3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!decab043-01e1-4e48-b3d5-4906937c4b05.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!decc785c-12d2-4519-91b5-7ee133c243fd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ded508f6-716f-47fa-af07-1bc51e02011e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!def0c346-1d37-4fb5-94f9-6a60adfcaf28.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!df4406bf-991b-4629-80bb-81b398b6efc4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!df63f51f-0b81-499b-92e6-bec76301b2a7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!dfdab292-e4e1-4ec8-9f33-5772b72e94ec.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e0a9a21a-5531-48e0-811a-a0c500d74773.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e0ba39a4-0ff0-4d7a-a9ef-a912a7eec9a5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e1b15766-67e9-4def-97fe-f85df1534ec5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e2100b51-5cd7-4fb2-9f4a-9fa53ca36631.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e25d2c90-e67e-4de1-80ef-4c3d2fc8d390.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e25e8fe0-0255-4540-9671-97a405fe5c9a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e28ba2cb-9086-4232-84c2-23edeefc39e3.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e2c7f584-bcbe-43b7-8afb-67ce6d20c12c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e2fa5556-3258-4e0e-9c89-f864f172be90.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e2fbeb66-bd5e-40b0-a8c7-fe3aa89c9322.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e380a8d3-316b-432f-aa9c-7828f76db67c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e3a1236e-d962-4a23-8a9e-1408e3bf2a3f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e488ffdd-f384-41ca-bd3a-4b39bb94d612.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e4ae4070-6afd-4265-8346-369fa1152975.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e4b752c0-eff0-401d-a638-d04e2e3b6d9e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e4fe59c3-ea66-42ed-ae48-f79502a29089.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e511c2da-ac81-46bb-80a2-cb6c96f8bdc7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e54a68fb-35ee-463e-adc3-3860b0009430.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e57f9d10-e29b-4d35-937c-ffd3d4c1f836.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e5af2e4c-6ebe-406b-8546-ed74864bc2f6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e65f0abb-6c8e-461d-9179-63f7d12f17bd.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e6cd4d3f-8fb9-4863-ba38-ba11eda1678c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e7662a5e-a03f-4cf1-b146-24016265f243.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e784d386-7e5a-46c8-9eed-c82b7b76f8f7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e79d0c3f-b28e-436a-8548-291c44971bc0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e86555a7-663b-4015-82fc-8419d18e4dfc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e865fa7d-3ef3-43c2-b795-d7800aba783f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e8f4f551-a625-4629-a761-fc8a8c43155e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e93d0da0-2748-4684-bf03-f172d66308e7.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!e9586638-033d-4bf7-bf12-1dc34049ea68.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ea24a093-a472-489b-8e23-54969e6e5301.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ea4529ac-405a-46a0-a3da-b7b3626ff077.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ea47d3eb-792a-4ba4-97c9-69f0a3c57731.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!eabdab62-a67f-4278-832b-798b43f74084.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!eb377b96-8b92-498e-a9a0-29649def6c09.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ebb7bca0-6d0d-483c-a5da-8604b9247ce6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ebbc7459-8a49-4551-815a-0cd82d4bff76.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ebf932bb-81e8-42a1-80f4-130ca6c63579.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ec199ae7-14da-43ec-9ae2-fd642f9b494d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ec326a41-3e29-42a8-b03a-0aa67a291a6f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ecbdbee3-c469-46f0-840f-2c8defa0d489.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ecd9f0ab-3e20-457c-8f1f-21fbac6b4580.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ecdd7831-ba15-4137-93e2-14339d7b0075.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ed44b30c-87cc-44e2-b2b9-d980d1804dd2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ed889161-7f2b-4e35-a2db-172ea6bd6b8b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ed9760e9-39fa-45a3-904e-89a003b24346.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ed9d3267-95c0-4fec-b687-882c98e0a990.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!edabb2ee-09b1-460f-b718-04c6578b2530.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!edadd699-5e5e-49be-a4c9-7eee94036a03.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ee0b072d-cb98-486d-95bc-a205e861ca4b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ee5a8c9c-e03c-46f5-821d-5109413a2f67.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ee65ca66-0d4e-4a8b-a7f9-825b05a6b1d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ee96c3b4-e8dd-483e-ab60-eb2d3187332a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!eeb86305-cd5e-4a08-a573-17f3f316cc84.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!eef8b7e4-8616-41d8-88c7-0cc316fdab15.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ef30f611-473d-471a-87a2-76e5ebf87e81.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ef6ad6cd-ef80-43a4-97ae-46fec076933c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f077b013-67c4-4390-bda5-b818e4369b2d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f08a25f5-8a88-41f7-8a85-bcd166612307.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0a3f6ab-cd54-4820-9378-648c1167e803.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0ab6959-16c0-43a1-a779-1061dd46318a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0bf3c1c-0589-4007-b6cc-e70bc76f4539.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0ea32a1-2c37-44a3-8266-e2983739b247.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0ed4b69-505c-4423-ad4a-f588edf85983.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f0ee301e-1140-4b84-ac93-3ceede0f5090.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f1144f3d-ed1e-42c9-aaaa-8cfc936d4961.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f1206f9b-00ec-4479-9de4-19eb62ec2ca2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f187a57f-b9ec-4409-8533-7672b56a4437.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f1fb620c-967c-4f30-878a-e17fb1ede7ac.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f244a993-0ae3-4091-b866-7a2ef326ea73.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f2956010-fcaa-4474-947a-3d77d889e3af.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f337e441-1f89-401e-b9d5-560a7f210011.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f35a9de0-3bf9-49f7-806c-d30d192a8c7d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f3790c7f-c6d9-49bf-b2ce-636236e0d46c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f3a60a02-30a7-468b-81bf-13add7646fd8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f3fd25f4-4ec1-452a-a724-9d39c00f2904.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f443ea90-2902-44fb-bbbc-66d97e3cc67c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f4558fd1-8e24-456d-b283-902f6ff29a10.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f45b1d36-ff13-412f-b17f-a64ee194df2a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f464e756-7e8a-4b18-8785-9d5f54ac25d5.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f470fb58-ca47-41cb-acdf-a615f0b82d3f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f48693d8-60e4-49b6-820e-ad6150c26965.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f4ad97d6-f54e-4d7f-bf27-61d7a4eec94c.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f4e70d71-1c25-4407-840c-0442c50fd16b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f591cd2c-4da9-44e2-ab78-4802e06bca19.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f5dbd7a0-4fcc-4497-a23d-3d6f52c1e62b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f5fd4678-6a69-493b-9a10-d66d03bc3f4a.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f60c1baf-033d-46ba-be73-dd9cce98ad58.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f61575c7-93e2-4e91-8cd8-48a6b7c95b9e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f69039d7-90b4-42b4-9950-bfd155095924.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f69253de-b613-4026-829d-b2586ed27986.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f6984628-b745-447c-ad5a-f70b6434ebc6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f6b83ee2-2cf5-49c2-873a-22ebbd5e64d2.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f6e0edc0-0912-4242-9f8d-96df82e93ae9.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f7a624dc-40ea-4412-bf05-e5aed8ba75df.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f7e17e14-747f-4ba7-bdfc-be68d898fb53.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f865a40b-c6f7-4db2-9327-ff48a524100e.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f9623569-329b-44d7-b71e-b8afb4a9e5d8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f96597bc-ca74-410c-9eab-41b5055d7f77.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!f9a24a87-d4c7-4d36-ba82-f953ec8d5ca0.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fa291d6c-434e-4ba4-abee-b26bc573a954.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fa65f5d0-cbb3-4953-85ee-fafe5b155b2f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fa97b46a-3a11-46cb-977f-e898ac1fc544.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fb8501d7-1fda-460b-b9ae-344e976f2f6d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fb918d1b-2419-4242-adba-f947d84b0f1b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fbd80379-e48b-42c6-9936-37d42f4ad4f1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fbe34067-7807-4f3b-b228-a17cc23edb78.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fbed909d-3d2b-45fc-b05d-2daf52a3c1c4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fc2334f2-952b-4b8a-9d38-ff42f758ee64.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fc346ac3-47a9-4a56-97c1-465e582c261d.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fcabe18c-b801-4c93-a700-1c39b428957f.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fcf53b6c-9b21-480b-8a7a-7200f00e83d1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fd2fd43d-f4c7-4e0e-b31c-4a9a2232b5d6.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fd389908-05d5-4505-b350-598878fe9120.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fd9cf1a1-4bcb-437f-b481-8b85c704e7dc.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fe2b9d32-94cd-436c-97bf-bc44006fc7e1.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!fe5a6674-ac51-46a6-ba84-928ec8fdf6bb.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ff20bf31-83b5-46ef-9ae5-2c15be948c22.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ff30dfd7-0c9c-47c7-b992-4fbef3b97bed.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ff76473e-e874-424f-84b4-496ae7814131.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ff95f473-3648-45cd-886c-8c700018d78b.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffa69c54-30d9-4777-bfe9-a5a3309b60a4.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffbaeac3-6204-4714-b014-2b6e4a3d2a21.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffc46a54-d0dd-4a3c-bd21-cd89d8a31b69.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffda8b8c-baf0-4938-bda5-12d30ef39fe8.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffe1fed4-8e49-4584-8f74-a1f9a58b70ed.json" /> + <_ContentIncludedByDefault Remove="data\rooms\!ffffc7e7-4b1e-40a9-9f24-ebd95267e758.json" /> + <_ContentIncludedByDefault Remove="data\users\@emma:hse.localhost\tokens.json" /> + <_ContentIncludedByDefault Remove="data\users\@emma:hse.localhost\user.json" /> + </ItemGroup> + +</Project> diff --git a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http
index 01d5fcc..01d5fcc 100644 --- a/Tests/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http +++ b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.http
diff --git a/Tests/LibMatrix.HomeserverEmulator/Program.cs b/Utilities/LibMatrix.HomeserverEmulator/Program.cs
index 9ea6fce..c72df5a 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Program.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Program.cs
@@ -1,7 +1,6 @@ using System.Net.Mime; using System.Text.Json.Serialization; using LibMatrix; -using LibMatrix.HomeserverEmulator.Extensions; using LibMatrix.HomeserverEmulator.Services; using LibMatrix.Services; using Microsoft.AspNetCore.Diagnostics; @@ -32,7 +31,7 @@ builder.Services.AddSwaggerGen(c => { }); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); -builder.Services.AddSingleton<HSEConfiguration>(); +builder.Services.AddSingleton<HseConfiguration>(); builder.Services.AddSingleton<UserStore>(); builder.Services.AddSingleton<RoomStore>(); builder.Services.AddSingleton<MediaStore>(); @@ -77,7 +76,7 @@ app.UseExceptionHandler(exceptionHandlerApp => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); if (exceptionHandlerPathFeature?.Error is not null) - Console.WriteLine(exceptionHandlerPathFeature.Error.ToString()!); + Console.WriteLine(exceptionHandlerPathFeature.Error.ToString()); if (exceptionHandlerPathFeature?.Error is MatrixException mxe) { context.Response.StatusCode = mxe.ErrorCode switch { @@ -86,14 +85,14 @@ app.UseExceptionHandler(exceptionHandlerApp => { _ => StatusCodes.Status500InternalServerError }; context.Response.ContentType = MediaTypeNames.Application.Json; - await context.Response.WriteAsync(mxe.GetAsJson()!); + await context.Response.WriteAsync(mxe.GetAsJson()); } else { context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = MediaTypeNames.Application.Json; await context.Response.WriteAsync(new MatrixException() { ErrorCode = "M_UNKNOWN", - Error = exceptionHandlerPathFeature?.Error.ToString() + Error = exceptionHandlerPathFeature?.Error?.ToString() ?? "Unknown error" }.GetAsJson()); } }); diff --git a/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json b/Utilities/LibMatrix.HomeserverEmulator/Properties/launchSettings.json
index 4ddf341..4ddf341 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Properties/launchSettings.json +++ b/Utilities/LibMatrix.HomeserverEmulator/Properties/launchSettings.json
diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs
index 73b0d23..04ce050 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/HSEConfiguration.cs
@@ -4,12 +4,12 @@ using ArcaneLibs.Extensions; namespace LibMatrix.HomeserverEmulator.Services; -public class HSEConfiguration { - private static ILogger<HSEConfiguration> _logger; - public static HSEConfiguration Current { get; set; } +public class HseConfiguration { + private static ILogger<HseConfiguration> _logger; + public static HseConfiguration Current { get; set; } [RequiresUnreferencedCode("Uses reflection binding")] - public HSEConfiguration(ILogger<HSEConfiguration> logger, IConfiguration config, HostBuilderContext host) { + public HseConfiguration(ILogger<HseConfiguration> logger, IConfiguration config, HostBuilderContext host) { Current = this; _logger = logger; logger.LogInformation("Loading configuration for environment: {}...", host.HostingEnvironment.EnvironmentName); @@ -22,13 +22,15 @@ public class HSEConfiguration { _logger.LogInformation("Configuration loaded: {}", this.ToJson()); } - public string CacheStoragePath { get; set; } + public required string CacheStoragePath { get; set; } - public string DataStoragePath { get; set; } + public required string DataStoragePath { get; set; } - public bool StoreData { get; set; } = true; + public required bool StoreData { get; set; } = true; + + public required string ServerName { get; set; } = "localhost"; - public bool UnknownSyncTokenIsInitialSync { get; set; } = true; + public required bool UnknownSyncTokenIsInitialSync { get; set; } = true; private static string ExpandPath(string path, bool retry = true) { _logger.LogInformation("Expanding path `{}`", path); diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/MediaStore.cs
index 00f2a42..7945d3a 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/MediaStore.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/MediaStore.cs
@@ -4,18 +4,18 @@ using LibMatrix.Services; namespace LibMatrix.HomeserverEmulator.Services; public class MediaStore { - private readonly HSEConfiguration _config; + private readonly HseConfiguration _config; private readonly HomeserverResolverService _hsResolver; - private List<MediaInfo> index = new(); + private List<MediaInfo> _mediaIndex = new(); - public MediaStore(HSEConfiguration config, HomeserverResolverService hsResolver) { + public MediaStore(HseConfiguration config, HomeserverResolverService hsResolver) { _config = config; _hsResolver = hsResolver; if (config.StoreData) { var path = Path.Combine(config.DataStoragePath, "media"); if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (File.Exists(Path.Combine(path, "index.json"))) - index = JsonSerializer.Deserialize<List<MediaInfo>>(File.ReadAllText(Path.Combine(path, "index.json"))); + _mediaIndex = JsonSerializer.Deserialize<List<MediaInfo>>(File.ReadAllText(Path.Combine(path, "index.json"))); } else Console.WriteLine("Data storage is disabled, not loading rooms from disk"); @@ -36,7 +36,9 @@ public class MediaStore { if (_config.StoreData) { var path = Path.Combine(_config.DataStoragePath, "media", serverName, mediaId); if (!File.Exists(path)) { - var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + // var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + var homeserver = (await _hsResolver.ResolveHomeserverFromWellKnown(serverName)).Client; + var mediaUrl = homeserver is null ? null : $"{homeserver}/_matrix/media/v3/download/"; if (mediaUrl is null) throw new MatrixException() { ErrorCode = "M_NOT_FOUND", @@ -50,7 +52,9 @@ public class MediaStore { return new FileStream(path, FileMode.Open); } else { - var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + // var mediaUrl = await _hsResolver.ResolveMediaUri(serverName, $"mxc://{serverName}/{mediaId}"); + var homeserver = (await _hsResolver.ResolveHomeserverFromWellKnown(serverName)).Client; + var mediaUrl = homeserver is null ? null : $"{homeserver}/_matrix/media/v3/download/"; if (mediaUrl is null) throw new MatrixException() { ErrorCode = "M_NOT_FOUND", diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs
index 0128ba6..0603a2d 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/PaginationTokenResolverService.cs
@@ -35,15 +35,15 @@ public class PaginationTokenResolverService(ILogger<PaginationTokenResolverServi } } - public async Task<StateEventResponse?> ResolveTokenToEvent(string token, RoomStore.Room room) { + public Task<StateEventResponse?> ResolveTokenToEvent(string token, RoomStore.Room room) { if (token.StartsWith('$')) { //we have an event ID logger.LogTrace("ResolveTokenToEvent(EventId({token}), Room({room})): searching for event...", token, room.RoomId); var evt = room.Timeline.SingleOrDefault(x => x.EventId == token); - if (evt is not null) return evt; + if (evt is not null) return Task.FromResult(evt); logger.LogTrace("ResolveTokenToEvent({token}, Room({room})): event not in requested room...", token, room.RoomId); - return null; + return Task.FromResult<StateEventResponse?>(null); } else { // we have a sync token diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/RoomStore.cs
index 5cdc3ab..b6fe7c2 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/RoomStore.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/RoomStore.cs
@@ -2,13 +2,12 @@ using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Text.Json; using System.Text.Json.Nodes; using ArcaneLibs; using ArcaneLibs.Collections; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.HomeserverEmulator.Controllers.Rooms; using LibMatrix.Responses; @@ -19,14 +18,14 @@ public class RoomStore { public ConcurrentBag<Room> _rooms = new(); private FrozenDictionary<string, Room> _roomsById = FrozenDictionary<string, Room>.Empty; - public RoomStore(ILogger<RoomStore> logger, HSEConfiguration config) { + public RoomStore(ILogger<RoomStore> logger, HseConfiguration config) { _logger = logger; if (config.StoreData) { var path = Path.Combine(config.DataStoragePath, "rooms"); if (!Directory.Exists(path)) Directory.CreateDirectory(path); foreach (var file in Directory.GetFiles(path)) { var room = JsonSerializer.Deserialize<Room>(File.ReadAllText(file)); - if (room is not null) _rooms.Add(room); + if (room is not null && room.State.Any(x => x.Type == RoomCreateEventContent.EventId)) _rooms.Add(room); } } else @@ -35,25 +34,8 @@ public class RoomStore { RebuildIndexes(); } - private SemaphoreSlim a = new(1, 1); private void RebuildIndexes() { - // a.Wait(); - // lock (_roomsById) - // _roomsById = new ConcurrentDictionary<string, Room>(_rooms.ToDictionary(u => u.RoomId)); - // foreach (var room in _rooms) { - // _roomsById.AddOrUpdate(room.RoomId, room, (key, old) => room); - // } - // - // var roomsArr = _rooms.ToArray(); - // foreach (var (id, room) in _roomsById) { - // if (!roomsArr.Any(x => x.RoomId == id)) - // _roomsById.TryRemove(id, out _); - // } - - // _roomsById = new ConcurrentDictionary<string, Room>(_rooms.ToDictionary(u => u.RoomId)); _roomsById = _rooms.ToFrozenDictionary(u => u.RoomId); - - // a.Release(); } public Room? GetRoomById(string roomId, bool createIfNotExists = false) { @@ -64,18 +46,19 @@ public class RoomStore { if (!createIfNotExists) return null; - return CreateRoom(new() { }); + return CreateRoom(new()); } public Room CreateRoom(CreateRoomRequest request, UserStore.User? user = null) { var room = new Room(roomId: $"!{Guid.NewGuid().ToString()}"); var newCreateEvent = new StateEvent() { Type = RoomCreateEventContent.EventId, - RawContent = new() { } + RawContent = new() }; foreach (var (key, value) in request.CreationContent) { newCreateEvent.RawContent[key] = value.DeepClone(); + Console.WriteLine($"RawContent[{key}] = {value.DeepClone().ToJson(ignoreNull: true)}"); } if (user != null) { @@ -137,7 +120,7 @@ public class RoomStore { public Room(string roomId) { if (string.IsNullOrWhiteSpace(roomId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(roomId)); - if (roomId[0] != '!') throw new ArgumentException("Room ID must start with !", nameof(roomId)); + if (roomId[0] != '!') throw new ArgumentException($"Room ID must start with '!', provided value: {roomId ?? "null"}", nameof(roomId)); RoomId = roomId; Timeline = new(); AccountData = new(); @@ -207,7 +190,7 @@ public class RoomStore { : JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(request.TypedContent))) }; Timeline.Add(state); - if(state.StateKey != null) + if (state.StateKey != null) RebuildState(); return state; } @@ -226,32 +209,36 @@ public class RoomStore { } // public async Task SaveDebounced() { - // if (!HSEConfiguration.Current.StoreData) return; - // await _debounceCts.CancelAsync(); - // _debounceCts = new CancellationTokenSource(); - // try { - // await Task.Delay(250, _debounceCts.Token); - // // Ensure all state events are in the timeline - // State.Where(s => !Timeline.Contains(s)).ToList().ForEach(s => Timeline.Add(s)); - // var path = Path.Combine(HSEConfiguration.Current.DataStoragePath, "rooms", $"{RoomId}.json"); - // Console.WriteLine($"Saving room {RoomId} to {path}!"); - // await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true)); - // } - // catch (TaskCanceledException) { } + // if (!HSEConfiguration.Current.StoreData) return; + // await _debounceCts.CancelAsync(); + // _debounceCts = new CancellationTokenSource(); + // try { + // await Task.Delay(250, _debounceCts.Token); + // // Ensure all state events are in the timeline + // State.Where(s => !Timeline.Contains(s)).ToList().ForEach(s => Timeline.Add(s)); + // var path = Path.Combine(HSEConfiguration.Current.DataStoragePath, "rooms", $"{RoomId}.json"); + // Console.WriteLine($"Saving room {RoomId} to {path}!"); + // await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true)); + // } + // catch (TaskCanceledException) { } // } private SemaphoreSlim saveSemaphore = new(1, 1); + private CancellationTokenSource _saveCts = new(); + public async Task SaveDebounced() { Task.Run(async () => { - await saveSemaphore.WaitAsync(); + // await saveSemaphore.WaitAsync(); + await _saveCts.CancelAsync(); + _saveCts = new(); try { - var path = Path.Combine(HSEConfiguration.Current.DataStoragePath, "rooms", $"{RoomId}.json"); - Console.WriteLine($"Saving room {RoomId} to {path}!"); - await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true)); + var path = Path.Combine(HseConfiguration.Current.DataStoragePath, "rooms", $"{RoomId}.json"); + // Console.WriteLine($"Saving room {RoomId} to {path}!"); + await File.WriteAllTextAsync(path, this.ToJson(ignoreNull: true), _saveCts.Token); } finally { - saveSemaphore.Release(); + // saveSemaphore.Release(); } }); } diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/TokenService.cs
index cf79aae..cf79aae 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/TokenService.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/TokenService.cs
diff --git a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs
index 4ce9f92..7f211e3 100644 --- a/Tests/LibMatrix.HomeserverEmulator/Services/UserStore.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Services/UserStore.cs
@@ -5,7 +5,6 @@ using System.Text.Json.Nodes; using ArcaneLibs; using ArcaneLibs.Collections; using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.Filters; using LibMatrix.Responses; @@ -13,12 +12,14 @@ namespace LibMatrix.HomeserverEmulator.Services; public class UserStore { public ConcurrentBag<User> _users = new(); + private readonly HseConfiguration _config; private readonly RoomStore _roomStore; - public UserStore(HSEConfiguration config, RoomStore roomStore) { + public UserStore(HseConfiguration config, RoomStore roomStore) { + _config = config; _roomStore = roomStore; if (config.StoreData) { - var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users"); + var dataDir = Path.Combine(HseConfiguration.Current.DataStoragePath, "users"); if (!Directory.Exists(dataDir)) Directory.CreateDirectory(dataDir); foreach (var userId in Directory.GetDirectories(dataDir)) { var tokensDir = Path.Combine(dataDir, userId, "tokens.json"); @@ -64,12 +65,15 @@ public class UserStore { }; } - public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null) { + public async Task<User> CreateUser(string userId, Dictionary<string, object>? profile = null, string kind = "user") { profile ??= new(); - if (!profile.ContainsKey("displayname")) profile.Add("displayname", userId.Split(":")[0]); + var parts = userId.Split(":"); + var localPart = parts[0].TrimStart('@'); + if (!profile.ContainsKey("displayname")) profile.Add("displayname", localPart); if (!profile.ContainsKey("avatar_url")) profile.Add("avatar_url", null); var user = new User() { - UserId = userId, + UserId = $"@{localPart}:{_config.ServerName}", + IsGuest = kind == "guest", AccountData = new() { new StateEventResponse() { Type = "im.vector.analytics", @@ -80,7 +84,22 @@ public class UserStore { new StateEventResponse() { Type = "im.vector.web.settings", RawContent = new JsonObject() { - ["developerMode"] = true + ["developerMode"] = true, + ["alwaysShowTimestamps"] = true, + ["SpotlightSearch.showNsfwPublicRooms"] = true, + + } + }, + new() { + Type = "im.vector.setting.integration_provisioning", + RawContent = new JsonObject() { + ["enabled"] = false + } + }, + new() { + Type = "m.identity_server", + RawContent = new JsonObject() { + ["base_url"] = null } }, } @@ -118,7 +137,7 @@ public class UserStore { private ObservableDictionary<string, object> _profile; private ObservableCollection<StateEventResponse> _accountData; private ObservableDictionary<string, RoomKeysResponse> _roomKeys; - private ObservableDictionary<string, AuthorizedSession> _authorizedSessions; + private ObservableDictionary<string, LoginResponse> _authorizedSessions; public string UserId { get => _userId; @@ -175,7 +194,7 @@ public class UserStore { } } - public ObservableDictionary<string, AuthorizedSession> AuthorizedSessions { + public ObservableDictionary<string, LoginResponse> AuthorizedSessions { get => _authorizedSessions; set { if (value == _authorizedSessions) return; @@ -185,13 +204,15 @@ public class UserStore { } } + public bool IsGuest { get; set; } + public async Task SaveDebounced() { - if (!HSEConfiguration.Current.StoreData) return; + if (!HseConfiguration.Current.StoreData) return; await _debounceCts.CancelAsync(); _debounceCts = new CancellationTokenSource(); try { await Task.Delay(250, _debounceCts.Token); - var dataDir = Path.Combine(HSEConfiguration.Current.DataStoragePath, "users", _userId); + var dataDir = Path.Combine(HseConfiguration.Current.DataStoragePath, "users", _userId); if (!Directory.Exists(dataDir)) Directory.CreateDirectory(dataDir); var tokensDir = Path.Combine(dataDir, "tokens.json"); var path = Path.Combine(dataDir, $"user.json"); @@ -205,6 +226,7 @@ public class UserStore { public class SessionInfo { public string DeviceId { get; set; } = Guid.NewGuid().ToString(); + public string DeviceName { get; set; } = "Unnamed device"; public Dictionary<string, UserSyncState> SyncStates { get; set; } = new(); public class UserSyncState { @@ -241,10 +263,5 @@ public class UserStore { UserId = UserId }; } - - public class AuthorizedSession { - public string Homeserver { get; set; } - public string AccessToken { get; set; } - } } } \ No newline at end of file diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json b/Utilities/LibMatrix.HomeserverEmulator/appsettings.Development.json
index f14522d..f14522d 100644 --- a/Tests/LibMatrix.HomeserverEmulator/appsettings.Development.json +++ b/Utilities/LibMatrix.HomeserverEmulator/appsettings.Development.json
diff --git a/Tests/LibMatrix.HomeserverEmulator/appsettings.json b/Utilities/LibMatrix.HomeserverEmulator/appsettings.json
index b16968a..b16968a 100644 --- a/Tests/LibMatrix.HomeserverEmulator/appsettings.json +++ b/Utilities/LibMatrix.HomeserverEmulator/appsettings.json
diff --git a/Utilities/LibMatrix.JsonSerializerContextGenerator/LibMatrix.JsonSerializerContextGenerator.csproj b/Utilities/LibMatrix.JsonSerializerContextGenerator/LibMatrix.JsonSerializerContextGenerator.csproj
index 35cb9f8..486b6b6 100644 --- a/Utilities/LibMatrix.JsonSerializerContextGenerator/LibMatrix.JsonSerializerContextGenerator.csproj +++ b/Utilities/LibMatrix.JsonSerializerContextGenerator/LibMatrix.JsonSerializerContextGenerator.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <OutputType>exe</OutputType> diff --git a/Utilities/LibMatrix.JsonSerializerContextGenerator/Program.cs b/Utilities/LibMatrix.JsonSerializerContextGenerator/Program.cs
index f33efeb..21c73ef 100644 --- a/Utilities/LibMatrix.JsonSerializerContextGenerator/Program.cs +++ b/Utilities/LibMatrix.JsonSerializerContextGenerator/Program.cs
@@ -7,7 +7,7 @@ using LibMatrix.EventTypes; // var asm = Assembly.LoadFrom(binary); File.Delete("EventSerializerContexts.g.cs"); var stream = File.OpenWrite("EventSerializerContexts.g.cs"); -var eventContentTypes = new ClassCollector<EventContent>().ResolveFromAllAccessibleAssemblies(); +var eventContentTypes = ClassCollector<EventContent>.ResolveFromAllAccessibleAssemblies(); stream.WriteString("using System.Text.Json.Serialization;\n"); diff --git a/Tests/TestDataGenerator/Bot/DataFetcher.cs b/Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcher.cs
index 66b8a03..66b8a03 100644 --- a/Tests/TestDataGenerator/Bot/DataFetcher.cs +++ b/Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcher.cs
diff --git a/Tests/TestDataGenerator/Bot/DataFetcherConfiguration.cs b/Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcherConfiguration.cs
index 4f53a2a..4f53a2a 100644 --- a/Tests/TestDataGenerator/Bot/DataFetcherConfiguration.cs +++ b/Utilities/LibMatrix.TestDataGenerator/Bot/DataFetcherConfiguration.cs
diff --git a/Tests/TestDataGenerator/TestDataGenerator.csproj b/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj
index 879693e..b054aba 100644 --- a/Tests/TestDataGenerator/TestDataGenerator.csproj +++ b/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj
@@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> @@ -17,7 +17,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1"/> </ItemGroup> <ItemGroup> <Content Include="appsettings*.json"> @@ -27,6 +27,6 @@ <ItemGroup> <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj"/> <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj"/> - <ProjectReference Include="..\LibMatrix.Tests\LibMatrix.Tests.csproj"/> + <ProjectReference Include="..\..\Tests\LibMatrix.Tests\LibMatrix.Tests.csproj"/> </ItemGroup> </Project> diff --git a/Tests/TestDataGenerator/Program.cs b/Utilities/LibMatrix.TestDataGenerator/Program.cs
index 2583817..f3750a8 100644 --- a/Tests/TestDataGenerator/Program.cs +++ b/Utilities/LibMatrix.TestDataGenerator/Program.cs
@@ -2,6 +2,7 @@ using LibMatrix.Services; using LibMatrix.Utilities.Bot; +using LibMatrix.Utilities.Bot.AppServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using TestDataGenerator.Bot; diff --git a/Tests/TestDataGenerator/Properties/launchSettings.json b/Utilities/LibMatrix.TestDataGenerator/Properties/launchSettings.json
index 6c504ff..6c504ff 100644 --- a/Tests/TestDataGenerator/Properties/launchSettings.json +++ b/Utilities/LibMatrix.TestDataGenerator/Properties/launchSettings.json
diff --git a/Tests/TestDataGenerator/appsettings.Development.json b/Utilities/LibMatrix.TestDataGenerator/appsettings.Development.json
index 38c45c4..38c45c4 100644 --- a/Tests/TestDataGenerator/appsettings.Development.json +++ b/Utilities/LibMatrix.TestDataGenerator/appsettings.Development.json
diff --git a/Tests/TestDataGenerator/appsettings.json b/Utilities/LibMatrix.TestDataGenerator/appsettings.json
index e203e94..e203e94 100644 --- a/Tests/TestDataGenerator/appsettings.json +++ b/Utilities/LibMatrix.TestDataGenerator/appsettings.json
diff --git a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs
index afda89e..6dc76f6 100644 --- a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs +++ b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs
@@ -1,24 +1,48 @@ -namespace LibMatrix.Utilities.Bot; +using System.Text.Json.Serialization; + +namespace LibMatrix.Utilities.Bot.AppServices; public class AppServiceConfiguration { - public string Id { get; set; } = null!; - public string? Url { get; set; } = null!; - public string SenderLocalpart { get; set; } = null!; - public string AppserviceToken { get; set; } = null!; - public string HomeserverToken { get; set; } = null!; - public List<string>? Protocols { get; set; } = null!; - public bool? RateLimited { get; set; } = null!; + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("sender_localpart")] + public string SenderLocalpart { get; set; } + + [JsonPropertyName("as_token")] + public string AppserviceToken { get; set; } + + [JsonPropertyName("hs_token")] + public string HomeserverToken { get; set; } - public AppserviceNamespaces Namespaces { get; set; } = null!; + [JsonPropertyName("protocols")] + public List<string>? Protocols { get; set; } + + [JsonPropertyName("rate_limited")] + public bool? RateLimited { get; set; } + + [JsonPropertyName("namespaces")] + public AppserviceNamespaces Namespaces { get; set; } public class AppserviceNamespaces { + [JsonPropertyName("users")] public List<AppserviceNamespace>? Users { get; set; } = null; + + [JsonPropertyName("aliases")] public List<AppserviceNamespace>? Aliases { get; set; } = null; + + [JsonPropertyName("rooms")] public List<AppserviceNamespace>? Rooms { get; set; } = null; public class AppserviceNamespace { + [JsonPropertyName("exclusive")] public bool Exclusive { get; set; } - public string Regex { get; set; } = null!; + + [JsonPropertyName("regex")] + public string Regex { get; set; } } } diff --git a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs deleted file mode 100644
index 215f28a..0000000 --- a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs +++ /dev/null
@@ -1,68 +0,0 @@ -using ArcaneLibs; -using LibMatrix.EventTypes.Spec.State; -using LibMatrix.Homeservers; -using LibMatrix.Responses; -using LibMatrix.Services; -using LibMatrix.Utilities.Bot.Interfaces; -using LibMatrix.Utilities.Bot.Services; -using Microsoft.Extensions.DependencyInjection; - -namespace LibMatrix.Utilities.Bot; - -public static class BotCommandInstaller { - public static BotInstaller AddMatrixBot(this IServiceCollection services) { - return new BotInstaller(services).AddMatrixBot(); - } -} - -public class BotInstaller(IServiceCollection services) { - public BotInstaller AddMatrixBot() { - services.AddSingleton<LibMatrixBotConfiguration>(); - - services.AddScoped<AuthenticatedHomeserverGeneric>(x => { - var config = x.GetService<LibMatrixBotConfiguration>() ?? throw new Exception("No configuration found!"); - var hsProvider = x.GetService<HomeserverProviderService>() ?? throw new Exception("No homeserver provider found!"); - var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; - - return hs; - }); - - return this; - } - - public BotInstaller AddCommandHandler() { - Console.WriteLine("Adding command handler..."); - services.AddHostedService<CommandListenerHostedService>(); - return this; - } - - public BotInstaller DiscoverAllCommands() { - foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) { - Console.WriteLine($"Adding command {commandClass.Name}"); - services.AddScoped(typeof(ICommand), commandClass); - } - - return this; - } - public BotInstaller AddCommands(IEnumerable<Type> commandClasses) { - foreach (var commandClass in commandClasses) { - if(!commandClass.IsAssignableTo(typeof(ICommand))) - throw new Exception($"Type {commandClass.Name} is not assignable to ICommand!"); - Console.WriteLine($"Adding command {commandClass.Name}"); - services.AddScoped(typeof(ICommand), commandClass); - } - - return this; - } - - public BotInstaller WithInviteHandler(Func<InviteHandlerHostedService.InviteEventArgs, Task> inviteHandler) { - services.AddSingleton(inviteHandler); - services.AddHostedService<InviteHandlerHostedService>(); - return this; - } - - public BotInstaller WithCommandResultHandler(Func<CommandResult, Task> commandResultHandler) { - services.AddSingleton(commandResultHandler); - return this; - } -} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/BotServiceInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotServiceInstaller.cs new file mode 100644
index 0000000..ff0bc9e --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/BotServiceInstaller.cs
@@ -0,0 +1,98 @@ +using ArcaneLibs; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.AppServices; +using LibMatrix.Utilities.Bot.Interfaces; +using LibMatrix.Utilities.Bot.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace LibMatrix.Utilities.Bot; + +public static class BotServiceInstallerExtensions { + public static BotServiceInstaller AddMatrixBot(this IServiceCollection services) { + return new BotServiceInstaller(services).AddMatrixBot(); + } +} + +public class BotServiceInstaller(IServiceCollection services) { + public BotServiceInstaller AddMatrixBot() { + services.AddSingleton<LibMatrixBotConfiguration>(); + + services.AddSingleton<AuthenticatedHomeserverGeneric>(x => { + var config = x.GetService<LibMatrixBotConfiguration>() ?? throw new Exception("No configuration found!"); + var hsProvider = x.GetService<HomeserverProviderService>() ?? throw new Exception("No homeserver provider found!"); + + if (x.GetService<AppServiceConfiguration>() is AppServiceConfiguration appsvcConfig) + config.AccessToken = appsvcConfig.AppserviceToken; + else if (Environment.GetEnvironmentVariable("LIBMATRIX_ACCESS_TOKEN_PATH") is string path) + config.AccessTokenPath = path; + + if (string.IsNullOrWhiteSpace(config.AccessToken) && string.IsNullOrWhiteSpace(config.AccessTokenPath)) + throw new Exception("Unable to add bot service without an access token or access token path!"); + + if (!string.IsNullOrWhiteSpace(config.AccessTokenPath)) { + var token = File.ReadAllText(config.AccessTokenPath); + config.AccessToken = token.Trim(); + } + + var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; + + return hs; + }); + + return this; + } + + public BotServiceInstaller AddCommandHandler() { + Console.WriteLine("Adding command handler..."); + services.AddSingleton(s => s.GetRequiredService<LibMatrixBotConfiguration>().CommandListener + ?? throw new Exception("Command handling is enabled, but configuration is missing the LibMatrixBot:CommandListener configuration section!") + ); + services.AddHostedService<CommandListenerHostedService>(); + return this; + } + + public BotServiceInstaller DiscoverAllCommands() { + foreach (var commandClass in ClassCollector<ICommand>.ResolveFromAllAccessibleAssemblies()) { + Console.WriteLine($"Adding command {commandClass.Name}"); + services.AddScoped(typeof(ICommand), commandClass); + } + + return this; + } + + public BotServiceInstaller AddCommands(IEnumerable<Type> commandClasses) { + foreach (var commandClass in commandClasses) { + if (!commandClass.IsAssignableTo(typeof(ICommand))) + throw new Exception($"Type {commandClass.Name} is not assignable to ICommand!"); + Console.WriteLine($"Adding command {commandClass.Name}"); + services.AddScoped(typeof(ICommand), commandClass); + } + + return this; + } + + public BotServiceInstaller WithCommandResultHandler(Func<CommandResult, Task> commandResultHandler) { + services.AddSingleton(commandResultHandler); + return this; + } + + public BotServiceInstaller WithInviteHandler(Func<RoomInviteContext, Task> inviteHandler) { + services.AddSingleton(inviteHandler); + services.AddSingleton(s => s.GetRequiredService<LibMatrixBotConfiguration>().InviteListener + ?? throw new Exception("Invite handling is enabled, but configuration is missing the LibMatrixBot:InviteListener configuration section!") + ); + services.AddHostedService<InviteHandlerHostedService>(); + return this; + } + + public BotServiceInstaller WithInviteHandler<T>() where T : class, IRoomInviteHandler { + services.AddSingleton<T>(); + services.AddSingleton<Func<RoomInviteContext, Task>>(sp => sp.GetRequiredService<T>().HandleInviteAsync); + services.AddSingleton(s => s.GetRequiredService<LibMatrixBotConfiguration>().InviteListener + ?? throw new Exception("Invite handling is enabled, but configuration is missing the LibMatrixBot:InviteListener configuration section!") + ); + services.AddHostedService<InviteHandlerHostedService>(); + return this; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Commands/AliassesCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/AliassesCommand.cs
index 5c9c480..9107c4c 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Commands/AliassesCommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/AliassesCommand.cs
@@ -1,4 +1,3 @@ -using System.Collections.Frozen; using System.Text; using LibMatrix.EventTypes.Spec; using LibMatrix.Helpers; diff --git a/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs
index 0abc76b..f29d6e9 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs
@@ -1,6 +1,3 @@ -using System.Collections.Frozen; -using System.Text; -using LibMatrix.EventTypes.Spec; using LibMatrix.Helpers; using LibMatrix.Utilities.Bot.Interfaces; using Microsoft.Extensions.DependencyInjection; @@ -9,7 +6,7 @@ namespace LibMatrix.Utilities.Bot.Commands; public class HelpCommand(IServiceProvider services) : ICommand { public string Name { get; } = "help"; - public string[]? Aliases { get; } = new[] { "?" }; + public string[]? Aliases { get; } = ["?"]; public string Description { get; } = "Displays this help message"; public bool Unlisted { get; } diff --git a/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs
index 76e48f5..ff41b70 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs
@@ -5,9 +5,29 @@ namespace LibMatrix.Utilities.Bot.Commands; public class PingCommand : ICommand { public string Name { get; } = "ping"; - public string[]? Aliases { get; } = [ ]; + public string[]? Aliases { get; } = []; public string Description { get; } = "Pong!"; public bool Unlisted { get; } - public async Task Invoke(CommandContext ctx) => await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: "pong!")); + public async Task Invoke(CommandContext ctx) { + var latency = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - ctx.MessageEvent.OriginServerTs; + await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent(body: $"Pong! ({latency} ms)") { + AdditionalData = new() { + // maubot ping compatibility + ["pong"] = new { + ms = latency, + from = ctx.Homeserver.ServerName, + ping = ctx.MessageEvent.EventId + }, + }, + RelatesTo = new() { + RelationType = "xyz.maubot.pong", + EventId = ctx.MessageEvent.EventId, + AdditionalData = new() { + ["ms"] = latency!, + ["from"] = ctx.Homeserver.ServerName + } + } + }); + } } \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Configuration/CommandListenerConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/Configuration/CommandListenerConfiguration.cs new file mode 100644
index 0000000..e3026cd --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Configuration/CommandListenerConfiguration.cs
@@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; +using LibMatrix.Filters; + +namespace LibMatrix.Utilities.Bot.Configuration; + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Configuration")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Configuration")] +public class CommandListenerSyncConfiguration { + // public SyncFilter? Filter { get; set; } + public TimeSpan? MinimumSyncTime { get; set; } + public int? Timeout { get; set; } + public string? Presence { get; set; } + // public bool InitialSyncOnStartup { get; set; } +} + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Configuration")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Configuration")] +public class CommandListenerConfiguration { + public CommandListenerSyncConfiguration SyncConfiguration { get; set; } = new(); + + public required List<string> Prefixes { get; set; } + public bool MentionPrefix { get; set; } + public bool SelfCommandsOnly { get; set; } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Configuration/InviteListenerConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/Configuration/InviteListenerConfiguration.cs new file mode 100644
index 0000000..7fce400 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Configuration/InviteListenerConfiguration.cs
@@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using LibMatrix.Filters; + +namespace LibMatrix.Utilities.Bot.Configuration; + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Configuration")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Configuration")] +public class InviteListenerSyncConfiguration { + public SyncFilter? Filter { get; set; } + public TimeSpan? MinimumSyncTime { get; set; } + public int? Timeout { get; set; } + public string? Presence { get; set; } + public bool InitialSyncOnStartup { get; set; } +} + +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Configuration")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global", Justification = "Configuration")] +public class InviteListenerConfiguration { + public InviteListenerSyncConfiguration SyncConfiguration { get; set; } = new(); +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Configuration/LibMatrixBotConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/Configuration/LibMatrixBotConfiguration.cs new file mode 100644
index 0000000..cd272e0 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Configuration/LibMatrixBotConfiguration.cs
@@ -0,0 +1,17 @@ +using LibMatrix.Utilities.Bot.Configuration; +using Microsoft.Extensions.Configuration; + +namespace LibMatrix.Utilities.Bot; + +public class LibMatrixBotConfiguration { + public LibMatrixBotConfiguration(IConfiguration config) => config.GetRequiredSection("LibMatrixBot").Bind(this); + public string Homeserver { get; set; } + public string? AccessToken { get; set; } + public string? AccessTokenPath { get; set; } + public string? LogRoom { get; set; } + + public string? Presence { get; set; } + + public InviteListenerConfiguration? InviteListener { get; set; } + public CommandListenerConfiguration? CommandListener { get; set; } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs
index c6abde2..71ecbed 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs
@@ -1,5 +1,6 @@ using LibMatrix.EventTypes.Spec; using LibMatrix.Homeservers; +using LibMatrix.Responses; using LibMatrix.RoomTypes; namespace LibMatrix.Utilities.Bot.Interfaces; diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs
index 941d69e..7b4afa9 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs
@@ -1,7 +1,3 @@ -using System.Collections.Frozen; -using System.Collections.Immutable; -using Microsoft.Extensions.DependencyInjection; - namespace LibMatrix.Utilities.Bot.Interfaces; public interface ICommand { diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/IRoomInviteHandler.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/IRoomInviteHandler.cs new file mode 100644
index 0000000..a25ca4a --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/IRoomInviteHandler.cs
@@ -0,0 +1,5 @@ +namespace LibMatrix.Utilities.Bot.Interfaces; + +public interface IRoomInviteHandler { + public Task HandleInviteAsync(RoomInviteContext invite); +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/RoomInviteContext.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/RoomInviteContext.cs new file mode 100644
index 0000000..380c1c7 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/RoomInviteContext.cs
@@ -0,0 +1,71 @@ +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Homeservers; +using LibMatrix.Responses; + +namespace LibMatrix.Utilities.Bot.Interfaces; + +public class RoomInviteContext { + public required string RoomId { get; init; } + public required AuthenticatedHomeserverGeneric Homeserver { get; init; } + public required StateEventResponse MemberEvent { get; init; } + public required SyncResponse.RoomsDataStructure.InvitedRoomDataStructure InviteData { get; init; } + + public async Task<string> TryGetInviterNameAsync() { + var name = InviteData.InviteState?.Events? + .FirstOrDefault(evt => evt is { Type: RoomMemberEventContent.EventId } && evt.StateKey == MemberEvent.Sender)? + .ContentAs<RoomMemberEventContent>()?.DisplayName; + + if (!string.IsNullOrWhiteSpace(name)) + return name; + + try { + await Homeserver.GetProfileAsync(MemberEvent.Sender!); + } + catch { + //ignored + } + + return MemberEvent.Sender!; + } + + public async Task<string> TryGetRoomNameAsync() { + // try to get room name from invite state + var name = InviteData.InviteState?.Events? + .FirstOrDefault(evt => evt is { Type: RoomNameEventContent.EventId, StateKey: "" })? + .ContentAs<RoomNameEventContent>()?.Name; + + if (!string.IsNullOrWhiteSpace(name)) + return name; + + // try to get room alias + var alias = InviteData.InviteState?.Events? + .FirstOrDefault(evt => evt is { Type: RoomCanonicalAliasEventContent.EventId, StateKey: "" })? + .ContentAs<RoomCanonicalAliasEventContent>()?.Alias; + + if (!string.IsNullOrWhiteSpace(alias)) + return alias; + + // try to get room name via public previews + try { + name = await Homeserver.GetRoom(RoomId).GetNameOrFallbackAsync(); + if (name != RoomId && !string.IsNullOrWhiteSpace(name)) + return name; + } + catch { + //ignored + } + + // fallback to room alias via public previews + try { + alias = (await Homeserver.GetRoom(RoomId).GetCanonicalAliasAsync())?.Alias; + if (!string.IsNullOrWhiteSpace(alias)) + return alias; + } + catch { + //ignored + } + + // fall back to room ID + return RoomId; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj
index 6e67373..bbb0a65 100644 --- a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj +++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>preview</LangVersion> @@ -12,9 +12,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" /> </ItemGroup> diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs deleted file mode 100644
index 245442f..0000000 --- a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs +++ /dev/null
@@ -1,13 +0,0 @@ -using LibMatrix.Utilities.Bot.Interfaces; -using Microsoft.Extensions.Configuration; - -namespace LibMatrix.Utilities.Bot; - -public class LibMatrixBotConfiguration { - public LibMatrixBotConfiguration(IConfiguration config) => config.GetRequiredSection("LibMatrixBot").Bind(this); - public string Homeserver { get; set; } - public string AccessToken { get; set; } - public List<string> Prefixes { get; set; } - public bool MentionPrefix { get; set; } - public string? LogRoom { get; set; } -} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs
index 601e598..d0a93a4 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs
@@ -1,10 +1,11 @@ -using System.Reflection.Metadata; +using System.Collections.Frozen; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; -using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Filters; using LibMatrix.Helpers; using LibMatrix.Homeservers; +using LibMatrix.Utilities.Bot.Configuration; using LibMatrix.Utilities.Bot.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -12,93 +13,119 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Utilities.Bot.Services; -public class CommandListenerHostedService : IHostedService { - private readonly AuthenticatedHomeserverGeneric _hs; - private readonly ILogger<CommandListenerHostedService> _logger; - private readonly IEnumerable<ICommand> _commands; - private readonly LibMatrixBotConfiguration _config; - private readonly Func<CommandResult, Task>? _commandResultHandler; +public class CommandListenerHostedService( + AuthenticatedHomeserverGeneric hs, + ILogger<CommandListenerHostedService> logger, + IServiceProvider services, + LibMatrixBotConfiguration botConfig, + CommandListenerConfiguration config, + Func<CommandResult, Task>? commandResultHandler = null +) + : IHostedService { + private FrozenSet<ICommand> _commands = null!; private Task? _listenerTask; - - public CommandListenerHostedService(AuthenticatedHomeserverGeneric hs, ILogger<CommandListenerHostedService> logger, IServiceProvider services, - LibMatrixBotConfiguration config, Func<CommandResult, Task>? commandResultHandler = null) { - logger.LogInformation("{} instantiated!", GetType().Name); - _hs = hs; - _logger = logger; - _config = config; - _commandResultHandler = commandResultHandler; - _logger.LogInformation("Getting commands..."); - _commands = services.GetServices<ICommand>(); - _logger.LogInformation("Got {} commands!", _commands.Count()); - } + private readonly CancellationTokenSource _cts = new(); + private long _startupTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); /// <summary>Triggered when the application host is ready to start the service.</summary> /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> public Task StartAsync(CancellationToken cancellationToken) { - _listenerTask = Run(cancellationToken); - _logger.LogInformation("Command listener started (StartAsync)!"); + _listenerTask = Run(_cts.Token); + logger.LogInformation("Getting commands..."); + _commands = services.GetServices<ICommand>().ToFrozenSet(); + logger.LogInformation("Got {} commands!", _commands.Count); + logger.LogInformation("Command listener started (StartAsync)!"); return Task.CompletedTask; } private async Task? Run(CancellationToken cancellationToken) { - _logger.LogInformation("Starting command listener!"); - var filter = await _hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.libmatrix.utilities.bot.command_listener_syncfilter.dev2", new SyncFilter() { - AccountData = new SyncFilter.EventFilter(notTypes: ["*"], limit: 1), - Presence = new SyncFilter.EventFilter(notTypes: ["*"]), - Room = new SyncFilter.RoomFilter() { - AccountData = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - Timeline = new SyncFilter.RoomFilter.StateFilter(types: ["m.room.message"], notSenders: [_hs.WhoAmI.UserId]), - } - }); + logger.LogInformation("Starting command listener!"); + var filter = await hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.libmatrix.utilities.bot.command_listener_syncfilter.dev3" + (config.SelfCommandsOnly), + new SyncFilter() { + AccountData = new SyncFilter.EventFilter(notTypes: ["*"], limit: 1), + Presence = new SyncFilter.EventFilter(notTypes: ["*"]), + Room = new SyncFilter.RoomFilter() { + AccountData = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Timeline = new SyncFilter.RoomFilter.StateFilter(types: ["m.room.message"], + notSenders: config.SelfCommandsOnly ? null : [hs.WhoAmI.UserId], + senders: config.SelfCommandsOnly ? [hs.WhoAmI.UserId] : null + ), + } + }); - var syncHelper = new SyncHelper(_hs, _logger) { - Timeout = 300_000, - FilterId = filter + var syncHelper = new SyncHelper(hs, logger) { + FilterId = filter, + Timeout = config.SyncConfiguration.Timeout ?? 30_000, + MinimumDelay = config.SyncConfiguration.MinimumSyncTime ?? TimeSpan.Zero, + SetPresence = config.SyncConfiguration.Presence ?? botConfig.Presence, }; - syncHelper.TimelineEventHandlers.Add(async @event => { - try { - var room = _hs.GetRoom(@event.RoomId); - // _logger.LogInformation(eventResponse.ToJson(indent: false)); - if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) - if (message is { MessageType: "m.text" }) { - var usedPrefix = await GetUsedPrefix(@event); - if (usedPrefix is null) return; - var res = await InvokeCommand(@event, usedPrefix); - await (_commandResultHandler?.Invoke(res) ?? HandleResult(res)); + syncHelper.SyncReceivedHandlers.Add(async sync => { + logger.LogInformation("Sync received!"); + foreach (var roomResp in sync.Rooms?.Join ?? []) { + if (roomResp.Value.Timeline?.Events is null) continue; + foreach (var @event in roomResp.Value.Timeline.Events) { + @event.RoomId = roomResp.Key; + if (config.SelfCommandsOnly && @event.Sender != hs.WhoAmI.UserId) continue; + if (@event.OriginServerTs < _startupTime) continue; // ignore events older than startup time + + try { + if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) + if (message is { MessageType: "m.text" }) { + var usedPrefix = await GetUsedPrefix(@event); + if (usedPrefix is null) return; + var res = await InvokeCommand(@event, usedPrefix); + await (commandResultHandler?.Invoke(res) ?? HandleResult(res)); + } } - } - catch (Exception e) { - _logger.LogError(e, "Error in command listener!"); + catch (Exception e) { + logger.LogError(e, "Error in command listener!"); + Console.WriteLine(@event.ToJson(ignoreNull: false, indent: true)); + var fakeResult = new CommandResult() { + Result = CommandResult.CommandResultType.Failure_Exception, + Exception = e, + Success = false, + Context = new() { + Homeserver = hs, + CommandName = "[CommandListener.SyncHandler]", + Room = hs.GetRoom(roomResp.Key), + Args = [], + MessageEvent = @event + } + }; + await (commandResultHandler?.Invoke(fakeResult) ?? HandleResult(fakeResult)); + } + } } }); - await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationToken); + + await syncHelper.RunSyncLoopAsync(cancellationToken: _cts.Token); } /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> public async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Shutting down command listener!"); + logger.LogInformation("Shutting down command listener!"); if (_listenerTask is null) { - _logger.LogError("Could not shut down command listener task because it was null!"); + logger.LogError("Could not shut down command listener task because it was null!"); return; } - await _listenerTask.WaitAsync(cancellationToken); + await _cts.CancelAsync(); } private async Task<string?> GetUsedPrefix(StateEventResponse evt) { var messageContent = evt.TypedContent as RoomMessageEventContent; var message = messageContent!.BodyWithoutReplyFallback; - var prefix = _config.Prefixes.OrderByDescending(x => x.Length).FirstOrDefault(message.StartsWith); - if (prefix is null && _config.MentionPrefix) { - var profile = await _hs.GetProfileAsync(_hs.WhoAmI.UserId); - var roomProfile = await _hs.GetRoom(evt.RoomId!).GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, _hs.WhoAmI.UserId); - if (message.StartsWith(_hs.WhoAmI.UserId + ": ")) prefix = profile.DisplayName + ": "; // `@bot:server.xyz: ` - else if (message.StartsWith(_hs.WhoAmI.UserId + " ")) prefix = profile.DisplayName + " "; // `@bot:server.xyz ` + var prefix = config.Prefixes.OrderByDescending(x => x.Length).FirstOrDefault(message.StartsWith); + if (prefix is null && config.MentionPrefix) { + var profile = await hs.GetProfileAsync(hs.WhoAmI.UserId); + var roomProfile = await hs.GetRoom(evt.RoomId!).GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.WhoAmI.UserId); + if (message.StartsWith(hs.WhoAmI.UserId + ": ")) prefix = profile.DisplayName + ": "; // `@bot:server.xyz: ` + else if (message.StartsWith(hs.WhoAmI.UserId + " ")) prefix = profile.DisplayName + " "; // `@bot:server.xyz ` else if (!string.IsNullOrWhiteSpace(roomProfile?.DisplayName) && message.StartsWith(roomProfile.DisplayName + ": ")) prefix = roomProfile.DisplayName + ": "; // `local bot: ` else if (!string.IsNullOrWhiteSpace(roomProfile?.DisplayName) && message.StartsWith(roomProfile.DisplayName + " ")) @@ -112,21 +139,27 @@ public class CommandListenerHostedService : IHostedService { private async Task<CommandResult> InvokeCommand(StateEventResponse evt, string usedPrefix) { var message = evt.TypedContent as RoomMessageEventContent; - var room = _hs.GetRoom(evt.RoomId!); + var room = hs.GetRoom(evt.RoomId!); var commandWithoutPrefix = message.BodyWithoutReplyFallback[usedPrefix.Length..].Trim(); + var usedCommand = _commands + .SelectMany<ICommand, string>(x => [x.Name, ..x.Aliases ?? []]) + .OrderByDescending(x => x.Length) + .FirstOrDefault(commandWithoutPrefix.StartsWith); + var args = + usedCommand == null || commandWithoutPrefix.Length <= usedCommand.Length + ? [] + : commandWithoutPrefix[(usedCommand.Length + 1)..].Split(' ').SelectMany(x => x.Split('\n')).ToArray(); var ctx = new CommandContext { Room = room, - MessageEvent = @evt, - Homeserver = _hs, - Args = commandWithoutPrefix.Split(' ').Length == 1 ? [] : commandWithoutPrefix.Split(' ')[1..], - CommandName = commandWithoutPrefix.Split(' ')[0] + MessageEvent = evt, + Homeserver = hs, + Args = args, + CommandName = usedCommand ?? commandWithoutPrefix.Split(' ')[0] }; try { - var command = _commands.SingleOrDefault(x => x.Name == commandWithoutPrefix.Split(' ')[0] || x.Aliases?.Contains(commandWithoutPrefix.Split(' ')[0]) == true); + var command = _commands.SingleOrDefault(x => x.Name == ctx.CommandName || x.Aliases?.Contains(ctx.CommandName) == true); if (command == null) { - await room.SendMessageEventAsync( - new RoomMessageEventContent("m.notice", $"Command \"{ctx.CommandName}\" not found!")); return new() { Success = false, Result = CommandResult.CommandResultType.Failure_InvalidCommand, diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs
index 7c5cc44..99491a8 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs
@@ -1,86 +1,91 @@ -using System.Reflection.Metadata; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec; +using System.Diagnostics.CodeAnalysis; using LibMatrix.Filters; using LibMatrix.Helpers; using LibMatrix.Homeservers; -using LibMatrix.Responses; +using LibMatrix.Utilities.Bot.Configuration; using LibMatrix.Utilities.Bot.Interfaces; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace LibMatrix.Utilities.Bot.Services; -public class InviteHandlerHostedService : IHostedService { - private readonly AuthenticatedHomeserverGeneric _hs; - private readonly ILogger<InviteHandlerHostedService> _logger; - private readonly Func<InviteEventArgs, Task> _inviteHandler; - +public class InviteHandlerHostedService( + ILogger<InviteHandlerHostedService> logger, + AuthenticatedHomeserverGeneric hs, + LibMatrixBotConfiguration botConfig, + InviteListenerConfiguration config, + Func<RoomInviteContext, Task> inviteHandler +) : IHostedService { private Task? _listenerTask; + private CancellationTokenSource _cts = new(); - public InviteHandlerHostedService(AuthenticatedHomeserverGeneric hs, ILogger<InviteHandlerHostedService> logger, - Func<InviteEventArgs, Task> inviteHandler) { - logger.LogInformation("{} instantiated!", GetType().Name); - _hs = hs; - _logger = logger; - _inviteHandler = inviteHandler; - } + private readonly SyncHelper _syncHelper = new(hs, logger) { + Timeout = config.SyncConfiguration.Timeout ?? 30_000, + MinimumDelay = config.SyncConfiguration.MinimumSyncTime ?? TimeSpan.Zero, + SetPresence = config.SyncConfiguration.Presence ?? botConfig.Presence + }; /// <summary>Triggered when the application host is ready to start the service.</summary> /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> public Task StartAsync(CancellationToken cancellationToken) { _listenerTask = Run(cancellationToken); - _logger.LogInformation("Command listener started (StartAsync)!"); return Task.CompletedTask; } private async Task? Run(CancellationToken cancellationToken) { - _logger.LogInformation("Starting invite listener!"); - var filter = await _hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.libmatrix.utilities.bot.command_listener_syncfilter.dev2", new SyncFilter() { - AccountData = new SyncFilter.EventFilter(notTypes: ["*"], limit: 1), - Presence = new SyncFilter.EventFilter(notTypes: ["*"]), - Room = new SyncFilter.RoomFilter() { - AccountData = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), - Timeline = new SyncFilter.RoomFilter.StateFilter(types: ["m.room.message"], notSenders: [_hs.WhoAmI.UserId]), - } - }); + logger.LogInformation("Starting invite listener!"); + var nextBatchFile = $"inviteHandler.{hs.WhoAmI.UserId}.{hs.WhoAmI.DeviceId}.nextBatch"; + if (config.SyncConfiguration.Filter is not null) { + _syncHelper.Filter = config.SyncConfiguration.Filter; + } + else { + _syncHelper.FilterId = await hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.libmatrix.utilities.bot.invite_listener_syncfilter.dev0", new SyncFilter() { + AccountData = SyncFilter.EventFilter.Empty, + Presence = SyncFilter.EventFilter.Empty, + Room = new SyncFilter.RoomFilter { + AccountData = SyncFilter.RoomFilter.StateFilter.Empty, + Ephemeral = SyncFilter.RoomFilter.StateFilter.Empty, + State = SyncFilter.RoomFilter.StateFilter.Empty, + Timeline = new SyncFilter.RoomFilter.StateFilter(types: [], notSenders: [hs.WhoAmI.UserId]), + IncludeLeave = false + } + }); + } - var syncHelper = new SyncHelper(_hs, _logger) { - Timeout = 300_000, - FilterId = filter - }; - syncHelper.InviteReceivedHandlers.Add(async invite => { - _logger.LogInformation("Received invite to room {}", invite.Key); + if (File.Exists(nextBatchFile) && !config.SyncConfiguration.InitialSyncOnStartup) { + _syncHelper.Since = await File.ReadAllTextAsync(nextBatchFile, cancellationToken); + } - var inviteEventArgs = new InviteEventArgs() { + _syncHelper.InviteReceivedHandlers.Add(async invite => { + logger.LogInformation("Received invite to room {}", invite.Key); + var inviteEventArgs = new RoomInviteContext() { RoomId = invite.Key, - MemberEvent = invite.Value.InviteState.Events.First(x => x.Type == "m.room.member" && x.StateKey == _hs.WhoAmI.UserId), - Homeserver = _hs + InviteData = invite.Value, + MemberEvent = invite.Value.InviteState?.Events?.First(x => x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId) + ?? throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, + Error = "Room invite doesn't contain a membership event!" + }, + Homeserver = hs }; - await _inviteHandler(inviteEventArgs); + await inviteHandler(inviteEventArgs); }); - await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationToken); + if (!config.SyncConfiguration.InitialSyncOnStartup) + _syncHelper.SyncReceivedHandlers.Add(sync => File.WriteAllTextAsync(nextBatchFile, sync.NextBatch, cancellationToken)); + await _syncHelper.RunSyncLoopAsync(cancellationToken: _cts.Token); } /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> public async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Shutting down invite listener!"); + logger.LogInformation("Shutting down invite listener!"); if (_listenerTask is null) { - _logger.LogError("Could not shut down invite listener task because it was null!"); + logger.LogError("Could not shut down invite listener task because it was null!"); return; } - await _listenerTask.WaitAsync(cancellationToken); - } - - public class InviteEventArgs { - public string RoomId { get; set; } - public StateEventResponse MemberEvent { get; set; } - public AuthenticatedHomeserverGeneric Homeserver { get; set; } + await _cts.CancelAsync(); } } \ No newline at end of file