about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules2
-rw-r--r--.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml22
-rw-r--r--.idea/.idea.MatrixRoomUtils/.idea/vcs.xml6
-rw-r--r--.idea/.idea.MatrixUtils/.idea/.gitignore (renamed from .idea/.idea.MatrixRoomUtils/.idea/.gitignore)0
-rw-r--r--.idea/.idea.MatrixUtils/.idea/.name1
-rw-r--r--.idea/.idea.MatrixUtils/.idea/avalonia.xml (renamed from .idea/.idea.MatrixRoomUtils/.idea/avalonia.xml)0
-rw-r--r--.idea/.idea.MatrixUtils/.idea/codeStyles/codeStyleConfig.xml (renamed from .idea/.idea.MatrixRoomUtils/.idea/codeStyles/codeStyleConfig.xml)0
-rw-r--r--.idea/.idea.MatrixUtils/.idea/encodings.xml (renamed from .idea/.idea.MatrixRoomUtils/.idea/encodings.xml)0
-rw-r--r--.idea/.idea.MatrixUtils/.idea/indexLayout.xml (renamed from .idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml)0
-rw-r--r--.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml53
-rw-r--r--.idea/.idea.MatrixUtils/.idea/vcs.xml9
-rw-r--r--Benchmarks/.gitignore3
-rw-r--r--Benchmarks/Benchmarks.csproj17
-rw-r--r--Benchmarks/Program.cs303
m---------LibMatrix0
-rw-r--r--MatrixRoomUtils.sln232
-rw-r--r--MatrixUtils.Abstractions/FileStorageProvider.cs1
-rw-r--r--MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj6
-rw-r--r--MatrixUtils.Abstractions/RoomInfo.cs9
-rw-r--r--MatrixUtils.Desktop/App.axaml.cs7
-rw-r--r--MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs8
-rw-r--r--MatrixUtils.Desktop/MainWindow.axaml.cs2
-rw-r--r--MatrixUtils.Desktop/MatrixUtils.Desktop.csproj33
-rw-r--r--MatrixUtils.Desktop/RMUDesktopConfiguration.cs3
-rw-r--r--MatrixUtils.Desktop/SentryService.cs29
-rw-r--r--MatrixUtils.Desktop/appsettings.Development.json21
-rw-r--r--MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj52
-rw-r--r--MatrixUtils.DmSpaced/Program.cs14
-rw-r--r--MatrixUtils.LibDMSpace/DMSpaceRoom.cs13
-rw-r--r--MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj2
-rw-r--r--MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs1
-rw-r--r--MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs1
-rw-r--r--MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs1
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs32
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs33
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs31
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs63
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs35
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs39
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs78
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs115
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs252
-rw-r--r--MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj22
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Program.cs41
-rw-r--r--MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json12
-rw-r--r--MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs5
-rw-r--r--MatrixUtils.RoomUpgradeCLI/appsettings.Development.json17
-rw-r--r--MatrixUtils.RoomUpgradeCLI/appsettings.json8
-rwxr-xr-xMatrixUtils.RoomUpgradeCLI/mass-upgrade.sh9
-rw-r--r--MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj8
-rw-r--r--MatrixUtils.Web.Server/Program.cs6
-rw-r--r--MatrixUtils.Web/App.razor8
-rw-r--r--MatrixUtils.Web/Classes/Constants/RoomConstants.cs5
-rw-r--r--MatrixUtils.Web/Classes/LocalStorageProviderService.cs18
-rw-r--r--MatrixUtils.Web/Classes/RMUStorageWrapper.cs138
-rw-r--r--MatrixUtils.Web/Classes/RmuSessionStore.cs336
-rw-r--r--MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs13
-rw-r--r--MatrixUtils.Web/Classes/SessionStorageProviderService.cs2
-rw-r--r--MatrixUtils.Web/Classes/UserAuth.cs14
-rw-r--r--MatrixUtils.Web/MatrixUtils.Web.csproj90
-rw-r--r--MatrixUtils.Web/Pages/About.razor4
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevOptions.razor21
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevUtilities.razor34
-rw-r--r--MatrixUtils.Web/Pages/Dev/ModalTest.razor12
-rw-r--r--MatrixUtils.Web/Pages/Dev/WellKnownRes.razor123
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor13
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor43
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor201
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor29
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor192
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css (renamed from MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css)0
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor74
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css35
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor266
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor618
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor212
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor243
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/HSEInit.razor2
-rw-r--r--MatrixUtils.Web/Pages/Index.razor145
-rw-r--r--MatrixUtils.Web/Pages/InvalidSession.razor56
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor4
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/Index.razor6
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor17
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor24
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor10
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor11
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor3
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor41
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor6
-rw-r--r--MatrixUtils.Web/Pages/LoginPage.razor66
-rw-r--r--MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor16
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create.razor12
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create2.razor147
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index.razor69
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor791
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs144
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor240
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css32
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor74
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor88
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor218
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyLists.razor238
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor52
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor92
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor83
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor60
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor19
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor123
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor70
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor65
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor51
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Space.razor56
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateEditor.razor4
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateViewer.razor4
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Timeline.razor5
-rw-r--r--MatrixUtils.Web/Pages/ServerInfo.razor3
-rw-r--r--MatrixUtils.Web/Pages/StreamTest.razor119
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor70
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor6
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor12
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor48
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor177
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor27
-rw-r--r--MatrixUtils.Web/Pages/Tools/InviteCounter.razor33
-rw-r--r--MatrixUtils.Web/Pages/Tools/MassCMEBan.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor138
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor (renamed from MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor)49
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor139
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor192
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor30
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor55
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor696
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor14
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor158
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor51
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor204
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor8
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor20
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor27
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/StickerManager.razor80
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor4
-rw-r--r--MatrixUtils.Web/Pages/User/DMManager.razor4
-rw-r--r--MatrixUtils.Web/Pages/User/Profile.razor129
-rw-r--r--MatrixUtils.Web/Program.cs29
-rw-r--r--MatrixUtils.Web/Properties/launchSettings.json2
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor17
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor31
-rw-r--r--MatrixUtils.Web/Shared/InlineUserItem.razor5
-rw-r--r--MatrixUtils.Web/Shared/InputLocalPart.razor50
-rw-r--r--MatrixUtils.Web/Shared/MainLayout.razor12
-rw-r--r--MatrixUtils.Web/Shared/MainLayout.razor.css1
-rw-r--r--MatrixUtils.Web/Shared/MxcAvatar.razor49
-rw-r--r--MatrixUtils.Web/Shared/MxcImage.razor78
-rw-r--r--MatrixUtils.Web/Shared/NavMenu.razor6
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor221
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css15
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor106
-rw-r--r--MatrixUtils.Web/Shared/RoomList.razor5
-rw-r--r--MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor13
-rw-r--r--MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor13
-rw-r--r--MatrixUtils.Web/Shared/RoomListItem.razor53
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor2
-rw-r--r--MatrixUtils.Web/Shared/UserListItem.razor25
-rw-r--r--MatrixUtils.Web/_Imports.razor11
-rw-r--r--MatrixUtils.Web/appsettings.Development.json5
-rw-r--r--MatrixUtils.Web/appsettings.json3
-rw-r--r--MatrixUtils.Web/wwwroot/appsettings.json3
-rw-r--r--MatrixUtils.Web/wwwroot/css/app.css5
-rw-r--r--MatrixUtils.Web/wwwroot/index.html29
-rw-r--r--MatrixUtils.Web/wwwroot/sw-registrator.js2
-rw-r--r--MatrixUtils.sln489
m---------MxApiExtensions0
-rw-r--r--global.json2
-rwxr-xr-xscripts/deploy.sh3
194 files changed, 9443 insertions, 1947 deletions
diff --git a/.gitignore b/.gitignore

index 984ec54..67e71e8 100644 --- a/.gitignore +++ b/.gitignore
@@ -10,6 +10,7 @@ MatrixRoomUtils.Bot/bot_data/ appsettings.Local*.json nixpkgs/ *.DotSettings.user +**/.DS_Store *.patch test.tsv diff --git a/.gitmodules b/.gitmodules
index 8cbedc0..487c63b 100644 --- a/.gitmodules +++ b/.gitmodules
@@ -1,6 +1,6 @@ [submodule "LibMatrix"] path = LibMatrix - url = https://git.rory.gay/matrix/LibMatrix.git + url = https://cgit.rory.gay/matrix/LibMatrix.git [submodule "MxApiExtensions"] path = MxApiExtensions url = https://cgit.rory.gay/matrix/tools/MxApiExtensions.git diff --git a/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644
index 55540ea..0000000 --- a/.idea/.idea.MatrixRoomUtils/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null
@@ -1,22 +0,0 @@ -<component name="InspectionProjectProfileManager"> - <profile version="1.0"> - <option name="myName" value="Project Default" /> - <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myValues"> - <value> - <list size="7"> - <item index="0" class="java.lang.String" itemvalue="nobr" /> - <item index="1" class="java.lang.String" itemvalue="noembed" /> - <item index="2" class="java.lang.String" itemvalue="comment" /> - <item index="3" class="java.lang.String" itemvalue="noscript" /> - <item index="4" class="java.lang.String" itemvalue="embed" /> - <item index="5" class="java.lang.String" itemvalue="script" /> - <item index="6" class="java.lang.String" itemvalue="width" /> - </list> - </value> - </option> - <option name="myCustomValuesEnabled" value="true" /> - </inspection_tool> - <inspection_tool class="JsonStandardCompliance" enabled="false" level="ERROR" enabled_by_default="false" /> - </profile> -</component> \ No newline at end of file diff --git a/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml b/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml deleted file mode 100644
index 94a25f7..0000000 --- a/.idea/.idea.MatrixRoomUtils/.idea/vcs.xml +++ /dev/null
@@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$" vcs="Git" /> - </component> -</project> \ No newline at end of file diff --git a/.idea/.idea.MatrixRoomUtils/.idea/.gitignore b/.idea/.idea.MatrixUtils/.idea/.gitignore
index f938d5a..f938d5a 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/.gitignore +++ b/.idea/.idea.MatrixUtils/.idea/.gitignore
diff --git a/.idea/.idea.MatrixUtils/.idea/.name b/.idea/.idea.MatrixUtils/.idea/.name new file mode 100644
index 0000000..65d8b74 --- /dev/null +++ b/.idea/.idea.MatrixUtils/.idea/.name
@@ -0,0 +1 @@ +MatrixUtils \ No newline at end of file diff --git a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml b/.idea/.idea.MatrixUtils/.idea/avalonia.xml
index 0aa65bb..0aa65bb 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/avalonia.xml +++ b/.idea/.idea.MatrixUtils/.idea/avalonia.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.MatrixUtils/.idea/codeStyles/codeStyleConfig.xml
index a55e7a1..a55e7a1 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/.idea.MatrixUtils/.idea/codeStyles/codeStyleConfig.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/encodings.xml b/.idea/.idea.MatrixUtils/.idea/encodings.xml
index df87cf9..df87cf9 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/encodings.xml +++ b/.idea/.idea.MatrixUtils/.idea/encodings.xml
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
index 4520708..4520708 100644 --- a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml +++ b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
diff --git a/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml new file mode 100644
index 0000000..0e61b0a --- /dev/null +++ b/.idea/.idea.MatrixUtils/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,53 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> + <option name="myValues"> + <value> + <list size="7"> + <item index="0" class="java.lang.String" itemvalue="nobr" /> + <item index="1" class="java.lang.String" itemvalue="noembed" /> + <item index="2" class="java.lang.String" itemvalue="comment" /> + <item index="3" class="java.lang.String" itemvalue="noscript" /> + <item index="4" class="java.lang.String" itemvalue="embed" /> + <item index="5" class="java.lang.String" itemvalue="script" /> + <item index="6" class="java.lang.String" itemvalue="width" /> + </list> + </value> + </option> + <option name="myCustomValuesEnabled" value="true" /> + </inspection_tool> + <inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true"> + <option name="ignoredUrls"> + <list> + <option value="http://" /> + <option value="http://0.0.0.0" /> + <option value="http://127.0.0.1" /> + <option value="http://activemq.apache.org/schema/" /> + <option value="http://cxf.apache.org/schemas/" /> + <option value="http://java.sun.com/" /> + <option value="http://javafx.com/fxml" /> + <option value="http://javafx.com/javafx/" /> + <option value="http://json-schema.org/draft" /> + <option value="http://localhost" /> + <option value="http://maven.apache.org/POM/" /> + <option value="http://maven.apache.org/xsd/" /> + <option value="http://primefaces.org/ui" /> + <option value="http://schema.cloudfoundry.org/spring/" /> + <option value="http://schemas.xmlsoap.org/" /> + <option value="http://tiles.apache.org/" /> + <option value="http://www.ibm.com/webservices/xsd" /> + <option value="http://www.jboss.com/xml/ns/" /> + <option value="http://www.jboss.org/j2ee/schema/" /> + <option value="http://www.springframework.org/schema/" /> + <option value="http://www.springframework.org/security/tags" /> + <option value="http://www.springframework.org/tags" /> + <option value="http://www.thymeleaf.org" /> + <option value="http://www.w3.org/" /> + <option value="http://xmlns.jcp.org/" /> + </list> + </option> + </inspection_tool> + <inspection_tool class="JsonStandardCompliance" enabled="false" level="ERROR" enabled_by_default="false" /> + </profile> +</component> \ No newline at end of file diff --git a/.idea/.idea.MatrixUtils/.idea/vcs.xml b/.idea/.idea.MatrixUtils/.idea/vcs.xml new file mode 100644
index 0000000..df05d42 --- /dev/null +++ b/.idea/.idea.MatrixUtils/.idea/vcs.xml
@@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/LibMatrix" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/LibMatrix/ArcaneLibs" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/MxApiExtensions" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/Benchmarks/.gitignore b/Benchmarks/.gitignore new file mode 100644
index 0000000..a7d52bf --- /dev/null +++ b/Benchmarks/.gitignore
@@ -0,0 +1,3 @@ + +BenchmarkDotNet.Artifacts +benchmark.log diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj new file mode 100644
index 0000000..a174f1f --- /dev/null +++ b/Benchmarks/Benchmarks.csproj
@@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net10.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <!-- <PublishAot>true</PublishAot>--> + <InvariantGlobalization>true</InvariantGlobalization> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.15.4" /> + </ItemGroup> + +</Project> diff --git a/Benchmarks/Program.cs b/Benchmarks/Program.cs new file mode 100644
index 0000000..90d004a --- /dev/null +++ b/Benchmarks/Program.cs
@@ -0,0 +1,303 @@ +// See https://aka.ms/new-console-template for more information + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Running; + +[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 5, iterationCount: 5, id: "FastAndDirtyJob")] +[InProcess] +[ProcessCount(4)] +public class Program { + public static void Main(string[] args) { + BenchmarkRunner.Run<Program>(args: args); + } + + [Params(true, false)] + public bool DoDisambiguate { get; set; } = true; + + [Params(true, false)] + public bool DisambiguateProfileUpdates { + get => field && DoDisambiguate; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateKicks { + get => field && DoDisambiguate; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateUnbans { + get => field && DoDisambiguate; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateInviteAccepted { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateInviteRejected { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateInviteRetracted { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateKnockAccepted { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateKnockRejected { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateKnockRetracted { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateKnockActions { + get => field && DoDisambiguate; + set; + } = true; + + [Params(true, false)] + public bool DisambiguateInviteActions { + get => field && DoDisambiguate; + set; + } = true; + + public enum MembershipTransition : uint { + None, + Join = 0b0001, + Leave = 0b0010, + Knock = 0b0100, + Invite = 0b1000, + Ban = 0b1001, + + // disambiguated + ProfileUpdate = 0b0000_0001_0001, + Kick = 0b0000_0001_0010, + Unban = 0b0000_0010_0010, + InviteAccepted = 0b0000_0100_0001, + InviteRejected = 0b0000_1000_0010, + InviteRetracted = 0b0001_0000_0010, + KnockAccepted = 0b0010_0000_1000, + KnockRejected = 0b0100_0000_0010, + KnockRetracted = 0b1000_0000_0010 + } + + public readonly struct MembershipEntry { + public required MembershipTransition State { get; init; } + public string Aba { get; init; } + public string Abb { get; init; } + public string Abc { get; init; } + public string Abd { get; init; } + } + + [Params(100, 10_000, 1_000_000)] public int N; + + [GlobalSetup] + public void Setup() { + entries = Enumerable.Range(0, N).Select(_ => new MembershipEntry() { + State = (MembershipTransition)new Random().Next(1, 16), + Aba = Guid.NewGuid().ToString(), + Abb = Guid.NewGuid().ToString(), + Abc = Guid.NewGuid().ToString(), + Abd = Guid.NewGuid().ToString() + }).ToImmutableList(); + } + + public ImmutableList<MembershipEntry> entries = ImmutableList<MembershipEntry>.Empty; + + [Benchmark] + public void TestTruthyness() { + var @switch = AmbiguateMembershipsSwitch().GetEnumerator(); + var @switchpm = AmbiguateMembershipsSwitchPatternMatching().GetEnumerator(); + var @if = AmbiguateMembershipsIf().GetEnumerator(); + var @map = AmbiguateMembershipsStaticMap().GetEnumerator(); + var @binmask = AmbiguateMembershipsBinMask().GetEnumerator(); + + while (@switch.MoveNext() && @map.MoveNext() && @if.MoveNext() && @switchpm.MoveNext() && @binmask.MoveNext()) { + if (@switch.Current.State != @map.Current.State || @switch.Current.State != @if.Current.State || @switch.Current.State != @switchpm.Current.State || + @switch.Current.State != @binmask.Current.State) { + throw new InvalidOperationException("Results do not match!"); + } + } + + @switch.Dispose(); + @switchpm.Dispose(); + @if.Dispose(); + @map.Dispose(); + @binmask.Dispose(); + } + + [Benchmark] + public void TestAmbiguateMembershipsSwitchPatternMatching() => AmbiguateMembershipsSwitchPatternMatching().Consume(new Consumer()); + + public IEnumerable<MembershipEntry> AmbiguateMembershipsSwitchPatternMatching() { + foreach (var entry in entries) { + var newState = entry.State switch { + MembershipTransition.ProfileUpdate when !DoDisambiguate || !DisambiguateProfileUpdates => MembershipTransition.Join, + MembershipTransition.Kick when !DoDisambiguate || !DisambiguateKicks => MembershipTransition.Leave, + MembershipTransition.Unban when !DoDisambiguate || !DisambiguateUnbans => MembershipTransition.Leave, + MembershipTransition.InviteAccepted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted => MembershipTransition.Join, + MembershipTransition.InviteRejected when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected => MembershipTransition.Leave, + MembershipTransition.InviteRetracted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted => MembershipTransition.Leave, + MembershipTransition.KnockAccepted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted => MembershipTransition.Invite, + MembershipTransition.KnockRejected when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected => MembershipTransition.Leave, + MembershipTransition.KnockRetracted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted => MembershipTransition.Leave, + _ => entry.State + }; + yield return newState == entry.State ? entry : entry with { State = newState }; + } + } + + [Benchmark] + public void TestAmbiguateMembershipsSwitch() => AmbiguateMembershipsSwitch().Consume(new Consumer()); + + public IEnumerable<MembershipEntry> AmbiguateMembershipsSwitch() { + foreach (var entry in entries) { + if (!DoDisambiguate) { + yield return entry; + continue; + } + + MembershipTransition newState; + switch (entry.State) { + case MembershipTransition.ProfileUpdate: + newState = !DisambiguateProfileUpdates ? MembershipTransition.Join : entry.State; + break; + case MembershipTransition.Kick: + newState = !DisambiguateKicks ? MembershipTransition.Leave : entry.State; + break; + case MembershipTransition.Unban when !DisambiguateUnbans: + newState = MembershipTransition.Leave; + break; + case MembershipTransition.InviteAccepted when !DisambiguateInviteActions || !DisambiguateInviteAccepted: + newState = MembershipTransition.Join; + break; + case MembershipTransition.InviteRejected when !DisambiguateInviteActions || !DisambiguateInviteRejected: + + newState = MembershipTransition.Leave; + break; + case MembershipTransition.InviteRetracted when !DisambiguateInviteActions || !DisambiguateInviteRetracted: + newState = MembershipTransition.Leave; + break; + case MembershipTransition.KnockAccepted when !DisambiguateKnockActions || !DisambiguateKnockAccepted: + newState = MembershipTransition.Invite; + break; + case MembershipTransition.KnockRejected when !DisambiguateKnockActions || !DisambiguateKnockRejected: + newState = MembershipTransition.Leave; + break; + case MembershipTransition.KnockRetracted when !DisambiguateKnockActions || !DisambiguateKnockRetracted: + newState = MembershipTransition.Leave; + break; + default: + newState = entry.State; + break; + } + + yield return newState == entry.State ? entry : entry with { State = newState }; + } + } + + [Benchmark] + public void TestAmbiguateMembershipsIf() => AmbiguateMembershipsIf().Consume(new Consumer()); + + public IEnumerable<MembershipEntry> AmbiguateMembershipsIf() { + foreach (var entry in entries) { + MembershipTransition newState; + if (entry.State == MembershipTransition.ProfileUpdate && (!DoDisambiguate || !DisambiguateProfileUpdates)) + newState = MembershipTransition.Join; + else if ((entry.State == MembershipTransition.Kick && (!DoDisambiguate || !DisambiguateKicks)) || + (entry.State == MembershipTransition.Unban && (!DoDisambiguate || !DisambiguateUnbans))) + newState = MembershipTransition.Leave; + else if (entry.State == MembershipTransition.InviteAccepted && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted)) + newState = MembershipTransition.Join; + else if ((entry.State == MembershipTransition.InviteRejected && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected)) || + (entry.State == MembershipTransition.InviteRetracted && (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted))) + newState = MembershipTransition.Leave; + else if (entry.State == MembershipTransition.KnockAccepted && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted)) + newState = MembershipTransition.Invite; + else if ((entry.State == MembershipTransition.KnockRejected && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected)) || + (entry.State == MembershipTransition.KnockRetracted && (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted))) + newState = MembershipTransition.Leave; + else + newState = entry.State; + + yield return newState == entry.State ? entry : entry with { State = newState }; + } + } + + [Benchmark] + public void TestAmbiguateMembershipsStaticMap() => AmbiguateMembershipsStaticMap().Consume(new Consumer()); + + public IEnumerable<MembershipEntry> AmbiguateMembershipsStaticMap() { + Dictionary<MembershipTransition, MembershipTransition> _map = []; + if (!DoDisambiguate || !DisambiguateProfileUpdates) _map[MembershipTransition.ProfileUpdate] = MembershipTransition.Join; + if (!DoDisambiguate || !DisambiguateKicks) _map[MembershipTransition.Kick] = MembershipTransition.Leave; + if (!DoDisambiguate || !DisambiguateUnbans) _map[MembershipTransition.Unban] = MembershipTransition.Leave; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted) _map[MembershipTransition.InviteAccepted] = MembershipTransition.Join; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected) _map[MembershipTransition.InviteRejected] = MembershipTransition.Leave; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted) _map[MembershipTransition.InviteRetracted] = MembershipTransition.Leave; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted) _map[MembershipTransition.KnockAccepted] = MembershipTransition.Invite; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected) _map[MembershipTransition.KnockRejected] = MembershipTransition.Leave; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted) _map[MembershipTransition.KnockRetracted] = MembershipTransition.Leave; + FrozenDictionary<MembershipTransition, MembershipTransition> map = _map.ToFrozenDictionary(); + // _map + foreach (var entry in entries) { + var newState = map.TryGetValue(entry.State, out var value) ? value : entry.State; + yield return newState == entry.State ? entry : entry with { State = newState }; + } + } + + [Benchmark] + public void TestAmbiguateMembershipsBinMask() => AmbiguateMembershipsBinMask().Consume(new Consumer()); + + public IEnumerable<MembershipEntry> AmbiguateMembershipsBinMask() { + uint mask = 0; + // dont mask last 4 bits + if (!DoDisambiguate || !DisambiguateProfileUpdates) mask |= (uint)MembershipTransition.ProfileUpdate >> 4; + if (!DoDisambiguate || !DisambiguateKicks) mask |= (uint)MembershipTransition.Kick >> 4; + if (!DoDisambiguate || !DisambiguateUnbans) mask |= (uint)MembershipTransition.Unban >> 4; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted) mask |= (uint)MembershipTransition.InviteAccepted >> 4; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected) mask |= (uint)MembershipTransition.InviteRejected >> 4; + if (!DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted) mask |= (uint)MembershipTransition.InviteRetracted >> 4; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted) mask |= (uint)MembershipTransition.KnockAccepted >> 4; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected) mask |= (uint)MembershipTransition.KnockRejected >> 4; + if (!DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted) mask |= (uint)MembershipTransition.KnockRetracted >> 4; + mask = (mask << 4) + 0b1111; + // Console.WriteLine(mask.ToString("b24")); + foreach (var entry in entries) { + if (((uint)entry.State & 0b1111_1111_0000) == 0) { + yield return entry; + continue; + } + + var newState = (MembershipTransition)((uint)entry.State & mask); + // Console.WriteLine(((uint)newState).ToString("b32")); + yield return newState == entry.State ? entry : entry with { State = newState }; + } + } +} \ No newline at end of file diff --git a/LibMatrix b/LibMatrix -Subproject 16e314ed714f8b3e298c0ecf2ebfe67b48e5f69 +Subproject 1db452c75de1e25a9a2a8fd4fe2a04a2e1047f2 diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln deleted file mode 100644
index 23d86b0..0000000 --- a/MatrixRoomUtils.sln +++ /dev/null
@@ -1,232 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web", "MatrixUtils.Web\MatrixUtils.Web.csproj", "{D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web.Server", "MatrixUtils.Web.Server\MatrixUtils.Web.Server.csproj", "{F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Desktop", "MatrixUtils.Desktop\MatrixUtils.Desktop.csproj", "{27C08A4F-5AF0-4C2C-AFCB-050E3388C116}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LibMatrix", "LibMatrix", "{8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix\LibMatrix.csproj", "{F4E241C3-0300-4B87-8707-BCBDEF1F0185}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MxApiExtensions", "MxApiExtensions", "{F1376F9A-FB65-4E60-BB9A-62A64F741FF4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExtensions\MxApiExtensions\MxApiExtensions.csproj", "{41200A7B-D2DB-4656-B66B-5206A63B367A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "LibMatrix\ArcaneLibs", "{B00C5CB6-6200-4B41-96BE-C6EAF1085A14}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "LibMatrix\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{2D6F31D7-3139-44EC-9D11-486282DD4ED1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes", "MxApiExtensions\MxApiExtensions.Classes\MxApiExtensions.Classes.csproj", "{99C016AA-AFBA-4D32-A687-D1FABC0F5212}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes.LibMatrix", "MxApiExtensions\MxApiExtensions.Classes.LibMatrix\MxApiExtensions.Classes.LibMatrix.csproj", "{C298E274-5D6C-47C8-9B71-A6B34D0195A3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "LibMatrix\ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{2CB12623-4918-4176-9B4A-88D846CCD3ED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "LibMatrix\ExampleBots\ModerationBot\ModerationBot.csproj", "{48DBB05F-B007-4B24-89B3-3CC177C79007}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "LibMatrix\Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{5DEF66C8-B931-435F-B4B5-E1858590D52E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{D8E5C678-3BE5-470C-A3A5-B5D525FC2012}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "LibMatrix\ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{95052EE6-7513-46FB-91BD-EE82026B42F1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{7D2C9959-8309-4110-A67F-DEE64E97C1D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "LibMatrix\Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDataGenerator", "LibMatrix\Tests\TestDataGenerator\TestDataGenerator.csproj", "{F3312DE9-4335-4E85-A4CF-2616427A651E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.LibDMSpace", "MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj", "{EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{1CAA2B6D-0365-4C8B-96EE-26026514FEE2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Abstractions", "MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "LibMatrix\Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{CC836863-0EE8-44BD-BF39-45076F57C416}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "LibMatrix\Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{5CE239F8-C124-4A96-A0F8-B56B9AE27434}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{6D93DA72-69D8-43BD-BC19-7FFF8A313971}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "LibMatrix\ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.DmSpaced", "MatrixUtils.DmSpaced\MatrixUtils.DmSpaced.csproj", "{B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "LibMatrix\ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "LibMatrix\ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "LibMatrix\ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{FDB12B6A-01AA-46C0-A55E-0F984496AB81}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "LibMatrix\ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{84EE78FF-E198-4090-BFE9-C47D266E115E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "LibMatrix\ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{1607FCA9-7B5B-45B0-8D1F-205ABACB7173}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix\LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{56A42391-4514-4352-B22B-622EE7A618AA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.Build.0 = Release|Any CPU - {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.Build.0 = Release|Any CPU - {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.Build.0 = Release|Any CPU - {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4E241C3-0300-4B87-8707-BCBDEF1F0185}.Release|Any CPU.Build.0 = Release|Any CPU - {41200A7B-D2DB-4656-B66B-5206A63B367A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41200A7B-D2DB-4656-B66B-5206A63B367A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41200A7B-D2DB-4656-B66B-5206A63B367A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41200A7B-D2DB-4656-B66B-5206A63B367A}.Release|Any CPU.Build.0 = Release|Any CPU - {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F}.Release|Any CPU.Build.0 = Release|Any CPU - {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D6F31D7-3139-44EC-9D11-486282DD4ED1}.Release|Any CPU.Build.0 = Release|Any CPU - {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99C016AA-AFBA-4D32-A687-D1FABC0F5212}.Release|Any CPU.Build.0 = Release|Any CPU - {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C298E274-5D6C-47C8-9B71-A6B34D0195A3}.Release|Any CPU.Build.0 = Release|Any CPU - {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2CB12623-4918-4176-9B4A-88D846CCD3ED}.Release|Any CPU.Build.0 = Release|Any CPU - {48DBB05F-B007-4B24-89B3-3CC177C79007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48DBB05F-B007-4B24-89B3-3CC177C79007}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48DBB05F-B007-4B24-89B3-3CC177C79007}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48DBB05F-B007-4B24-89B3-3CC177C79007}.Release|Any CPU.Build.0 = Release|Any CPU - {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5DEF66C8-B931-435F-B4B5-E1858590D52E}.Release|Any CPU.Build.0 = Release|Any CPU - {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D8E5C678-3BE5-470C-A3A5-B5D525FC2012}.Release|Any CPU.Build.0 = Release|Any CPU - {95052EE6-7513-46FB-91BD-EE82026B42F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95052EE6-7513-46FB-91BD-EE82026B42F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95052EE6-7513-46FB-91BD-EE82026B42F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95052EE6-7513-46FB-91BD-EE82026B42F1}.Release|Any CPU.Build.0 = Release|Any CPU - {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881}.Release|Any CPU.Build.0 = Release|Any CPU - {F3312DE9-4335-4E85-A4CF-2616427A651E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3312DE9-4335-4E85-A4CF-2616427A651E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3312DE9-4335-4E85-A4CF-2616427A651E}.Release|Any CPU.Build.0 = Release|Any CPU - {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.Build.0 = Release|Any CPU - {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.Build.0 = Release|Any CPU - {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU - {CC836863-0EE8-44BD-BF39-45076F57C416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC836863-0EE8-44BD-BF39-45076F57C416}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC836863-0EE8-44BD-BF39-45076F57C416}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC836863-0EE8-44BD-BF39-45076F57C416}.Release|Any CPU.Build.0 = Release|Any CPU - {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5CE239F8-C124-4A96-A0F8-B56B9AE27434}.Release|Any CPU.Build.0 = Release|Any CPU - {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D93DA72-69D8-43BD-BC19-7FFF8A313971}.Release|Any CPU.Build.0 = Release|Any CPU - {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0}.Release|Any CPU.Build.0 = Release|Any CPU - {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B3FEA1EF-6CFE-49C5-A0B2-11DB58D4CD1C}.Release|Any CPU.Build.0 = Release|Any CPU - {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA}.Release|Any CPU.Build.0 = Release|Any CPU - {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC}.Release|Any CPU.Build.0 = Release|Any CPU - {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDB12B6A-01AA-46C0-A55E-0F984496AB81}.Release|Any CPU.Build.0 = Release|Any CPU - {84EE78FF-E198-4090-BFE9-C47D266E115E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84EE78FF-E198-4090-BFE9-C47D266E115E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84EE78FF-E198-4090-BFE9-C47D266E115E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84EE78FF-E198-4090-BFE9-C47D266E115E}.Release|Any CPU.Build.0 = Release|Any CPU - {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1607FCA9-7B5B-45B0-8D1F-205ABACB7173}.Release|Any CPU.Build.0 = Release|Any CPU - {56A42391-4514-4352-B22B-622EE7A618AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56A42391-4514-4352-B22B-622EE7A618AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56A42391-4514-4352-B22B-622EE7A618AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56A42391-4514-4352-B22B-622EE7A618AA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - {41200A7B-D2DB-4656-B66B-5206A63B367A} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4} - {B00E29F5-1ED8-40A0-A70D-DE9F23FC572F} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {2D6F31D7-3139-44EC-9D11-486282DD4ED1} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {99C016AA-AFBA-4D32-A687-D1FABC0F5212} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4} - {C298E274-5D6C-47C8-9B71-A6B34D0195A3} = {F1376F9A-FB65-4E60-BB9A-62A64F741FF4} - {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - {2CB12623-4918-4176-9B4A-88D846CCD3ED} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2} - {48DBB05F-B007-4B24-89B3-3CC177C79007} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2} - {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - {5DEF66C8-B931-435F-B4B5-E1858590D52E} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} - {D8E5C678-3BE5-470C-A3A5-B5D525FC2012} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} - {95052EE6-7513-46FB-91BD-EE82026B42F1} = {3E0FDE30-8DA8-4E65-A3C6-AA53B5BC70A2} - {7D2C9959-8309-4110-A67F-DEE64E97C1D8} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - {E37B78F1-D7A5-4F79-ADBA-E12DF7D0F881} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8} - {F3312DE9-4335-4E85-A4CF-2616427A651E} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8} - {1CAA2B6D-0365-4C8B-96EE-26026514FEE2} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - {CC836863-0EE8-44BD-BF39-45076F57C416} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} - {5CE239F8-C124-4A96-A0F8-B56B9AE27434} = {A4BCBF5F-4936-44B9-BAB3-FAF240BDF40D} - {6D93DA72-69D8-43BD-BC19-7FFF8A313971} = {7D2C9959-8309-4110-A67F-DEE64E97C1D8} - {EDFC9BA8-CAA9-4B51-AFAF-834C1D74DCF0} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {748D215E-CA40-4D0F-BE1D-D2350D4AB8CA} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {E33F7CB0-A03D-4ED0-9AE8-95B31A2D7ACC} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {FDB12B6A-01AA-46C0-A55E-0F984496AB81} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {84EE78FF-E198-4090-BFE9-C47D266E115E} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {1607FCA9-7B5B-45B0-8D1F-205ABACB7173} = {B00C5CB6-6200-4B41-96BE-C6EAF1085A14} - {56A42391-4514-4352-B22B-622EE7A618AA} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} - EndGlobalSection -EndGlobal diff --git a/MatrixUtils.Abstractions/FileStorageProvider.cs b/MatrixUtils.Abstractions/FileStorageProvider.cs
index fbe068d..1083002 100644 --- a/MatrixUtils.Abstractions/FileStorageProvider.cs +++ b/MatrixUtils.Abstractions/FileStorageProvider.cs
@@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Interfaces.Services; using Microsoft.Extensions.Logging; diff --git a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
index 1665ff0..751aa5d 100644 --- a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj +++ b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
@@ -1,12 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> - + <ItemGroup> - <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/> </ItemGroup> </Project> diff --git a/MatrixUtils.Abstractions/RoomInfo.cs b/MatrixUtils.Abstractions/RoomInfo.cs
index aff0e25..4b2a53c 100644 --- a/MatrixUtils.Abstractions/RoomInfo.cs +++ b/MatrixUtils.Abstractions/RoomInfo.cs
@@ -3,7 +3,6 @@ using System.Collections.ObjectModel; using System.Text.Json.Nodes; using ArcaneLibs; using LibMatrix; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers; using LibMatrix.RoomTypes; @@ -13,13 +12,13 @@ namespace MatrixUtils.Abstractions; public class RoomInfo : NotifyPropertyChanged { public RoomInfo(GenericRoom room) { Room = room; - _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); + // _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); RegisterEventListener(); } public RoomInfo(GenericRoom room, List<StateEventResponse>? stateEvents) { Room = room; - _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); + // _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); if (stateEvents is { Count: > 0 }) StateEvents = new(stateEvents!); RegisterEventListener(); ProcessNewItems(stateEvents!); @@ -30,7 +29,7 @@ public class RoomInfo : NotifyPropertyChanged { public ObservableCollection<StateEventResponse?> Timeline { get; private set; } = new(); private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new(); - private static SvgIdenticonGenerator identiconGenerator = new(); + // private static SvgIdenticonGenerator identiconGenerator = new(); public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") { if (homeserversWithoutEventFormatSupport.Contains(Room.Homeserver)) return await GetStateEventForged(type, stateKey); @@ -96,7 +95,7 @@ public class RoomInfo : NotifyPropertyChanged { } public string? RoomIcon { - get => _roomIcon ?? _fallbackIcon; + get => _roomIcon; set => SetField(ref _roomIcon, value); } diff --git a/MatrixUtils.Desktop/App.axaml.cs b/MatrixUtils.Desktop/App.axaml.cs
index 3a106ab..8a5d3e2 100644 --- a/MatrixUtils.Desktop/App.axaml.cs +++ b/MatrixUtils.Desktop/App.axaml.cs
@@ -15,7 +15,6 @@ public partial class App : Application { public override void OnFrameworkInitializationCompleted() { host = Host.CreateDefaultBuilder().ConfigureServices((ctx, services) => { services.AddSingleton<RMUDesktopConfiguration>(); - services.AddSingleton<SentryService>(); services.AddSingleton<TieredStorageService>(x => new TieredStorageService( cacheStorageProvider: new FileStorageProvider(x.GetService<RMUDesktopConfiguration>()!.CacheStoragePath), @@ -40,10 +39,10 @@ public partial class App : Application { var scope = scopeFac.CreateScope(); desktop.MainWindow = scope.ServiceProvider.GetRequiredService<MainWindow>(); } - - if(Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false) + + if (Environment.GetEnvironmentVariable("AVALONIA_THEME")?.Equals("dark", StringComparison.OrdinalIgnoreCase) ?? false) RequestedThemeVariant = ThemeVariant.Dark; - + base.OnFrameworkInitializationCompleted(); } } \ No newline at end of file diff --git a/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
index 1e4a127..1e6b99f 100644 --- a/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs +++ b/MatrixUtils.Desktop/Components/RoomListEntry.axaml.cs
@@ -44,7 +44,7 @@ public partial class RoomListEntry : UserControl { var avatarEvent = await Room.GetStateEvent("m.room.avatar"); if (avatarEvent?.TypedContent is RoomAvatarEventContent avatarData) { var mxcUrl = avatarData.Url; - var resolvedUrl = await Room.Room.GetResolvedRoomAvatarUrlAsync(); + var resolvedUrl = await Room.Room.GetAvatarUrlAsync(); // await using var svc = _serviceScopeFactory.CreateAsyncScope(); // var hs = await svc.ServiceProvider.GetService<RMUStorageWrapper>()?.GetCurrentSessionOrPrompt()!; @@ -54,10 +54,10 @@ public partial class RoomListEntry : UserControl { var storage = new FileStorageProvider("cache"); var storageKey = $"media/{mxcUrl.Replace("mxc://", "").Replace("/", ".")}"; try { - if (!await storage.ObjectExistsAsync(storageKey)) - await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl)); + // if (!await storage.ObjectExistsAsync(storageKey)) + // await storage.SaveStreamAsync(storageKey, await hc.GetStreamAsync(resolvedUrl)); - RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException()); + // RoomIcon.Source = new Bitmap(await storage.LoadStreamAsync(storageKey) ?? throw new NullReferenceException()); } catch (IOException) { } catch (MatrixException e) { diff --git a/MatrixUtils.Desktop/MainWindow.axaml.cs b/MatrixUtils.Desktop/MainWindow.axaml.cs
index 9c783e4..a1eef56 100644 --- a/MatrixUtils.Desktop/MainWindow.axaml.cs +++ b/MatrixUtils.Desktop/MainWindow.axaml.cs
@@ -14,7 +14,7 @@ public partial class MainWindow : Window { private readonly RMUDesktopConfiguration _configuration; public static MainWindow Instance { get; private set; } = null!; - public MainWindow(ILogger<MainWindow> logger, IServiceScopeFactory scopeFactory, SentryService _) { + public MainWindow(ILogger<MainWindow> logger, IServiceScopeFactory scopeFactory) { Instance = this; _logger = logger; _scopeFactory = scopeFactory; diff --git a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
index ce009d5..419ae88 100644 --- a/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj +++ b/MatrixUtils.Desktop/MatrixUtils.Desktop.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <BuiltInComInteropSupport>true</BuiltInComInteropSupport> <ApplicationManifest>app.manifest</ApplicationManifest> @@ -10,31 +10,28 @@ <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <InvariantGlobalization>true</InvariantGlobalization> -<!-- <PublishTrimmed>true</PublishTrimmed>--> -<!-- <PublishReadyToRun>true</PublishReadyToRun>--> -<!-- <PublishSingleFile>true</PublishSingleFile>--> -<!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> -<!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> -<!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> + <!-- <PublishTrimmed>true</PublishTrimmed>--> + <!-- <PublishReadyToRun>true</PublishReadyToRun>--> + <!-- <PublishSingleFile>true</PublishSingleFile>--> + <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> + <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> + <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> </PropertyGroup> <ItemGroup> - <PackageReference Include="Avalonia" Version="11.0.10" /> - <PackageReference Include="Avalonia.Desktop" Version="11.0.10" /> - <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" /> - <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" /> + <PackageReference Include="Avalonia" Version="11.3.8"/> + <PackageReference Include="Avalonia.Desktop" Version="11.3.8"/> + <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.8"/> + <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8"/> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> - <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" /> - <PackageReference Include="Sentry" Version="4.7.0" /> + <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1"/> </ItemGroup> - - <ItemGroup> - <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.10.9" /> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> + <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.3.0.6"/> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/> </ItemGroup> <ItemGroup> <Content Include="appsettings*.json"> @@ -45,6 +42,6 @@ </Content> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj" /> + <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj"/> </ItemGroup> </Project> diff --git a/MatrixUtils.Desktop/RMUDesktopConfiguration.cs b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
index 62646ca..f9515f6 100644 --- a/MatrixUtils.Desktop/RMUDesktopConfiguration.cs +++ b/MatrixUtils.Desktop/RMUDesktopConfiguration.cs
@@ -21,7 +21,6 @@ public class RMUDesktopConfiguration { public string DataStoragePath { get; set; } = ""; public string CacheStoragePath { get; set; } = ""; - public string? SentryDsn { get; set; } private static string ExpandPath(string path, bool retry = true) { _logger.LogInformation("Expanding path `{}`", path); @@ -44,4 +43,4 @@ public class RMUDesktopConfiguration { return path; } -} +} \ No newline at end of file diff --git a/MatrixUtils.Desktop/SentryService.cs b/MatrixUtils.Desktop/SentryService.cs deleted file mode 100644
index c965632..0000000 --- a/MatrixUtils.Desktop/SentryService.cs +++ /dev/null
@@ -1,29 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Sentry; - -namespace MatrixUtils.Desktop; - -public class SentryService : IDisposable { - private IDisposable? _sentrySdkDisposable; - public SentryService(IServiceScopeFactory scopeFactory, ILogger<SentryService> logger) { - var config = scopeFactory.CreateScope().ServiceProvider.GetRequiredService<RMUDesktopConfiguration>(); - if (config.SentryDsn is null) { - logger.LogWarning("Sentry DSN is not set, skipping Sentry initialisation"); - return; - } - _sentrySdkDisposable = SentrySdk.Init(o => { - o.Dsn = config.SentryDsn; - // When configuring for the first time, to see what the SDK is doing: - o.Debug = true; - // Set traces_sample_rate to 1.0 to capture 100% of transactions for performance monitoring. - // We recommend adjusting this value in production. - o.TracesSampleRate = 1.0; - // Enable Global Mode if running in a client app - o.IsGlobalModeEnabled = true; - }); - } - - /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> - public void Dispose() => _sentrySdkDisposable?.Dispose(); -} diff --git a/MatrixUtils.Desktop/appsettings.Development.json b/MatrixUtils.Desktop/appsettings.Development.json
index a1add03..baec0e2 100644 --- a/MatrixUtils.Desktop/appsettings.Development.json +++ b/MatrixUtils.Desktop/appsettings.Development.json
@@ -1,14 +1,13 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, - "RMUDesktop": { - "DataStoragePath": "rmu-desktop/data", - "CacheStoragePath": "rmu-desktop/cache", - "SentryDsn": "https://a41e99dd2fdd45f699c432b21ebce632@sentry.thearcanebrony.net/15" + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" } + }, + "RMUDesktop": { + "DataStoragePath": "rmu-desktop/data", + "CacheStoragePath": "rmu-desktop/cache" + } } diff --git a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
index 4b0f599..96c3bff 100644 --- a/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj +++ b/MatrixUtils.DmSpaced/MatrixUtils.DmSpaced.csproj
@@ -1,31 +1,31 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> - <LangVersion>preview</LangVersion> - <ImplicitUsings>enable</ImplicitUsings> - <Nullable>enable</Nullable> - <PublishAot>false</PublishAot> - <InvariantGlobalization>true</InvariantGlobalization> - <!-- <PublishTrimmed>true</PublishTrimmed>--> - <!-- <PublishReadyToRun>true</PublishReadyToRun>--> - <!-- <PublishSingleFile>true</PublishSingleFile>--> - <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> - <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> - <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> - </PropertyGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net10.0</TargetFramework> + <LangVersion>preview</LangVersion> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <PublishAot>false</PublishAot> + <InvariantGlobalization>true</InvariantGlobalization> + <!-- <PublishTrimmed>true</PublishTrimmed>--> + <!-- <PublishReadyToRun>true</PublishReadyToRun>--> + <!-- <PublishSingleFile>true</PublishSingleFile>--> + <!-- <PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>--> + <!-- <PublishTrimmedShowLinkerSizeComparison>true</PublishTrimmedShowLinkerSizeComparison>--> + <!-- <PublishTrimmedShowLinkerSizeComparisonWarnings>true</PublishTrimmedShowLinkerSizeComparisonWarnings>--> + </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" /> - </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj"/> + </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> - </ItemGroup> - <ItemGroup> - <Content Include="appsettings*.json"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - </Content> - </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/> + </ItemGroup> + <ItemGroup> + <Content Include="appsettings*.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + </ItemGroup> </Project> diff --git a/MatrixUtils.DmSpaced/Program.cs b/MatrixUtils.DmSpaced/Program.cs
index ae352b7..6ed6cbc 100644 --- a/MatrixUtils.DmSpaced/Program.cs +++ b/MatrixUtils.DmSpaced/Program.cs
@@ -22,17 +22,17 @@ if (Environment.GetEnvironmentVariable("MODERATIONBOT_APPSETTINGS_PATH") is stri builder.ConfigureAppConfiguration(x => x.AddJsonFile(path)); var host = builder.ConfigureServices((_, services) => { - services.AddScoped<TieredStorageService>(x => - new TieredStorageService( - cacheStorageProvider: new FileStorageProvider("bot_data/cache/"), - dataStorageProvider: new FileStorageProvider("bot_data/data/") - ) - ); + // services.AddScoped<TieredStorageService>(x => + // new TieredStorageService( + // cacheStorageProvider: new FileStorageProvider("bot_data/cache/"), + // dataStorageProvider: new FileStorageProvider("bot_data/data/") + // ) + // ); services.AddSingleton<ModerationBotConfiguration>(); services.AddRoryLibMatrixServices(); - services.AddSingleton<ModerationBotRoomProvider>(); + // services.AddSingleton<ModerationBotRoomProvider>(); services.AddHostedService<ModerationBot.ModerationBot>(); }).UseConsoleLifetime().Build(); diff --git a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
index e2c8192..1186b6c 100644 --- a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs +++ b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
@@ -1,7 +1,5 @@ -using System.Net; using ArcaneLibs.Extensions; using LibMatrix; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers; using LibMatrix.Responses; @@ -60,10 +58,10 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI var (userId, dmRooms) = entry; DMSpaceChildLayer? layer = await GetStateOrNullAsync<DMSpaceChildLayer>(DMSpaceChildLayer.EventId, userId.UrlEncode()) ?? await CreateLayer(userId); return (entry, layer); - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var ((userId, dmRooms), layer) in layerTasks) { - var space = Homeserver.GetRoom(layer.SpaceId).AsSpace; + var space = Homeserver.GetRoom(layer.SpaceId).AsSpace(); foreach (var roomid in dmRooms) { var dri = new DMRoomInfo() { AttributedUser = userId @@ -119,12 +117,11 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI catch { return (x, null); } - - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var (layer, profile) in getProfileTasks) { if (profile is null) continue; var layerContent = layer.TypedContent as DMSpaceChildLayer; - var space = Homeserver.GetRoom(layerContent!.SpaceId).AsSpace; + var space = Homeserver.GetRoom(layerContent!.SpaceId).AsSpace(); try { await space.SendStateEventAsync(RoomAvatarEventContent.EventId, "", new RoomAvatarEventContent() { @@ -142,7 +139,7 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI private async Task UpdateLayer(DMSpaceChildLayer layer, string mxid) { UserProfileResponse? profile = null; - var space = Homeserver.GetRoom(layer.SpaceId).AsSpace; + var space = Homeserver.GetRoom(layer.SpaceId).AsSpace(); if (string.IsNullOrWhiteSpace(layer.OverrideAvatar) || string.IsNullOrWhiteSpace(layer.OverrideName)) { try { diff --git a/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj b/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
index 72c1666..225b264 100644 --- a/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj +++ b/MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LinkIncremental>true</LinkIncremental> diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
index bc595b5..f7e1e20 100644 --- a/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs +++ b/MatrixUtils.LibDMSpace/StateEvents/DMRoomInfo.cs
@@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using LibMatrix.EventTypes; -using LibMatrix.Interfaces; namespace MatrixUtils.LibDMSpace.StateEvents; diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs
index 16c7b70..886c34d 100644 --- a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs +++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceChildLayer.cs
@@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using LibMatrix.EventTypes; -using LibMatrix.Interfaces; namespace MatrixUtils.LibDMSpace.StateEvents; diff --git a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
index f5daa74..170efc7 100644 --- a/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs +++ b/MatrixUtils.LibDMSpace/StateEvents/DMSpaceInfo.cs
@@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using LibMatrix.EventTypes; -using LibMatrix.Interfaces; namespace MatrixUtils.LibDMSpace.StateEvents; diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs new file mode 100644
index 0000000..abae488 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteAllRoomsCommand.cs
@@ -0,0 +1,32 @@ +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class DevDeleteAllRoomsCommand(ILogger<DevDeleteAllRoomsCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + var synapse = hs as AuthenticatedHomeserverSynapse; + await foreach (var room in synapse.Admin.SearchRoomsAsync()) + { + try + { + await synapse.Admin.DeleteRoom(room.RoomId, new() { ForcePurge = true }); + Console.WriteLine($"Deleted room: {room.RoomId}"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete room {room.RoomId}: {ex.Message}"); + } + } + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: execute [filename]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs new file mode 100644
index 0000000..10d667f --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevDeleteRoomCommand.cs
@@ -0,0 +1,33 @@ +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class DevDeleteRoomCommand(ILogger<DevDeleteRoomCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + var synapse = hs as AuthenticatedHomeserverSynapse; + if (ctx.Args.Length == 2) { + var room = synapse.GetRoom(ctx.Args[1]); + await synapse.Admin.DeleteRoom(room.RoomId, new() { Purge = true }); + } + else { + string line; + do { + line = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(line)) continue; + var room = synapse.GetRoom(line); + await synapse.Admin.DeleteRoom(room.RoomId, new() { Purge = true }); + } while (line is not null); + } + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: execute [filename]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs new file mode 100644
index 0000000..7ff7b6a --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/DevCommands/DevGetRoomDirStateCommand.cs
@@ -0,0 +1,31 @@ +using System.Web; +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class DevGetRoomDirStateCommand(ILogger<DevGetRoomDirStateCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + var synapse = hs as AuthenticatedHomeserverSynapse; + if (ctx.Args.Length == 2) { + var res = await hs.ClientHttpClient.GetAsync(" /_matrix/client/v3/directory/list/room/" + HttpUtility.UrlEncode(ctx.Args[1])); + if (res.IsSuccessStatusCode) { + var data = await res.Content.ReadAsStringAsync(); + Console.WriteLine("Room Directory State for " + ctx.Args[1] + ":"); + Console.WriteLine(data); + } else { + Console.WriteLine("Failed to get room directory state for " + ctx.Args[1] + ": " + res.ReasonPhrase); + } + } + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: execute [filename]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs new file mode 100644
index 0000000..41c8cca --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs
@@ -0,0 +1,63 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class ExecuteCommand(ILogger<ExecuteCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + if (ctx.Args.Length <= 1) { + await PrintHelp(); + return; + } + var filename = ctx.Args[1]; + if (filename.StartsWith("--")) { + Console.WriteLine("Filename cannot start with --, please provide a valid filename."); + await PrintHelp(); + } + + if (Directory.Exists(filename)) { + await ExecuteDirectory(filename); + } + else if (File.Exists(filename)) { + await ExecuteFile(filename); + } + else { + Console.WriteLine($"File or directory {filename} does not exist."); + await PrintHelp(); + } + + await host.StopAsync(cancellationToken); + } + + public async Task ExecuteFile(string filename) { + var rbj = await JsonSerializer.DeserializeAsync<JsonObject>(File.OpenRead(filename)); + var rb = rbj.ContainsKey(nameof(RoomUpgradeBuilder.OldRoomId)) + ? rbj.Deserialize<RoomUpgradeBuilder>() + : rbj.Deserialize<RoomBuilder>(); + Console.WriteLine($"Executing room builder file of type {rb.GetType().Name}..."); + await rb!.Create(hs); + } + + public async Task ExecuteDirectory(string dirName) { + if (!Directory.Exists(dirName)) { + Console.WriteLine($"Directory {dirName} does not exist."); + return; + } + var files = Directory.GetFiles(dirName, "*.json"); + foreach (var file in files) { + Console.WriteLine($"Executing file: {file}"); + await ExecuteFile(file); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: execute [filename]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs new file mode 100644
index 0000000..960905b --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/ImportUpgradeStateCommand.cs
@@ -0,0 +1,35 @@ +using System.Text.Json; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class ImportUpgradeStateCommand(ILogger<ImportUpgradeStateCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + if (ctx.Args.Length <= 1) { + await PrintHelp(); + return; + } + var filename = ctx.Args[1]; + if (filename.StartsWith("--")) { + Console.WriteLine("Filename cannot start with --, please provide a valid filename."); + await PrintHelp(); + } + + var rb = await JsonSerializer.DeserializeAsync<RoomUpgradeBuilder>(File.OpenRead(filename)); + await rb!.ImportAsync(hs.GetRoom(rb.OldRoomId)); + await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken); + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: import-upgrade-state [filename]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs new file mode 100644
index 0000000..3860448 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/ModifyCommand.cs
@@ -0,0 +1,39 @@ +using System.Text.Json; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using MatrixUtils.RoomUpgradeCLI.Extensions; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class ModifyCommand(ILogger<ModifyCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + if (ctx.Args.Length <= 2 || ctx.Args.Contains("--help")) { + await PrintHelp(); + return; + } + + var filename = ctx.Args[1]; + if (filename.StartsWith("--")) { + Console.WriteLine("Filename cannot start with --, please provide a valid filename."); + await PrintHelp(); + } + + var rb = ctx.Args.Contains("--upgrade") + ? await JsonSerializer.DeserializeAsync<RoomUpgradeBuilder>(File.OpenRead(filename), cancellationToken: cancellationToken) + : await JsonSerializer.DeserializeAsync<RoomBuilder>(File.OpenRead(filename), cancellationToken: cancellationToken); + await rb!.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: false); + await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken); + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: new [filename] [options]"); + Console.WriteLine("Options:"); + + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs new file mode 100644
index 0000000..08daf71 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/NewFileCommand.cs
@@ -0,0 +1,78 @@ +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using MatrixUtils.RoomUpgradeCLI.Extensions; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class NewFileCommand(ILogger<NewFileCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + var rb = ctx.Args.Contains("--upgrade") ? new RoomUpgradeBuilder() : new RoomBuilder(); + if (ctx.Args.Length <= 1) { + await PrintHelp(); + return; + } + var filename = ctx.Args[1]; + if (filename.StartsWith("--")) { + Console.WriteLine("Filename cannot start with --, please provide a valid filename."); + await PrintHelp(); + } + await rb.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: true); + // check for room membership! + if (rb is RoomUpgradeBuilder rub) { + try { + var room = hs.GetRoom(rub.OldRoomId); + var membership = await room.GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.UserId); + } + catch (Exception e) { + Console.WriteLine("Error checking room membership: " + e.Message); + Console.WriteLine("Please ensure you are a member of the room you are trying to upgrade. -- ABORTING --"); + await host.StopAsync(); + return; + } + } + await File.WriteAllTextAsync(filename, rb.ToJson(), cancellationToken); + + await host.StopAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: new [filename] [options]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + Console.WriteLine(" --version <version> Set the room version (e.g. 9, 10, 11, 12)"); + Console.WriteLine("-- New room options --"); + Console.WriteLine(" --alias <alias> Set the room alias (local part)"); + Console.WriteLine(" --avatar-url <url> Set the room avatar URL"); + Console.WriteLine(" --copy-avatar <roomId> Copy the avatar from an existing room"); + Console.WriteLine(" --copy-powerlevels <roomId> Copy power levels from an existing room"); + Console.WriteLine(" --invite-admin <userId> Invite a user as an admin (userId must start with '@')"); + Console.WriteLine(" --invite <userId> Invite a user (userId must start with '@')"); + Console.WriteLine(" --name <name> Set the room name (can be multiple words)"); + Console.WriteLine(" --topic <topic> Set the room topic (can be multiple words)"); + Console.WriteLine(" --federate <true|false> Set whether the room is federatable"); + Console.WriteLine(" --public Set the room join rule to public"); + Console.WriteLine(" --invite-only Set the room join rule to invite-only"); + Console.WriteLine(" --knock Set the room join rule to knock"); + Console.WriteLine(" --restricted Set the room join rule to restricted"); + Console.WriteLine(" --knock_restricted Set the room join rule to knock_restricted"); + Console.WriteLine(" --private Set the room join rule to private"); + Console.WriteLine(" --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private)"); + Console.WriteLine(" --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable)"); + Console.WriteLine(" --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.)"); + // upgrade opts + Console.WriteLine("-- Upgrade options --"); + Console.WriteLine(" --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options"); + Console.WriteLine(" --invite-members Invite members during room upgrade"); + Console.WriteLine(" --invite-powerlevel-users Invite users with power levels during room upgrade"); + Console.WriteLine(" --migrate-bans Migrate bans during room upgrade"); + Console.WriteLine(" --migrate-empty-state-events Migrate empty state events during room upgrade"); + Console.WriteLine(" --upgrade-unstable-values Upgrade unstable values during room upgrade"); + Console.WriteLine(" --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list"); + Console.WriteLine("WARNING: The --upgrade option is incompatible with options listed under \"New room\", please use the equivalent options in the `modify` command instead."); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs b/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs new file mode 100644
index 0000000..40ab791 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/NewFromRoomDirCommand.cs
@@ -0,0 +1,115 @@ +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using MatrixUtils.RoomUpgradeCLI.Extensions; + +namespace MatrixUtils.RoomUpgradeCLI.Commands; + +public class NewFromRoomDirCommand(ILogger<NewFromRoomDirCommand> logger, IHost host, RuntimeContext ctx, AuthenticatedHomeserverGeneric hs) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + if (ctx.Args.Length <= 1) { + await PrintHelp(); + return; + } + + var dirName = ctx.Args[1]; + if (dirName.StartsWith("--")) { + Console.WriteLine("Directory name cannot start with --, please provide a valid directory name."); + await PrintHelp(); + } + + if (Directory.Exists(dirName)) + Directory.Delete(dirName, true); + Directory.CreateDirectory(dirName); + List<Task> tasks = []; + await foreach (var rooms in hs.EnumeratePublicRoomsAsync().WithCancellation(cancellationToken)) { + // foreach (var room in rooms.Chunk) { } + tasks.AddRange(rooms.Chunk.Select(x=> ProcessRoom(dirName, x))); + } + await Task.WhenAll(tasks); + + // var rb = ctx.Args.Contains("--upgrade") ? new RoomUpgradeBuilder() : new RoomBuilder(); + // + // // check for room membership! + // if (rb is RoomUpgradeBuilder rub) { + + // } + await host.StopAsync(cancellationToken); + } + + private async Task ProcessRoom(string dirName, PublicRoomDirectoryResult.PublicRoomListItem roomListItem) { + Console.WriteLine(roomListItem.Name ?? roomListItem.RoomId); + var room = hs.GetRoom(roomListItem.RoomId); + var rb = new RoomUpgradeBuilder() { + OldRoomId = roomListItem.RoomId + }; + + await rb.ApplyRoomUpgradeCLIArgs(hs, ctx.Args[2..], isNewState: true); + try { + var membership = await room.GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, hs.UserId); + } + catch (Exception e) { + Console.WriteLine("Error checking room membership: " + e.Message); + Console.WriteLine("Please ensure you are a member of the room you are trying to upgrade. -- ABORTING --"); + await host.StopAsync(); + return; + } + + await rb.ImportAsync(hs.GetRoom(roomListItem.RoomId)); + + var validFileNameChars = (roomListItem.Name ?? roomListItem.CanonicalAlias ?? roomListItem.RoomId) + // .Replace('&', '_') + // .Replace(':', '_') + // .Replace('\'', '_') + // .Replace(' ', '_') + .ToList(); + validFileNameChars.RemoveAll(Path.GetInvalidFileNameChars().Contains); + var filename = string.Join("", validFileNameChars); + while (File.Exists(filename)) + filename += "_"; + + await File.WriteAllTextAsync(dirName + "/" + filename + ".json", rb.ToJson()); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private async Task PrintHelp() { + Console.WriteLine("Usage: new [filename] [options]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --help Show this help message"); + Console.WriteLine(" --version <version> Set the room version (e.g. 9, 10, 11, 12)"); + Console.WriteLine("-- New room options --"); + Console.WriteLine(" --alias <alias> Set the room alias (local part)"); + Console.WriteLine(" --avatar-url <url> Set the room avatar URL"); + Console.WriteLine(" --copy-avatar <roomId> Copy the avatar from an existing room"); + Console.WriteLine(" --copy-powerlevels <roomId> Copy power levels from an existing room"); + Console.WriteLine(" --invite-admin <userId> Invite a user as an admin (userId must start with '@')"); + Console.WriteLine(" --invite <userId> Invite a user (userId must start with '@')"); + Console.WriteLine(" --name <name> Set the room name (can be multiple words)"); + Console.WriteLine(" --topic <topic> Set the room topic (can be multiple words)"); + Console.WriteLine(" --federate <true|false> Set whether the room is federatable"); + Console.WriteLine(" --public Set the room join rule to public"); + Console.WriteLine(" --invite-only Set the room join rule to invite-only"); + Console.WriteLine(" --knock Set the room join rule to knock"); + Console.WriteLine(" --restricted Set the room join rule to restricted"); + Console.WriteLine(" --knock_restricted Set the room join rule to knock_restricted"); + Console.WriteLine(" --private Set the room join rule to private"); + Console.WriteLine(" --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private)"); + Console.WriteLine(" --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable)"); + Console.WriteLine(" --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.)"); + // upgrade opts + Console.WriteLine("-- Upgrade options --"); + Console.WriteLine( + " --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options"); + Console.WriteLine(" --invite-members Invite members during room upgrade"); + Console.WriteLine(" --invite-powerlevel-users Invite users with power levels during room upgrade"); + Console.WriteLine(" --migrate-bans Migrate bans during room upgrade"); + Console.WriteLine(" --migrate-empty-state-events Migrate empty state events during room upgrade"); + Console.WriteLine(" --upgrade-unstable-values Upgrade unstable values during room upgrade"); + Console.WriteLine(" --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list"); + Console.WriteLine( + "WARNING: The --upgrade option is incompatible with options listed under \"New room\", please use the equivalent options in the `modify` command instead."); + await host.StopAsync(); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs new file mode 100644
index 0000000..75852bc --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs
@@ -0,0 +1,252 @@ +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; + +namespace MatrixUtils.RoomUpgradeCLI.Extensions; + +public static class RoomBuilderExtensions { + public static async Task ApplyRoomUpgradeCLIArgs(this RoomBuilder rb, AuthenticatedHomeserverGeneric hs, string[] args, bool isNewState = false) { + for (int i = 0; i < args.Length; i++) { + // Console.WriteLine($"Parsing arg {i}: {args[i]}"); + switch (args[i]) { + case "--alias": + rb.AliasLocalPart = args[++i]; + break; + case "--avatar-url": + rb.Avatar!.Url = args[++i]; + break; + case "--copy-avatar": { + var room = hs.GetRoom(args[++i]); + rb.Avatar = await room.GetAvatarUrlAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have an avatar"); + break; + } + case "--copy-powerlevels": { + var room = hs.GetRoom(args[++i]); + rb.PowerLevels = await room.GetPowerLevelsAsync() ?? throw new ArgumentException($"Room {room.RoomId} does not have power levels???"); + break; + } + case "--invite-admin": + var inviteAdmin = args[++i]; + if (!inviteAdmin.StartsWith('@')) { + throw new ArgumentException("Invalid user reference: " + inviteAdmin); + } + + rb.Invites.Add(inviteAdmin, "Marked explicitly as admin to be invited"); + break; + case "--invite": + var inviteUser = args[++i]; + if (!inviteUser.StartsWith('@')) { + throw new ArgumentException("Invalid user reference: " + inviteUser); + } + + rb.Invites.Add(inviteUser, "Marked explicitly to be invited"); + break; + case "--name": + var nameEvt = rb.Name = new() { Name = "" }; + while (i + 1 < args.Length && !args[i + 1].StartsWith("--")) { + nameEvt.Name += (nameEvt.Name.Length > 0 ? " " : "") + args[++i]; + } + + break; + case "--topic": + var topicEvt = rb.Topic = new() { Topic = "" }; + while (i + 1 < args.Length && !args[i + 1].StartsWith("--")) { + topicEvt.Topic += (topicEvt.Topic.Length > 0 ? " " : "") + args[++i]; + } + + break; + case "--federate": + rb.IsFederatable = GetBoolArg(args, ref i, true); + break; + case "--public": + case "--invite-only": + case "--knock": + case "--restricted": + case "--knock_restricted": + case "--private": + rb.JoinRules.JoinRule = args[i].Replace("--", "").ToLowerInvariant() switch { + "public" => RoomJoinRulesEventContent.JoinRules.Public, + "invite-only" => RoomJoinRulesEventContent.JoinRules.Invite, + "knock" => RoomJoinRulesEventContent.JoinRules.Knock, + "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted, + "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted, + "private" => RoomJoinRulesEventContent.JoinRules.Private, + _ => throw new ArgumentException("Unknown join rule: " + args[i]) + }; + break; + case "--join-rule": + if (i + 1 >= args.Length || !args[i + 1].StartsWith("--")) { + throw new ArgumentException("Expected join rule after --join-rule"); + } + + rb.JoinRules.JoinRule = args[++i].ToLowerInvariant() switch { + "public" => RoomJoinRulesEventContent.JoinRules.Public, + "invite" => RoomJoinRulesEventContent.JoinRules.Invite, + "knock" => RoomJoinRulesEventContent.JoinRules.Knock, + "restricted" => RoomJoinRulesEventContent.JoinRules.Restricted, + "knock_restricted" => RoomJoinRulesEventContent.JoinRules.KnockRestricted, + "private" => RoomJoinRulesEventContent.JoinRules.Private, + _ => throw new ArgumentException("Unknown join rule: " + args[i]) + }; + break; + case "--history-visibility": + rb.HistoryVisibility = new RoomHistoryVisibilityEventContent { + HistoryVisibility = args[++i].ToLowerInvariant() switch { + "shared" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared, + "invited" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Invited, + "joined" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Joined, + "world_readable" => RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.WorldReadable, + _ => throw new ArgumentException("Unknown history visibility: " + args[i]) + } + }; + break; + case "--type": + rb.Type = args[++i]; + break; + case "--version": + rb.Version = args[++i]; + // if (!RoomBuilder.V12PlusRoomVersions.Contains(rb.Version)) { + // logger.LogWarning("Using room version {Version} which is not v12 or higher, this may cause issues with some features.", rb.Version); + // } + break; + case "--encryption": + if (args[i + 1].StartsWith("--")) { + rb.Encryption.Algorithm = "m.megolm.v1.aes-sha2"; + } + else { + rb.Encryption.Algorithm = args[++i]; + if (rb.Encryption.Algorithm == "null") + rb.Encryption.Algorithm = null; // disable encryption + } + + break; + // upgrade options + case "--invite-members": + if (rb is not RoomUpgradeBuilder upgradeBuilder) { + throw new InvalidOperationException("Invite members can only be used with room upgrades"); + } + + upgradeBuilder.UpgradeOptions.InviteMembers = GetBoolArg(args, ref i, true); + break; + case "--invite-powerlevel-users": + case "--invite-power-level-users": + if (rb is not RoomUpgradeBuilder upgradeBuilderInvite) { + throw new InvalidOperationException("Invite powerlevel users can only be used with room upgrades"); + } + + upgradeBuilderInvite.UpgradeOptions.InvitePowerlevelUsers = GetBoolArg(args, ref i, true); + break; + case "--synapse-admin-join-local-users": + rb.SynapseAdminAutoAcceptLocalInvites = GetBoolArg(args, ref i, true); + break; + case "--migrate-bans": + if (rb is not RoomUpgradeBuilder upgradeBuilderBan) { + throw new InvalidOperationException("Migrate bans can only be used with room upgrades"); + } + + upgradeBuilderBan.UpgradeOptions.MigrateBans = GetBoolArg(args, ref i, true); + break; + case "--migrate-empty-state-events": + if (rb is not RoomUpgradeBuilder upgradeBuilderEmpty) { + throw new InvalidOperationException("Migrate empty state events can only be used with room upgrades"); + } + + upgradeBuilderEmpty.UpgradeOptions.MigrateEmptyStateEvents = GetBoolArg(args, ref i, true); + break; + case "--upgrade-unstable-values": + if (rb is not RoomUpgradeBuilder upgradeBuilderUnstable) { + throw new InvalidOperationException("Update unstable values can only be used with room upgrades"); + } + + upgradeBuilderUnstable.UpgradeOptions.UpgradeUnstableValues = GetBoolArg(args, ref i, true); + break; + case "--msc4321-policy-list-upgrade": + if (rb is not RoomUpgradeBuilder upgradeBuilderPolicy) { + throw new InvalidOperationException("MSC4321 policy list upgrade can only be used with room upgrades"); + } + + upgradeBuilderPolicy.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable = true; + upgradeBuilderPolicy.UpgradeOptions.Msc4321PolicyListUpgradeOptions.UpgradeType = args[++i].ToLowerInvariant() switch { + "move" => RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Move, + "transition" => RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition, + _ => throw new ArgumentException("Unknown MSC4321 policy list upgrade type: " + args[i]) + }; + break; + case "--force-upgrade": + if (rb is not RoomUpgradeBuilder upgradeBuilderForce) { + throw new InvalidOperationException("Force upgrade can only be used with room upgrades"); + } + + upgradeBuilderForce.UpgradeOptions.ForceUpgrade = GetBoolArg(args, ref i, true); + break; + case "--noop-upgrade": + if (rb is not RoomUpgradeBuilder upgradeBuilderNoop) { + throw new InvalidOperationException("No-op upgrade can only be used with room upgrades"); + } + + upgradeBuilderNoop.UpgradeOptions.NoopUpgrade = GetBoolArg(args, ref i, true); + break; + case "--upgrade": + if (rb is not RoomUpgradeBuilder upgradeBuilderUpgrade) { + throw new InvalidOperationException("Upgrade can only be used with room upgrades"); + } + + if (isNewState) { + upgradeBuilderUpgrade.OldRoomId = args[++i]; + Console.WriteLine($"Popping arg for --upgrade(isNewState={isNewState}): " + upgradeBuilderUpgrade.OldRoomId); + } + + break; + case "--help": + PrintHelpAndExit(); + return; + default: + throw new ArgumentException("Unknown argument: " + args[i]); + } + } + } + + private static bool GetBoolArg(string[] args, ref int i, bool defaultValue) { + if (i + 1 < args.Length && bool.TryParse(args[i + 1], out var result)) { + i++; + return result; + } + + return defaultValue; + } + + private static void PrintHelpAndExit() { + Console.WriteLine(""" + --help Show this help message + --version <version> Set the room version (e.g. 9, 10, 11, 12) + -- New room options -- + --federate [True|false] Set whether the room is federatable [WARNING: Cannot be updated later!] + --type <type> Set the room type (e.g. m.space, m.room, support.feline.policy.list.msc.v1 etc.) [WARNING: Cannot be updated later!] + --alias <alias> Set the room alias (local part) + --avatar-url <url> Set the room avatar URL + --copy-avatar <roomId> Copy the avatar from an existing room + --copy-powerlevels <roomId> Copy power levels from an existing room + --invite <userId> Invite a user (userId must start with '@') + --invite-admin <userId> Invite a user as an admin (userId must start with '@') + --synapse-admin-join-local-users [True|false] Automatically accept local user invites during room creation (Synapse only, requires synapse admin access) + --name <name> Set the room name (can be multiple words) + --topic <topic> Set the room topic (can be multiple words) + --join-rule <rule> Set the room join rule (public, invite, knock, restricted, knock_restricted, private) + Aliases: --public, --invite, --knock, --restricted, --knock_restricted, --private + --history-visibility <visibility> Set the room history visibility (shared, invited, joined, world_readable) + -- Upgrade options -- + --upgrade <roomId> Create a room upgrade file instead of a new room file - WARNING: incompatible with non-upgrade options + --invite-members [True|false] Invite members during room upgrade + --invite-local-users [True|false] Invite local users during room upgrade (also see --synapse-admin-join-local-users) + --invite-powerlevel-users [True|false] Invite users with power levels during room upgrade + --migrate-bans [True|false] Migrate bans during room upgrade + --migrate-empty-state-events [True|false] Migrate empty state events during room upgrade + --upgrade-unstable-values [True|false] Upgrade unstable values during room upgrade + --msc4321-policy-list-upgrade <move|transition> Upgrade MSC4321 policy list + --force-upgrade [True|false] Force upgrade even if you don't have the required permissions + --noop-upgrade [True|false] Perform the upgrade, but do not tombstone the old room + WARNING: The --upgrade option is incompatible with options listed under "New room", please use the equivalent options in the `modify` command instead. + """); + Environment.Exit(0); + } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj new file mode 100644
index 0000000..f349c81 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj
@@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk.Worker"> + + <PropertyGroup> + <TargetFramework>net10.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <UserSecretsId>dotnet-MatrixUtils.RoomUpgradeCLI-19ffcbc3-eeaa-4cef-b398-0db2008ca04b</UserSecretsId> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-rc.2.25502.107"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/> + <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj"/> + </ItemGroup> + + <ItemGroup> + <Folder Include="tmp\"/> + </ItemGroup> +</Project> diff --git a/MatrixUtils.RoomUpgradeCLI/Program.cs b/MatrixUtils.RoomUpgradeCLI/Program.cs new file mode 100644
index 0000000..e169830 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Program.cs
@@ -0,0 +1,41 @@ +using ArcaneLibs.Extensions; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot; +using MatrixUtils.RoomUpgradeCLI; +using MatrixUtils.RoomUpgradeCLI.Commands; + +foreach (var group in args.Split(";")) { + var argGroup = group.ToArray(); + var builder = Host.CreateApplicationBuilder(args); + builder.Services.AddRoryLibMatrixServices(); + builder.Services.AddMatrixBot(); + + if (argGroup.Length == 0) { + Console.WriteLine("Unknown command. Use 'new', 'modify', 'import-upgrade-state' or 'execute'."); + Console.WriteLine("Hint: you can chain commands with a semicolon (;) argument."); + return; + } + + Console.WriteLine($"Running command: {string.Join(", ", argGroup)}"); + + builder.Services.AddSingleton(new RuntimeContext() { + Args = argGroup + }); + + if (argGroup[0] == "new") builder.Services.AddHostedService<NewFileCommand>(); + else if (argGroup[0] == "new-from-room-dir") builder.Services.AddHostedService<NewFromRoomDirCommand>(); + else if (argGroup[0] == "modify") builder.Services.AddHostedService<ModifyCommand>(); + else if (argGroup[0] == "import-upgrade-state") builder.Services.AddHostedService<ImportUpgradeStateCommand>(); + else if (argGroup[0] == "execute") builder.Services.AddHostedService<ExecuteCommand>(); + // dev cmds + else if (argGroup[0] == "dev-delete-room") builder.Services.AddHostedService<DevDeleteRoomCommand>(); + else if (argGroup[0] == "dev-delete-all-rooms") builder.Services.AddHostedService<DevDeleteAllRoomsCommand>(); + else if (argGroup[0] == "dev-get-room-dir-state") builder.Services.AddHostedService<DevGetRoomDirStateCommand>(); + else { + Console.WriteLine("Unknown command. Use 'new', 'modify', 'import-upgrade-state' or 'execute'."); + return; + } + + var host = builder.Build(); + host.Run(); +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json b/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json new file mode 100644
index 0000000..76f122f --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Properties/launchSettings.json
@@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "MatrixUtils.RoomUpgradeCLI": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs b/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs new file mode 100644
index 0000000..50e6781 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/RuntimeContext.cs
@@ -0,0 +1,5 @@ +namespace MatrixUtils.RoomUpgradeCLI; + +public class RuntimeContext { + public string[] Args { get; set; } +} \ No newline at end of file diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json new file mode 100644
index 0000000..621d281 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json
@@ -0,0 +1,17 @@ +{ + // Don't touch this unless you know what you're doing: + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "LibMatrixBot": { + // Homeserver to connect to. + // Note: Homeserver resolution is applied here, but a direct base URL can be used. +// "Homeserver": "rory.gay", + + // Absolute path to the file containing the access token + "AccessTokenPath": "/home/Rory/matrix_access_token" + } +} diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.json b/MatrixUtils.RoomUpgradeCLI/appsettings.json new file mode 100644
index 0000000..4feb15c --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/appsettings.json
@@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Warning" + } + } +} diff --git a/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh b/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh new file mode 100755
index 0000000..f21ea3c --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/mass-upgrade.sh
@@ -0,0 +1,9 @@ +#! /usr/bin/env sh +dotnet build -c Release +cat lst | while read id +do + DOTNET_ENVIRONMENT=Local dotnet bin/Release/net9.0/MatrixUtils.RoomUpgradeCLI.dll new tmp/$id.json --upgrade $id --upgrade-unstable-values --force-upgrade --invite-powerlevel-users \; \ + import-upgrade-state tmp/$id.json \; \ + modify tmp/$id.json --version 12 & +done +wait \ No newline at end of file diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
index f2d47ea..880401a 100644 --- a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj +++ b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj
@@ -1,22 +1,22 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.6" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0-rc.2.25502.107"/> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\MatrixUtils.Web\MatrixUtils.Web.csproj" /> + <ProjectReference Include="..\MatrixUtils.Web\MatrixUtils.Web.csproj"/> </ItemGroup> <ItemGroup> - <Folder Include="Controllers" /> + <Folder Include="Controllers"/> </ItemGroup> diff --git a/MatrixUtils.Web.Server/Program.cs b/MatrixUtils.Web.Server/Program.cs
index cad3878..59d450a 100644 --- a/MatrixUtils.Web.Server/Program.cs +++ b/MatrixUtils.Web.Server/Program.cs
@@ -1,3 +1,6 @@ +using LibMatrix.Services; +using MatrixUtils.Web.Classes; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -5,6 +8,9 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); +builder.Services.AddRoryLibMatrixServices(); +builder.Services.AddScoped<RmuSessionStore>(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/MatrixUtils.Web/App.razor b/MatrixUtils.Web/App.razor
index 5e87bc3..7e8e1c3 100644 --- a/MatrixUtils.Web/App.razor +++ b/MatrixUtils.Web/App.razor
@@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.WebAssembly.Hosting <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> @@ -11,3 +11,9 @@ </LayoutView> </NotFound> </Router> + +@code { + + public static WebAssemblyHost Host { get; set; } = null!; + +} diff --git a/MatrixUtils.Web/Classes/Constants/RoomConstants.cs b/MatrixUtils.Web/Classes/Constants/RoomConstants.cs
index 5df0d01..dc81d04 100644 --- a/MatrixUtils.Web/Classes/Constants/RoomConstants.cs +++ b/MatrixUtils.Web/Classes/Constants/RoomConstants.cs
@@ -1,6 +1,7 @@ namespace MatrixUtils.Web.Classes.Constants; public class RoomConstants { - public static readonly string[] DangerousRoomVersions = { "1", "8" }; - public const string RecommendedRoomVersion = "10"; + public static readonly string[] DangerousRoomVersions = ["1", "8"]; + public static readonly string[] UnsupportedRoomVersions = ["1", "2", "3", "4", "5", "6"]; + public const string RecommendedRoomVersion = "11"; } diff --git a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
index 3803a17..ddf3eed 100644 --- a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs +++ b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
@@ -3,26 +3,20 @@ using LibMatrix.Interfaces.Services; namespace MatrixUtils.Web.Classes; -public class LocalStorageProviderService : IStorageProvider { - private readonly ILocalStorageService _localStorageService; - - public LocalStorageProviderService(ILocalStorageService localStorageService) { - _localStorageService = localStorageService; - } - +public class LocalStorageProviderService(ILocalStorageService localStorageService) : IStorageProvider { Task IStorageProvider.SaveAllChildrenAsync<T>(string key, T value) { throw new NotImplementedException(); } Task<T?> IStorageProvider.LoadAllChildrenAsync<T>(string key) where T : default => throw new NotImplementedException(); - async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await _localStorageService.SetItemAsync(key, value); + async Task IStorageProvider.SaveObjectAsync<T>(string key, T value) => await localStorageService.SetItemAsync(key, value); - async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await _localStorageService.GetItemAsync<T>(key); + async Task<T?> IStorageProvider.LoadObjectAsync<T>(string key) where T : default => await localStorageService.GetItemAsync<T>(key); - async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _localStorageService.ContainKeyAsync(key); + async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await localStorageService.ContainKeyAsync(key); - async Task<List<string>> IStorageProvider.GetAllKeysAsync() => (await _localStorageService.KeysAsync()).ToList(); + async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await localStorageService.KeysAsync()).ToList(); - async Task IStorageProvider.DeleteObjectAsync(string key) => await _localStorageService.RemoveItemAsync(key); + async Task IStorageProvider.DeleteObjectAsync(string key) => await localStorageService.RemoveItemAsync(key); } diff --git a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs b/MatrixUtils.Web/Classes/RMUStorageWrapper.cs deleted file mode 100644
index 45028ba..0000000 --- a/MatrixUtils.Web/Classes/RMUStorageWrapper.cs +++ /dev/null
@@ -1,138 +0,0 @@ -using LibMatrix; -using LibMatrix.Homeservers; -using LibMatrix.Services; -using Microsoft.AspNetCore.Components; - -namespace MatrixUtils.Web.Classes; - -public class RMUStorageWrapper(ILogger<RMUStorageWrapper> logger, TieredStorageService storageService, HomeserverProviderService homeserverProviderService, NavigationManager navigationManager) { - public async Task<List<UserAuth>?> GetAllTokens() { - logger.LogTrace("Getting all tokens."); - return await storageService.DataStorageProvider.LoadObjectAsync<List<UserAuth>>("rmu.tokens") ?? - new List<UserAuth>(); - } - - public async Task<UserAuth?> GetCurrentToken() { - logger.LogTrace("Getting current token."); - var currentToken = await storageService.DataStorageProvider.LoadObjectAsync<UserAuth>("rmu.token"); - var allTokens = await GetAllTokens(); - if (allTokens is null or { Count: 0 }) { - await SetCurrentToken(null); - return null; - } - - if (currentToken is null) { - await SetCurrentToken(currentToken = allTokens[0]); - } - - if (!allTokens.Any(x => x.AccessToken == currentToken.AccessToken)) { - await SetCurrentToken(currentToken = allTokens[0]); - } - - return currentToken; - } - - public async Task AddToken(UserAuth UserAuth) { - logger.LogTrace("Adding token."); - var tokens = await GetAllTokens() ?? new List<UserAuth>(); - - tokens.Add(UserAuth); - await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens); - } - - private async Task<AuthenticatedHomeserverGeneric?> GetCurrentSession() { - logger.LogTrace("Getting current session."); - var token = await GetCurrentToken(); - if (token == null) { - return null; - } - - return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); - } - - public async Task<AuthenticatedHomeserverGeneric?> GetSession(UserAuth userAuth) { - logger.LogTrace("Getting session."); - return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken, userAuth.Proxy); - } - - public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrNavigate() { - logger.LogTrace("Getting current session or navigating."); - AuthenticatedHomeserverGeneric? session = null; - - try { - //catch if the token is invalid - session = await GetCurrentSession(); - } - catch (MatrixException e) { - if (e.ErrorCode == "M_UNKNOWN_TOKEN") { - var token = await GetCurrentToken(); - logger.LogWarning("Encountered invalid token for {user} on {homeserver}", token.UserId, token.Homeserver); - navigationManager.NavigateTo("/InvalidSession?ctx=" + token.AccessToken); - return null; - } - - throw; - } - - if (session is null) { - logger.LogInformation("No session found. Navigating to login."); - navigationManager.NavigateTo("/Login"); - } - - return session; - } - - public class Settings { - public DeveloperSettings DeveloperSettings { get; set; } = new(); - } - - public class DeveloperSettings { - public bool EnableLogViewers { get; set; } - public bool EnableConsoleLogging { get; set; } = true; - public bool EnablePortableDevtools { get; set; } - } - - public async Task RemoveToken(UserAuth auth) { - logger.LogTrace("Removing token."); - var tokens = await GetAllTokens(); - if (tokens == null) { - return; - } - - tokens.RemoveAll(x => x.AccessToken == auth.AccessToken); - await storageService.DataStorageProvider.SaveObjectAsync("rmu.tokens", tokens); - } - - public async Task SetCurrentToken(UserAuth? auth) { - logger.LogTrace("Setting current token."); - await storageService.DataStorageProvider.SaveObjectAsync("rmu.token", auth); - } - - public async Task MigrateFromMRU() { - logger.LogInformation("Migrating from MRU token namespace!"); - var dsp = storageService.DataStorageProvider!; - if(await dsp.ObjectExistsAsync("token")) { - var oldToken = await dsp.LoadObjectAsync<UserAuth>("token"); - if (oldToken != null) { - await dsp.SaveObjectAsync("rmu.token", oldToken); - await dsp.DeleteObjectAsync("tokens"); - } - } - - if(await dsp.ObjectExistsAsync("tokens")) { - var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens"); - if (oldTokens != null) { - await dsp.SaveObjectAsync("rmu.tokens", oldTokens); - await dsp.DeleteObjectAsync("tokens"); - } - } - - if(await dsp.ObjectExistsAsync("mru.tokens")) { - var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens"); - if (oldTokens != null) { - await dsp.SaveObjectAsync("rmu.tokens", oldTokens); - await dsp.DeleteObjectAsync("mru.tokens"); - } - } - } -} diff --git a/MatrixUtils.Web/Classes/RmuSessionStore.cs b/MatrixUtils.Web/Classes/RmuSessionStore.cs new file mode 100644
index 0000000..1611b83 --- /dev/null +++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -0,0 +1,336 @@ +using LibMatrix; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using Microsoft.AspNetCore.Components; + +namespace MatrixUtils.Web.Classes; + +public class RmuSessionStore( + ILogger<RmuSessionStore> logger, + TieredStorageService storageService, + HomeserverProviderService homeserverProviderService, + NavigationManager navigationManager) { + private SessionInfo? CurrentSession { get; set; } + private Dictionary<string, SessionInfo> SessionCache { get; set; } = []; + + private bool _isInitialized; + private static readonly SemaphoreSlim InitSemaphore = new(1, 1); + +#region Sessions + + public async Task<Dictionary<string, SessionInfo>> GetAllSessions() { + await LoadStorage(); + logger.LogTrace("Getting all tokens."); + return SessionCache; + } + + public async Task<SessionInfo?> GetSession(string sessionId) { + await LoadStorage(); + if (string.IsNullOrEmpty(sessionId)) { + logger.LogWarning("No session ID provided."); + return null; + } + + if (SessionCache.TryGetValue(sessionId, out var cachedSession)) + return cachedSession; + + logger.LogWarning("Session {sessionId} not found in all tokens.", sessionId); + return null; + } + + public async Task<SessionInfo?> GetCurrentSession(bool log = true) { + await LoadStorage(); + if (log) logger.LogTrace("Getting current token."); + if (CurrentSession is not null) return CurrentSession; + + var currentSessionId = await storageService.DataStorageProvider!.LoadObjectAsync<string>("rmu.session"); + if (currentSessionId == null) { + if (log) logger.LogWarning("No current session ID found in storage."); + return null; + } + + return await GetSession(currentSessionId); + } + + public async Task<string> AddSession(UserAuth auth) { + await LoadStorage(); + logger.LogTrace("Adding token."); + + var sessionId = auth.GetHashCode().ToString(); + SessionCache[sessionId] = new() { + Auth = auth, + SessionId = sessionId + }; + + await SaveStorage(); + if (CurrentSession == null) await SetCurrentSession(sessionId); + + return sessionId; + } + + public async Task RemoveSession(string sessionId) { + await LoadStorage(); + if (SessionCache.Count == 0) { + logger.LogWarning("No sessions found."); + return; + } + + logger.LogTrace("Removing session {sessionId}.", sessionId); + + if ((await GetCurrentSession())?.SessionId == sessionId) + await SetCurrentSession(SessionCache.FirstOrDefault(x => x.Key != sessionId).Key); + + if (SessionCache.Remove(sessionId)) { + logger.LogInformation("RemoveSession: Removed session {sessionId}.", sessionId); + logger.LogInformation("RemoveSession: Remaining sessions: {sessionIds}.", string.Join(", ", SessionCache.Keys)); + await SaveStorage(log: true); + } + else + logger.LogWarning("RemoveSession: Session {sessionId} not found.", sessionId); + } + + public async Task SetCurrentSession(string? sessionId) { + await LoadStorage(); + logger.LogTrace("Setting current session to {sessionId}.", sessionId); + CurrentSession = await GetSession(sessionId); + await SaveStorage(); + } + +#endregion + +#region Homeservers + + public async Task<AuthenticatedHomeserverGeneric?> GetHomeserver(string session, bool log = true) { + await LoadStorage(); + if (log) logger.LogTrace("Getting session."); + if (!SessionCache.TryGetValue(session, out var cachedSession)) return null; + if (cachedSession.Homeserver is not null) return cachedSession.Homeserver; + + try { + cachedSession.Homeserver = + await homeserverProviderService.GetAuthenticatedWithToken(cachedSession.Auth.Homeserver, cachedSession.Auth.AccessToken, cachedSession.Auth.Proxy); + } + catch (Exception e) { + logger.LogError("Failed to get info for {0} via {1}: {2}", cachedSession.Auth.UserId, cachedSession.Auth.Homeserver, e); + logger.LogError("Continuing with server-less session"); + cachedSession.Homeserver = await homeserverProviderService.GetAuthenticatedWithToken(cachedSession.Auth.Homeserver, cachedSession.Auth.AccessToken, + cachedSession.Auth.Proxy, useGeneric: true, enableServer: false); + } + + return cachedSession.Homeserver; + } + + public async Task<AuthenticatedHomeserverGeneric?> GetCurrentHomeserver(bool log = true, bool navigateOnFailure = false) { + await LoadStorage(); + if (log) logger.LogTrace("Getting current session."); + if (CurrentSession?.Homeserver is not null) return CurrentSession.Homeserver; + + var currentSession = CurrentSession ??= await GetCurrentSession(log: false); + if (currentSession == null) { + if (navigateOnFailure) { + logger.LogInformation("No session found. Navigating to login."); + navigationManager.NavigateTo("/Login"); + } + + return null; + } + + try { + return currentSession.Homeserver ??= await GetHomeserver(currentSession.SessionId); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_UNKNOWN_TOKEN" && navigateOnFailure) { + logger.LogWarning("Encountered invalid token for {user} on {homeserver}", currentSession.Auth.UserId, currentSession.Auth.Homeserver); + if (navigateOnFailure) { + navigationManager.NavigateTo("/InvalidSession?ctx=" + currentSession.SessionId); + } + } + + throw; + } + } + + public async IAsyncEnumerable<AuthenticatedHomeserverGeneric> TryGetAllHomeservers(bool log = true, bool ignoreFailures = true) { + await LoadStorage(); + if (log) logger.LogTrace("Getting all homeservers."); + var tasks = SessionCache.Values.Select(async session => { + if (ignoreFailures && session.Auth.LastFailureReason != null && session.Auth.LastFailureReason != UserAuth.FailureReason.None) { + if (log) logger.LogTrace("Skipping session {sessionId} due to previous failure: {reason}", session.SessionId, session.Auth.LastFailureReason); + return null; + } + + try { + var hs = await GetHomeserver(session.SessionId, log: false); + if (session.Auth.LastFailureReason != null) { + SessionCache[session.SessionId].Auth.LastFailureReason = null; + await SaveStorage(); + } + + return hs; + } + catch (Exception e) { + logger.LogError("TryGetAllHomeservers: Failed to get homeserver for {userId} via {homeserver}: {ex}", session.Auth.UserId, session.Auth.Homeserver, e); + var reason = SessionCache[session.SessionId].Auth.LastFailureReason = e switch { + MatrixException { ErrorCode: MatrixException.ErrorCodes.M_UNKNOWN_TOKEN } => UserAuth.FailureReason.InvalidToken, + HttpRequestException => UserAuth.FailureReason.NetworkError, + _ => UserAuth.FailureReason.UnknownError + }; + await SaveStorage(log: true); + + // await LoadStorage(true); + if (SessionCache[session.SessionId].Auth.LastFailureReason != reason) { + await Console.Error.WriteLineAsync( + $"Warning: Session {session.SessionId} failure reason changed during reload from {reason} to {SessionCache[session.SessionId].Auth.LastFailureReason}"); + } + + throw; + } + }).ToList(); + + while (tasks.Count != 0) { + var finished = await Task.WhenAny(tasks); + tasks.Remove(finished); + if (finished.IsFaulted) continue; + + var result = await finished; + if (result != null) yield return result; + } + } + +#endregion + +#region Storage + + private async Task LoadStorage(bool hasMigrated = false) { + if (!await storageService.DataStorageProvider!.ObjectExistsAsync("rmu.sessions") || !await storageService.DataStorageProvider.ObjectExistsAsync("rmu.session")) { + if (!hasMigrated) { + await RunMigrations(); + await LoadStorage(true); + } + else + logger.LogWarning("No sessions found in storage."); + + return; + } + + SessionCache = (await storageService.DataStorageProvider.LoadObjectAsync<Dictionary<string, UserAuth>>("rmu.sessions") ?? throw new Exception("Failed to load sessions")) + .ToDictionary(x => x.Key, x => new SessionInfo { + SessionId = x.Key, + Auth = x.Value + }); + + var currentSessionId = await storageService.DataStorageProvider.LoadObjectAsync<string>("rmu.session"); + if (currentSessionId == null) { + logger.LogWarning("No current session found in storage."); + return; + } + + if (!SessionCache.TryGetValue(currentSessionId, out var currentSession)) { + logger.LogWarning("Current session {currentSessionId} not found in storage.", currentSessionId); + return; + } + + CurrentSession = currentSession; + } + + private async Task SaveStorage(bool log = false) { + if (log) logger.LogWarning("Saving {count} sessions to storage.", SessionCache.Count); + await storageService.DataStorageProvider!.SaveObjectAsync("rmu.sessions", + SessionCache.ToDictionary( + x => x.Key, + x => x.Value.Auth + ) + ); + await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId); + if (log) logger.LogWarning("{count} sessions saved to storage.", SessionCache.Count); + } + +#endregion + +#region Migrations + + public async Task RunMigrations() { + await MigrateFromMru(); + await MigrateAccountsToKeyedStorage(); + } + + private async Task MigrateFromMru() { + var dsp = storageService.DataStorageProvider!; + if (await dsp.ObjectExistsAsync("token") || await dsp.ObjectExistsAsync("tokens")) { + logger.LogInformation("Migrating from unnamespaced localstorage!"); + if (await dsp.ObjectExistsAsync("token")) { + var oldToken = await dsp.LoadObjectAsync<UserAuth>("token"); + if (oldToken != null) { + await dsp.SaveObjectAsync("mru.token", oldToken); + await dsp.DeleteObjectAsync("token"); + } + } + + if (await dsp.ObjectExistsAsync("tokens")) { + var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("tokens"); + if (oldTokens != null) { + await dsp.SaveObjectAsync("mru.tokens", oldTokens); + await dsp.DeleteObjectAsync("tokens"); + } + } + } + + if (await dsp.ObjectExistsAsync("mru.token") || await dsp.ObjectExistsAsync("mru.tokens")) { + logger.LogInformation("Migrating from MRU token namespace!"); + if (await dsp.ObjectExistsAsync("mru.token")) { + var oldToken = await dsp.LoadObjectAsync<UserAuth>("mru.token"); + if (oldToken != null) { + await dsp.SaveObjectAsync("rmu.token", oldToken); + await dsp.DeleteObjectAsync("mru.token"); + } + } + + if (await dsp.ObjectExistsAsync("mru.tokens")) { + var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens"); + if (oldTokens != null) { + await dsp.SaveObjectAsync("rmu.tokens", oldTokens); + await dsp.DeleteObjectAsync("mru.tokens"); + } + } + } + } + + private async Task MigrateAccountsToKeyedStorage() { + var dsp = storageService.DataStorageProvider!; + if (!await dsp.ObjectExistsAsync("rmu.tokens")) return; + logger.LogInformation("Migrating accounts to keyed storage!"); + var tokens = await dsp.LoadObjectAsync<UserAuth[]>("rmu.tokens") ?? throw new Exception("Failed to load tokens"); + Dictionary<string, UserAuth> keyedTokens = tokens.ToDictionary(x => x.GetHashCode().ToString(), x => x); + + if (await dsp.ObjectExistsAsync("rmu.token")) { + var token = await dsp.LoadObjectAsync<UserAuth>("rmu.token") ?? throw new Exception("Failed to load token"); + var sessionId = keyedTokens.FirstOrDefault(x => x.Value.Equals(token)).Key; + + if (sessionId is null) keyedTokens.Add(sessionId = token.GetHashCode().ToString(), token); + await dsp.SaveObjectAsync("rmu.session", sessionId); + + await dsp.DeleteObjectAsync("rmu.token"); + } + + await dsp.SaveObjectAsync("rmu.sessions", keyedTokens); + await dsp.DeleteObjectAsync("rmu.tokens"); + } + +#endregion + + public class Settings { + public DeveloperSettings DeveloperSettings { get; set; } = new(); + } + + public class DeveloperSettings { + public bool EnableLogViewers { get; set; } + public bool EnableConsoleLogging { get; set; } = true; + public bool EnablePortableDevtools { get; set; } + } + + public class SessionInfo { + public required string SessionId { get; set; } + public required UserAuth Auth { get; set; } + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index a627a9c..215ad14 100644 --- a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs +++ b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
@@ -1,6 +1,5 @@ using System.Text.Json.Nodes; using LibMatrix; -using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Responses; @@ -34,7 +33,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate { }, new() { Type = "m.room.server_acl", - TypedContent = new RoomServerACLEventContent() { + TypedContent = new RoomServerAclEventContent() { Allow = new List<string>() { "*" }, Deny = new List<string>(), AllowIpLiterals = false @@ -56,7 +55,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate { Redact = 50, Kick = 50, Ban = 50, - NotificationsPl = new RoomPowerLevelEventContent.NotificationsPL { + NotificationsPl = new RoomPowerLevelEventContent.NotificationsPowerLevels { Room = 50 }, Events = new() { @@ -83,10 +82,8 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate { //TODO: re-implement this } }, - CreationContent = new JsonObject { - { - "type", null - } + CreationContent = new() { + { "type", null } } }; -} +} \ No newline at end of file diff --git a/MatrixUtils.Web/Classes/SessionStorageProviderService.cs b/MatrixUtils.Web/Classes/SessionStorageProviderService.cs
index ae0bb79..da169de 100644 --- a/MatrixUtils.Web/Classes/SessionStorageProviderService.cs +++ b/MatrixUtils.Web/Classes/SessionStorageProviderService.cs
@@ -22,7 +22,7 @@ public class SessionStorageProviderService : IStorageProvider { async Task<bool> IStorageProvider.ObjectExistsAsync(string key) => await _sessionStorageService.ContainKeyAsync(key); - async Task<List<string>> IStorageProvider.GetAllKeysAsync() => (await _sessionStorageService.KeysAsync()).ToList(); + async Task<IEnumerable<string>> IStorageProvider.GetAllKeysAsync() => (await _sessionStorageService.KeysAsync()).ToList(); async Task IStorageProvider.DeleteObjectAsync(string key) => await _sessionStorageService.RemoveItemAsync(key); } diff --git a/MatrixUtils.Web/Classes/UserAuth.cs b/MatrixUtils.Web/Classes/UserAuth.cs
index 66476ae..16bb758 100644 --- a/MatrixUtils.Web/Classes/UserAuth.cs +++ b/MatrixUtils.Web/Classes/UserAuth.cs
@@ -1,9 +1,11 @@ +using System.Text.Json.Serialization; using LibMatrix.Responses; namespace MatrixUtils.Web.Classes; public class UserAuth : LoginResponse { public UserAuth() { } + public UserAuth(LoginResponse login) { Homeserver = login.Homeserver; UserId = login.UserId; @@ -12,4 +14,14 @@ public class UserAuth : LoginResponse { } public string? Proxy { get; set; } -} + + public FailureReason? LastFailureReason { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum FailureReason { + None, + InvalidToken, + NetworkError, + UnknownError + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index 8760e7a..f7ebb62 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LinkIncremental>true</LinkIncremental> @@ -12,62 +12,68 @@ <BlazorEnableCompression>false</BlazorEnableCompression> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> <BlazorCacheBootResources>false</BlazorCacheBootResources> -<!-- <RunAOTCompilation>true</RunAOTCompilation>--> + <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> + <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> + <WasmEnableHotReload>false</WasmEnableHotReload> </PropertyGroup> - <ItemGroup> - <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> - <PackageReference Include="Blazored.SessionStorage" Version="2.4.0" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" /> - <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" /> - </ItemGroup> + <!-- Explicitly disable all the unused runtime things trimming would have removed anyways --> + <!-- https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options --> + <!-- <PropertyGroup>--> + <!-- <AutoreleasePoolSupport>false</AutoreleasePoolSupport> &lt;!&ndash; Browser != MacOS &ndash;&gt;--> + <!-- <MetadataUpdaterSupport>false</MetadataUpdaterSupport> &lt;!&ndash; Unreliable &ndash;&gt;--> + <!-- <DebuggerSupport>false</DebuggerSupport> &lt;!&ndash; Unreliable &ndash;&gt;--> + <!-- <InvariantGlobalization>true</InvariantGlobalization> &lt;!&ndash; invariant globalization is fine &ndash;&gt;--> + <!-- &lt;!&ndash; unused features &ndash;&gt;--> + <!-- <EventSourceSupport>false</EventSourceSupport>--> + <!-- <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>--> + <!-- <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>--> + <!-- <EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>--> + <!-- <MetricsSupport>false</MetricsSupport>--> + <!-- <UseNativeHttpHandler>false</UseNativeHttpHandler>--> + <!-- <XmlResolverIsNetworkingEnabledByDefault>false</XmlResolverIsNetworkingEnabledByDefault>--> + <!-- <BuiltInComInteropSupport>false</BuiltInComInteropSupport>--> + <!-- <CustomResourceTypesSupport>false</CustomResourceTypesSupport>--> + <!-- <EnableCppCLIHostActivation>false</EnableCppCLIHostActivation>--> + <!-- <StartupHookSupport>false</StartupHookSupport>--> + <!-- </PropertyGroup>--> <ItemGroup> - <ProjectReference Condition="Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" /> - <PackageReference Condition="!Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs.Blazor.Components" Version="*-preview*" /> - <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> - <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj" /> - <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj" /> + <PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/> + <PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107"/> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0-rc.2.25502.107" PrivateAssets="all"/> + <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0-rc.2.25502.107"/> + <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="10.0.0-rc.2.25502.107"/> + <PackageReference Include="SpawnDev.BlazorJS" Version="2.38.0"/> + <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.21.0"/> </ItemGroup> <ItemGroup> - <Content Update="appsettings.Development.json"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - </Content> - <Content Update="appsettings.json"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - </Content> - <Content Update="wwwroot\appsettings.json"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - </Content> + <ProjectReference Include="..\MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj"/> + <ProjectReference Include="..\MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj"/> </ItemGroup> <ItemGroup> - <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" /> + <!-- <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20241210-161342" Condition="'$(Configuration)' == 'Release'"/>--> + <!-- <ProjectReference Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" Condition="'$(Configuration)' == 'Debug'"/>--> + <ProjectReference Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj"/> </ItemGroup> <ItemGroup> - <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\ClientRoomList.razor" /> - <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\ClientStatusList.razor" /> - <_ContentIncludedByDefault Remove="Pages\Client\ClientComponents\MatrixClient.razor" /> - <_ContentIncludedByDefault Remove="Pages\Client\Index.razor" /> + <Content Update="appsettings.Development.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + <Content Update="appsettings.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + <Content Update="wwwroot\appsettings.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> </ItemGroup> <ItemGroup> - <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\ClientRoomList.razor" /> - <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\ClientStatusList.razor" /> - <AdditionalFiles Include="Pages\Labs\Client\ClientComponents\MatrixClient.razor" /> - <AdditionalFiles Include="Pages\Labs\Client\Index.razor" /> - <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage0.razor" /> - <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage1.razor" /> - <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage2.razor" /> - <AdditionalFiles Include="Pages\Labs\DMSpace\DMSpaceStages\DMSpaceStage3.razor" /> - <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\MainTabComponents\MainTabSpaceItem.razor" /> - <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2ByRoomTypeTab.razor" /> - <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2DMsTab.razor" /> - <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2MainTab.razor" /> - <AdditionalFiles Include="Pages\Labs\Rooms2\Index2Components\RoomsIndex2SyncContainer.razor" /> + <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/> </ItemGroup> - + </Project> diff --git a/MatrixUtils.Web/Pages/About.razor b/MatrixUtils.Web/Pages/About.razor
index 18d7c3f..9f83991 100644 --- a/MatrixUtils.Web/Pages/About.razor +++ b/MatrixUtils.Web/Pages/About.razor
@@ -7,6 +7,6 @@ <p>Rory&::MatrixUtils is a "small" collection of tools to do not-so-everyday things.</p> <p>These range from joining rooms on dead homeservers, to managing your accounts and rooms, and creating rooms based on templates.</p> -<br/><br/> -<p>You can find the source code on <a href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/">cgit.rory.gay</a>.<br/></p> +<br/> +<p>You can find the source code on <a href="https://cgit.rory.gay/matrix/tools/MatrixUtils.git/about/">cgit.rory.gay</a>.<br/></p> <p>You can also join the <a href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support">Matrix room</a> for this project.</p> diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
index 7b646d1..281cf07 100644 --- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor +++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -2,7 +2,6 @@ @using ArcaneLibs.Extensions @using System.Text @using System.Text.Json -@using Microsoft.JSInterop @inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime @inject TieredStorageService TieredStorage @@ -20,6 +19,10 @@ <span>Export local storage: </span> <button @onclick="@ExportLocalStorage">Export</button> </p> +<details> + <summary>Manage local sessions</summary> + +</details> @if (userSettings is not null) { <InputCheckbox @bind-Value="@userSettings.DeveloperSettings.EnableLogViewers" @oninput="@LogStuff"></InputCheckbox> @@ -36,11 +39,15 @@ @code { - private RMUStorageWrapper.Settings? userSettings { get; set; } + private RmuSessionStore.Settings? userSettings { get; set; } + protected override async Task OnInitializedAsync() { - // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RMUStorageWrapper.Settings>("rmu.settings"); - - await base.OnInitializedAsync(); + await (Task)typeof(RmuSessionStore).GetMethod("LoadStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(sessionStore, [true])!; + await foreach (var _ in sessionStore.TryGetAllHomeservers()) { } + + await (Task)typeof(RmuSessionStore).GetMethod("SaveStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(sessionStore, [true])!; } private async Task LogStuff() { @@ -55,8 +62,9 @@ foreach (var key in keys) { data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key)); } + var dataUri = "data:application/json;base64,"; - dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); + dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank"); } @@ -66,6 +74,7 @@ foreach (var (key, value) in data) { await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value); } + NavigationManager.NavigateTo(NavigationManager.Uri, true, true); } diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
index bf5a396..f6392a4 100644 --- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor +++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,9 +1,13 @@ @page "/Dev/Utilities" @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.Ephemeral +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Helpers @using MatrixUtils.Abstractions <h3>Debug Tools</h3> <hr/> +<LinkButton href="/Dev/WellKnownRes">Well known res tests</LinkButton> @if (Rooms.Count == 0) { <p>You are not in any rooms!</p> @* <p>Loading progress: @checkedRoomCount/@totalRoomCount</p> *@ @@ -13,7 +17,7 @@ else { <summary>Room List</summary> @foreach (var roomId in Rooms) { <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@roomId.Replace('.', '~')"> - <RoomListItem RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> + <RoomListItem Homeserver="hs" RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> </a> } </details> @@ -38,7 +42,7 @@ else { protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs == null) return; Rooms = (await hs.GetJoinedRooms()).Select(x => x.RoomId).ToList(); Console.WriteLine("Fetched joined rooms!"); @@ -60,6 +64,7 @@ else { StateHasChanged(); return; } + if (res.Content.Headers.ContentType.MediaType == "application/json") GetRequestResult = $"Error: {res.StatusCode}\n" + (await res.Content.ReadFromJsonAsync<object>()).ToJson(); else @@ -68,7 +73,32 @@ else { catch (Exception e) { GetRequestResult = $"Error: {e}"; } + StateHasChanged(); } + private async Task TestRoomBuilder() { + var rb = new RoomBuilder() { + HistoryVisibility = new RoomHistoryVisibilityEventContent() { HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared }, + ImportantState = [ + new() { + RawContent = new() { + ["type"] = "m.room.name", + ["name"] = "Test Room" + } + }, + new() { + Type = "test", + TypedContent = new PresenceEventContent() { + Presence = "online", + LastActiveAgo = 0, + } + }, + + ] + }; + + await rb.Create(hs); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Dev/ModalTest.razor b/MatrixUtils.Web/Pages/Dev/ModalTest.razor new file mode 100644
index 0000000..665f548 --- /dev/null +++ b/MatrixUtils.Web/Pages/Dev/ModalTest.razor
@@ -0,0 +1,12 @@ +@page "/Dev/ModalTest" + +<PageTitle>Modal test</PageTitle> + +<h3>Rory&::MatrixUtils - Modal test</h3> +<hr/> +@for (int i = 0; i < 10; i++) +{ + <ModalWindow X="i*75" Y="i*75"> + <h1>Hello, world!</h1> + </ModalWindow> +} diff --git a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor new file mode 100644
index 0000000..c636c56 --- /dev/null +++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -0,0 +1,123 @@ +@page "/Dev/WellKnownRes" +@using ArcaneLibs.Extensions +@using LibMatrix.Services.WellKnownResolver +@using LibMatrix.Services.WellKnownResolver.WellKnownResolvers +@inject HomeserverResolverService legacyResolver +@inject WellKnownResolverService rewriteResolver +@inject ClientWellKnownResolver rewriteClientResolver +<h3>Known Homeserver List</h3> +<hr/> + +<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClickAsync="@Execute">Execute</LinkButton></span> + +<span>Stats:</span><br/> +<span>Server count: @entries.Count</span><br/> +<span>Client server resolution rate (N/O/T): @entries.Count(x => x.HasClientWellKnown)/@entries.Count(x => !string.IsNullOrWhiteSpace(x.LegacyResolutionResult?.Client))/@entries.Count</span> +<br/> +<span>Server server resolution rate (N/T): @entries.Count(x => x.HasServerWellKnown)/@entries.Count</span><br/> +<span>Support resolution rate (N/T): @entries.Count(x => x.HasSupportWellKnown)/@entries.Count</span><br/> + +<table class="table-bordered"> + <thead> + <td>Homeserver</td> + <td>Client API</td> + <td>Server API</td> + <td>Has support record</td> + </thead> + @foreach (var entry in entries) { + <tr> + <td>@entry.Homeserver</td> + <td style="background-color: @GetClientColor(entry)"> + <span>L: @entry.LegacyResolutionResult?.Client</span><br/> + <span>R: @entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver.BaseUrl</span> + </td> + <td style="background-color: @GetServerColor(entry)"> + <span>L: @entry.LegacyResolutionResult?.Server</span><br/> + <span>R: @entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver</span> + </td> + <td>@(entry.HasSupportWellKnown ? "Y" : "X")</td> + </tr> + <tr> + <td colspan="6"> + <details> + <pre>@(entry.WellKnownResolutionResult?.ToJson() ?? "null")</pre> + </details> + </td> + </tr> + } +</table> + +@code { + private List<TableEntry> entries = new(); + + [SupplyParameterFromQuery] + public string? RoomId { get; set; } + + AuthenticatedHomeserverGeneric? hs { get; set; } + + protected override async Task OnInitializedAsync() { + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + + if (RoomId is not null) { + await Execute(); + } + } + + private class TableEntry { + public required string Homeserver { get; set; } + public HomeserverResolverService.WellKnownUris? LegacyResolutionResult { get; set; } + public WellKnownResolverService.WellKnownRecords? WellKnownResolutionResult { get; set; } + + public bool HasClientWellKnown => WellKnownResolutionResult?.ClientWellKnown is { Content.Homeserver.BaseUrl: { Length: > 0 } }; + public bool HasServerWellKnown => WellKnownResolutionResult?.ServerWellKnown is { Content.Homeserver.Length: > 0 }; + public bool HasSupportWellKnown => WellKnownResolutionResult?.SupportWellKnown?.Content is not null and not { SupportPage: null, Contacts: null or { Count: 0 } }; + } + + private async Task Execute() { + var members = await hs!.GetRoom(RoomId!).GetMembersListAsync(); + var homeservers = members.Select(x => x.StateKey!.Split(':', 2)[1]).Distinct().ToList(); + var entries = new List<TableEntry>(); + foreach (var homeserver in homeservers) { + var e = new TableEntry() { Homeserver = homeserver }; + _ = TryResolveLegacy(e); + _ = TryFullResolveRewrite(e); + entries.Add(e); + } + + this.entries = entries; + StateHasChanged(); + } + + private async Task TryResolveLegacy(TableEntry entry) { + try { + var cTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false); + var sTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableClient: false); + entry.LegacyResolutionResult = (await cTask); + entry.LegacyResolutionResult.Server = (await sTask).Server; + StateHasChanged(); + } + catch { } + } + + private async Task TryFullResolveRewrite(TableEntry entry) { + try { + entry.WellKnownResolutionResult = await rewriteResolver.TryResolveWellKnownRecords(entry.Homeserver); + StateHasChanged(); + } + catch { } + } + + private string GetClientColor(TableEntry entry) { + if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl && entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl == null) return "#333333"; + if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl?.TrimEnd('/')) return "#008800"; + return "#ff0000"; + } + + private string GetServerColor(TableEntry entry) { + if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver && entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver == null) return "#333333"; + if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver.TrimEnd('/')) return "#008800"; + return "#ff0000"; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index 9c61431..21b0972 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -1,5 +1,6 @@ @page "/HSAdmin" @using ArcaneLibs.Extensions +@using LibMatrix.Responses.Federation <h3>Homeserver Admininistration</h3> <hr/> @@ -10,7 +11,15 @@ else { @if (Homeserver is AuthenticatedHomeserverSynapse) { <h4>Synapse tools</h4> <hr/> - <a href="/HSAdmin/RoomQuery">Query rooms</a> + <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/> + <a href="/HSAdmin/Synapse/UserQuery">Query users</a><br/> + <a href="/HSAdmin/Synapse/BlockMedia">Block media</a><br/> + <a href="/HSAdmin/Synapse/BackgroundJobs">View running background jobs</a><br/> + } + else if (Homeserver is AuthenticatedHomeserverHSE) { + <h4>Rory&amp;::LibMatrix.HomeserverEmulator tools</h4> + <hr/> + <a href="/HSAdmin/HSE/ManageExternalProfiles">Manage external profiles</a> } else { <p>Homeserver type @Homeserver.GetType().Name does not have any administration tools in RMU.</p> @@ -24,7 +33,7 @@ else { public ServerVersionResponse? ServerVersionResponse { get; set; } protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; ServerVersionResponse = await (Homeserver.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)); await base.OnInitializedAsync(); diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor new file mode 100644
index 0000000..ec2ec54 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -0,0 +1,43 @@ +@page "/HSAdmin/HSE/ManageExternalProfiles" +@using ArcaneLibs.Extensions +@using LibMatrix.Responses +<h3>Manage external profiles</h3> + +<LinkButton OnClickAsync="AddAllLocalProfiles">Add local sessions</LinkButton> + +@foreach(var p in ExternalProfiles) +{ + <h4>@p.Key</h4> + <pre>@p.Value.ToJson(indent: true)</pre> +} + +@code { + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private Dictionary<string, LoginResponse> ExternalProfiles = new(); + + protected override async Task OnInitializedAsync() + { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; + await LoadProfiles(); + await base.OnInitializedAsync(); + } + + private async Task LoadProfiles() { + if(Homeserver is AuthenticatedHomeserverHSE hse) + { + ExternalProfiles = await hse.GetExternalProfilesAsync(); + } + StateHasChanged(); + } + + private async Task AddAllLocalProfiles() { + if(Homeserver is AuthenticatedHomeserverHSE hse) { + var sessions = await sessionStore.GetAllSessions(); + foreach(var session in sessions) { + await hse.SetExternalProfile(session.Value.Auth.UserId, session.Value.Auth); + } + await LoadProfiles(); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor deleted file mode 100644
index 11df261..0000000 --- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor +++ /dev/null
@@ -1,201 +0,0 @@ -@page "/HSAdmin/RoomQuery" -@using LibMatrix.Responses.Admin -@using LibMatrix.Filters -@using ArcaneLibs.Extensions - -<h3>Homeserver Administration - Room Query</h3> - -<label>Search name: </label> -<InputText @bind-Value="SearchTerm"/><br/> -<label>Order by: </label> -<select @bind="OrderBy"> - @foreach (var item in validOrderBy) { - <option value="@item.Key">@item.Value</option> - } -</select><br/> -<label>Ascending: </label> -<InputCheckbox @bind-Value="Ascending"/><br/> -<details> - <summary> - <span>Local filtering (slow)</span> - - </summary> - <div style="margin-left: 8px; margin-bottom: 8px;"> - <u style="display: block;">String contains</u> - <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span> - <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span> - <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span> - <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span> - <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span> - <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span> - <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span> - <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span> - <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span> - - <u style="display: block;">Optional checks</u> - <span class="tile tile150"> - <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated: - @if (Filter.CheckFederation) { - <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox> - } - </span> - <span class="tile tile150"> - <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public: - @if (Filter.CheckPublic) { - <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox> - } - </span> - - <u style="display: block;">Ranges</u> - <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber> - </span> - <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber> - </span> - <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber> - </span> - </div> -</details> -<button class="btn btn-primary" @onclick="Search">Search</button> -<br/> - -@if (Results.Count > 0) { - <p>Found @Results.Count rooms</p> - <details> - <summary>TSV data (copy/paste)</summary> - <pre style="font-size: 0.6em;"> - <table> - @foreach (var res in Results) { - <tr> - <td style="padding: 8px;">@res.RoomId@("\t")</td> - <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td> - <td style="padding: 8px;">@res.Creator@("\t")</td> - <td style="padding: 8px;">@res.Name</td> - </tr> - } - </table> - </pre> - </details> -} - -@foreach (var res in Results) { - <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;"> - @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ - <p> - @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) { - <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span> - <br/> - } - else { - <span>@res.RoomId (@res.Name)</span> - <br/> - } - @if (!string.IsNullOrWhiteSpace(res.Creator)) { - @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@ - <span>Created by @res.Creator</span> - <br/> - } - </p> - <span>@res.StateEvents state events</span><br/> - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> - <details> - <summary>Full result data</summary> - <pre>@res.ToJson(ignoreNull: true)</pre> - </details> - </div> -} - -<style> - .int-input { - width: 128px; - } - .tile { - display: inline-block; - padding: 4px; - border: 1px solid #ffffff22; - } - .tile280 { - min-width: 280px; - } - .tile150 { - min-width: 150px; - } - .range-sep { - display: inline-block; - padding: 4px; - width: 150px; - } - .range-sep::before { - content: "@("<") "; - } - .range-sep::after { - content: " @("<")"; - } - .center-children { - text-align: center; - } -</style> - -@code { - - [Parameter] - [SupplyParameterFromQuery(Name = "order_by")] - public string? OrderBy { get; set; } - - [Parameter] - [SupplyParameterFromQuery(Name = "name_search")] - public string SearchTerm { get; set; } - - [Parameter] - [SupplyParameterFromQuery(Name = "ascending")] - public bool Ascending { get; set; } - - public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new(); - - private string Status { get; set; } - - public LocalRoomQueryFilter Filter { get; set; } = new(); - - protected override Task OnParametersSetAsync() { - if (Ascending == null) - Ascending = true; - OrderBy ??= "name"; - return Task.CompletedTask; - } - - private async Task Search() { - Results.Clear(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); - if (hs is AuthenticatedHomeserverSynapse synapse) { - var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); - while (await searchRooms.MoveNextAsync()) { - var room = searchRooms.Current; - Console.WriteLine("Hit: " + room.ToJson(false)); - Results.Add(room); - if (Results.Count % 10 == 0) - StateHasChanged(); - } - } - - StateHasChanged(); - } - - private readonly Dictionary<string, string> validOrderBy = new() { - { "name", "Room name" }, - { "canonical_alias", "Main alias address" }, - { "joined_members", "Number of members (reversed)" }, - { "joined_local_members", "Number of local members (reversed)" }, - { "version", "Room version" }, - { "creator", "Creator of the room" }, - { "encryption", "End-to-end encryption algorithm" }, - { "federatable", "Is room federated" }, - { "public", "Visibility in room list" }, - { "join_rules", "Join rules" }, - { "guest_access", "Guest access" }, - { "history_visibility", "Visibility of history" }, - { "state_events", "Number of state events" } - }; - -} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor new file mode 100644
index 0000000..d855cba --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
@@ -0,0 +1,29 @@ +@page "/HSAdmin/Synapse/BackgroundJobs" +@using ArcaneLibs.Extensions +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses + +<h3>Homeserver Administration - Background jobs</h3> +<pre>@BackgroundJobStatus?.ToJson(ignoreNull: true)</pre> + +@code { + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + private SynapseAdminBackgroundUpdateStatusResponse? BackgroundJobStatus { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) as AuthenticatedHomeserverSynapse; + if (hs is null) return; + Homeserver = hs; + + while (true) { + try { + BackgroundJobStatus = await hs.Admin.GetBackgroundUpdatesStatusAsync(); + StateHasChanged(); + await Task.Delay(1000); + } + catch (Exception ex) { + Console.WriteLine(ex); + } + } + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor new file mode 100644
index 0000000..e9d0cd2 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -0,0 +1,192 @@ +@page "/HSAdmin/Synapse/BlockMedia" +@using System.Text.Json +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec +@using LibMatrix.StructuredData + +<h3>Homeserver Administration - Block media</h3> +@if (Homeserver is not null) { + <label>Event URL: </label> + <FancyTextBox @bind-Value="EventUrl"/> + <br/> + <label>Event JSON: </label> + <details> + <summary>@(string.IsNullOrEmpty(EventJson) ? "" : "{ ... }")</summary> + <FancyTextBox Multiline="true" @bind-Value="EventJson"/> + </details> + <br/> + <label>MXC URI: </label> + <FancyTextBox @bind-Value="MxcUrl"/> + <br/> + <label>Room ID: </label> + <FancyTextBox @bind-Value="RoomId"/> + <br/> + <pre>@MxcUri?.ToJson(ignoreNull: true)</pre> + + @if (Event is not null) { + <LinkButton OnClickAsync="@RedactAllEvents">Redact all messages</LinkButton> + } + + @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) { + <p>User is a local user!</p> + <LinkButton OnClickAsync="@DeactivateUser">Deactivate User</LinkButton> + <LinkButton OnClickAsync="@QuarantineMediaByUser">Quarantine all media</LinkButton> + } +} + +<style> + .int-input { + width: 128px; + } + + .tile { + display: inline-block; + padding: 4px; + border: 1px solid #ffffff22; + } + + .tile280 { + min-width: 280px; + } + + .tile150 { + min-width: 150px; + } + + .range-sep { + display: inline-block; + padding: 4px; + width: 150px; + } + + .range-sep::before { + content: "@("<") "; + } + + .range-sep::after { + content: " @("<")"; + } + + .center-children { + text-align: center; + } +</style> + +@code { + + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) as AuthenticatedHomeserverSynapse; + if (hs is null) return; + Homeserver = hs; + + if (!string.IsNullOrWhiteSpace(EventUrl)) { + _ = ExpandEventUrl(); + } + } + + [SupplyParameterFromQuery] + public string? EventUrl { + get; + set { + field = value?.Split('?')[0]; + _ = ExpandEventUrl(); + } + } + + private StateEventResponse? Event { get; set; } + + private string? EventJson { + get; + set { + field = value; + _ = ExpandEventJson(); + } + } + + private string? MxcUrl { + get; + set { + field = value; + _ = ExpandMxcUri(); + } + } + + private MxcUri? MxcUri { get; set; } + + private string? RoomId { + get => Event?.RoomId ?? field; + set; + } + + private async Task ExpandEventUrl() { + Console.WriteLine("Expanding event URL..."); + if (!string.IsNullOrWhiteSpace(EventUrl)) { + Console.WriteLine("Parsing event URL..."); + var data = ParseEventUrl(EventUrl); + Console.WriteLine($"Room: {data.room}, Event: {data.eventId}"); + RoomId = data.room; + var room = Homeserver.GetRoom(data.room); + var eventResponse = await room.GetEventAsync(data.eventId); + eventResponse.RoomId ??= data.room; + EventJson = eventResponse?.ToJson() ?? "null"; + } + + StateHasChanged(); + } + + private async Task ExpandEventJson() { + Console.WriteLine("Expanding event JSON..."); + if (!string.IsNullOrWhiteSpace(EventJson)) { + Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson); + MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url; + Console.WriteLine($"MXC URL: {MxcUrl}"); + + var possiblyRelated = await Homeserver.Admin.GetRoomMediaAsync(Event!.RoomId!); + } + + StateHasChanged(); + } + + private async Task ExpandMxcUri() { + Console.WriteLine("Expanding MXC URI..."); + if (!string.IsNullOrWhiteSpace(MxcUrl)) { + MxcUri = MxcUrl; + } + + StateHasChanged(); + } + + private (string room, string eventId) ParseEventUrl(string url) { + var parts = url.Split('/'); + Console.WriteLine($"Parts: {string.Join(", ", parts)}"); + return (parts[4].UrlDecode(), parts[5].Split('?')[0].UrlDecode()); + } + +#region Local user + + private async Task DeactivateUser() { + await Homeserver.Admin.DeactivateUserAsync(Event.Sender, true); + } + + private async Task QuarantineMediaByUser() { + if (Event is null) return; + var media = Homeserver.Admin.GetUserMediaEnumerableAsync(Event?.Sender!); + await foreach (var m in media) { + if (m is not null) { + // await Homeserver.Admin.QuarantineMedia(m); + // await Homeserver.Admin.DeleteMedia(m); + } + } + } + +#endregion + + private async Task RedactAllEvents() { + if (Event is null) return; + await Homeserver!.Admin.DeleteAllMessages(Event.Sender!); + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
index e69de29..e69de29 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor new file mode 100644
index 0000000..f1c5907 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor
@@ -0,0 +1,74 @@ +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using MatrixUtils.Web.Shared.FilterComponents +<div style="margin-left: 8px; margin-bottom: 8px;"> + <u style="display: block;">String contains</u> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.RoomId" Label="Room ID"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.Name" Label="Room name"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.CanonicalAlias" Label="Canonical alias"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.Creator" Label="Creator"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.Version" Label="Room version"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.Encryption" Label="Encryption algorithm"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.JoinRules" Label="Join rules"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.GuestAccess" Label="Guest access"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.HistoryVisibility" Label="History visibility"/></span> + <span class="tile tile280"><StringFilterComponent Filter="@Filter.Topic" Label="Topic"/></span> + + <u style="display: block;">Optional checks</u> + <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Federation" Label="Is federated"/></span> + <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Public" Label="Is public"/></span> + <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Tombstone" Label="Is tombstoned"/></span> + + <u style="display: block;">Ranges</u> + <span class="tile center-children"> + <InputCheckbox @bind-Value="@Filter.StateEvents.Enabled"/> + @if (!Filter.StateEvents.Enabled) { + <span>State events</span> + } + else { + <InputCheckbox @bind-Value="@Filter.StateEvents.CheckGreaterThan"/> + <span> </span> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.GreaterThan"/> + <span class="range-sep">state events</span> + <InputCheckbox @bind-Value="@Filter.StateEvents.CheckLessThan"/> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.LessThan"/> + } + </span> + <span class="tile center-children"> + <InputCheckbox @bind-Value="@Filter.JoinedMembers.Enabled"/> + @if (!Filter.JoinedMembers.Enabled) { + <span>Joined members</span> + } + else { + <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckGreaterThan"/> + <span> </span> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.GreaterThan"/> + <span class="range-sep">members</span> + <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckLessThan"/> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.LessThan"/> + } + </span> + <span class="tile center-children"> + <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.Enabled"/> + <span> </span> + @if (!Filter.JoinedLocalMembers.Enabled) { + <span>Joined local members</span> + } + else { + <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckGreaterThan"/> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.GreaterThan"/> + <span class="range-sep">local members</span> + <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckLessThan"/> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.LessThan"/> + } + </span> +</div> +@* @{ *@ +@* Console.WriteLine($"Rendered SynapseRoomQueryFilter with filter: {Filter.ToJson()}"); *@ +@* } *@ + +@code { + + [Parameter] + public required SynapseAdminLocalRoomQueryFilter Filter { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css new file mode 100644
index 0000000..83ce426 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css
@@ -0,0 +1,35 @@ +.int-input { + width: 128px; +} + +.tile { + display: inline-block; + padding: 4px; + border: 1px solid #ffffff22; +} + +.tile280 { + min-width: 280px; +} + +.tile150 { + min-width: 150px; +} + +.range-sep { + display: inline-block; + padding: 4px; + width: 150px; +} + +.range-sep::before { + content: "< "; +} + +.range-sep::after { + content: " <"; +} + +.center-children { + text-align: center; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor new file mode 100644
index 0000000..5591072 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor
@@ -0,0 +1,5 @@ +<h3>SynapseRoomQueryResult</h3> + +@code { + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor new file mode 100644
index 0000000..d598994 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
@@ -0,0 +1,5 @@ +<h3>SynapseRoomShutdownWindow</h3> + +@code { + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor new file mode 100644
index 0000000..fc9f8e8 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -0,0 +1,266 @@ +@using System.Text.Json.Serialization +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses + +@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) { + <span>Block room: </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/> + <br/> + <span>Purge room: </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.Purge"/> + <br/> + <span>Force purge room (unsafe): </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox> + <br/> + <details> + <summary>Media</summary> + <span>Quarantine local media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalMedia"/> + <br/> + <span>Quarantine remote media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineRemoteMedia"/> + <br/> + <span>Delete remote media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteRemoteMedia"/> + </details> + + <details> + <summary>Local users</summary> + <span>Suspend local users: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.SuspendLocalUsers"></InputCheckbox> + <br/> + <span>Quarantine <b>ALL</b> local user media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalUserMedia"></InputCheckbox> + <br/> + <span>Delete <b>ALL</b> local user media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox> + <br/> + <span>Follow tombstone (if any): </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.FollowTombstone"/> + @if (!EditorOnly) { + <LinkButton InlineText="true" OnClickAsync="@FollowTombstoneAsync">Exec</LinkButton> + } + </details> + + <details> + <summary>Issue warning to local members (optional)</summary> + <b>All fields are required if used!</b><br/> + <span>Warning room User ID: </span> + <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/> + <br/> + <span>Warning room name: </span> + <FancyTextBox @bind-Value="@Context.DeleteRequest.RoomName"/> + <br/> + <span>Warning room message (plaintext): </span> + <FancyTextBox Multiline="true" @bind-Value="@Context.DeleteRequest.Message"/> + <br/> + </details> + + @if (!EditorOnly) { + <LinkButton OnClickAsync="@DeleteRoom">Execute</LinkButton> + } +} +else { + <pre> + @(_status?.ToJson() ?? "Loading status...") + </pre> + <br/> + <LinkButton InlineText="true" OnClickAsync="@OnComplete">[Stop tracking]</LinkButton> + if (_status?.Status == SynapseAdminRoomDeleteStatus.Failed) { + <LinkButton InlineText="true" OnClickAsync="@ForceDelete">[Force delete]</LinkButton> + } +} + +@code { + + [Parameter] + public required RoomShutdownContext Context { get; set; } + + [Parameter] + public required AuthenticatedHomeserverSynapse Homeserver { get; set; } + + [Parameter] + public bool EditorOnly { get; set; } + + private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!; + private SynapseAdminRoomDeleteStatus? _status = null; + private bool _isTracking = false; + + protected override async Task OnInitializedAsync() { + if (EditorOnly) return; + TaskMap = new NamedCache<RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + var existing = await TaskMap.GetValueAsync(Context.RoomId); + if (existing is not null) { + Context = existing; + } + + if (Context.ExecuteImmediately) + await DeleteRoom(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (EditorOnly) return; + if (!_isTracking) { + if (!string.IsNullOrWhiteSpace(Context.DeleteId)) { + _isTracking = true; + _ = Task.Run(async () => { + do { + _status = await Homeserver.Admin.GetRoomDeleteStatus(Context.DeleteId); + StateHasChanged(); + if (_status.Status == SynapseAdminRoomDeleteStatus.Complete) { + await OnComplete(); + break; + } + + await Task.Delay(1000); + } while (_status.Status != SynapseAdminRoomDeleteStatus.Failed && _status.Status != SynapseAdminRoomDeleteStatus.Complete); + }); + } + } + } + + public class RoomShutdownContext { + public required string RoomId { get; set; } + + [JsonIgnore] // do NOT persist - this triggers immediate purging + public bool ExecuteImmediately { get; set; } + + public string? DeleteId { get; set; } + public ExtraDeleteOptions ExtraOptions { get; set; } = new(); + + public SynapseAdminRoomDeleteRequest DeleteRequest { get; set; } = new() { + Block = true, + Purge = true, + ForcePurge = false + }; + + public SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom? RoomDetails { get; set; } + + public class ExtraDeleteOptions { + public bool FollowTombstone { get; set; } + + // media options + public bool QuarantineLocalMedia { get; set; } + public bool QuarantineRemoteMedia { get; set; } + public bool DeleteRemoteMedia { get; set; } + + // user options + public bool SuspendLocalUsers { get; set; } + public bool QuarantineLocalUserMedia { get; set; } + public bool DeleteLocalUserMedia { get; set; } + } + } + + public async Task OnComplete() { + if (EditorOnly) return; + Console.WriteLine($"Room shutdown task for {Context.RoomId} completed, removing from map."); + await OnCompleteLock.WaitAsync(); + try { + await TaskMap.RemoveValueAsync(Context.RoomId!); + } + catch (Exception e) { + Console.WriteLine("Failed to remove completed room shutdown task from map: " + e); + } + finally { + OnCompleteLock.Release(); + } + } + + public async Task DeleteRoom() { + if (EditorOnly) return; + if (Context.ExtraOptions.FollowTombstone) await FollowTombstoneAsync(); + + Console.WriteLine($"Deleting room {Context.RoomId} with options: " + Context.DeleteRequest.ToJson()); + + var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, Context.DeleteRequest, false); + Context.DeleteId = resp.DeleteId; + await TaskMap.SetValueAsync(Context.RoomId, Context); + } + + private static readonly SemaphoreSlim OnCompleteLock = new(1, 1); + + private async Task FollowTombstoneAsync() { + if (EditorOnly) return; + var tomb = await TryGetTombstoneAsync(); + var content = tomb?.ContentAs<RoomTombstoneEventContent>(); + if (content != null && !string.IsNullOrWhiteSpace(content.ReplacementRoom)) { + Console.WriteLine("Tombstone: " + tomb.ToJson()); + if (!content.ReplacementRoom.StartsWith('!')) { + Console.WriteLine($"Invalid replacement room ID in tombstone: {content.ReplacementRoom}, ignoring!"); + } + else { + var oldMembers = await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true); + var isKnownRoom = await Homeserver.Admin.CheckRoomKnownAsync(content.ReplacementRoom); + var targetMembers = isKnownRoom + ? await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true) + : new() { Members = [] }; + + var members = oldMembers.Members.Except(targetMembers.Members).ToList(); + Console.WriteLine("To migrate: " + members.ToJson()); + foreach (var member in members) { + var success = false; + do { + var sess = member == Homeserver.WhoAmI.UserId ? Homeserver : await Homeserver.Admin.GetHomeserverForUserAsync(member, TimeSpan.FromSeconds(15)); + var oldRoom = sess.GetRoom(Context.RoomId); + var room = sess.GetRoom(content.ReplacementRoom); + try { + var servers = (await oldRoom.GetMembersByHomeserverAsync(joinedOnly: true)) + .Select(x => new KeyValuePair<string, int>(x.Key, x.Value.Count)) + .OrderByDescending(x => x.Key == "matrix.org" ? 0 : x.Value); // try anything else first, to reduce load on matrix.org + + await room.JoinAsync(servers.Take(10).Select(x => x.Key).ToArray(), reason: "Automatically following tombstone as old room is being purged.", checkIfAlreadyMember: isKnownRoom); + Console.WriteLine($"Migrated {member} from {Context.RoomId} to {content.ReplacementRoom}"); + success = true; + } + catch (Exception e) { + if (e is MatrixException { ErrorCode: "M_FORBIDDEN" }) { + Console.WriteLine($"Cannot migrate {member} to {content.ReplacementRoom}: {(e as MatrixException)!.GetAsJson()}"); + success = true; // give up + continue; + } + + Console.WriteLine($"Failed to invite {member} to {content.ReplacementRoom}: {e}"); + success = false; + await Task.Delay(1000); + } + } while (!success); + } + } + } + } + + private async Task<StateEventResponse?> TryGetTombstoneAsync() { + if (EditorOnly) return null; + try { + return (await Homeserver.Admin.GetRoomStateAsync(Context.RoomId, RoomTombstoneEventContent.EventId)).Events.FirstOrDefault(x => x.StateKey == ""); + } + catch { + return null; + } + } + + private async Task ForceDelete() { + if (EditorOnly) return; + Console.WriteLine($"Forcing purge for {Context.RoomId}!"); + await OnCompleteLock.WaitAsync(); + try { + var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, new() { + ForcePurge = true + }, waitForCompletion: false); + Context.DeleteId = resp.DeleteId; + await TaskMap.SetValueAsync(Context.RoomId, Context); + StateHasChanged(); + } + catch (Exception e) { + Console.WriteLine("Failed to remove completed room shutdown task from map: " + e); + } + finally { + OnCompleteLock.Release(); + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor new file mode 100644
index 0000000..05899c8 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -0,0 +1,618 @@ +@page "/HSAdmin/Synapse/RoomQuery" +@using System.Diagnostics.CodeAnalysis +@using System.Text.Json +@using ArcaneLibs.Blazor.Components.Services +@using Microsoft.AspNetCore.WebUtilities +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery +@inject ILogger<RoomQuery> Logger +@inject BlazorSaveFileService BlazorSaveFileService + +<h3>Homeserver Administration - Room Query</h3> + +<label>Search name: </label> +<InputText @bind-Value="SearchTerm"/><br/> +<label>Order by: </label> +<select @bind="OrderBy"> + @foreach (var item in validOrderBy) { + <option value="@item.Key">@item.Value</option> + } +</select><br/> +<InputCheckbox @bind-Value="Ascending"/> +<label> Ascending</label><br/> +<InputCheckbox @bind-Value="FetchV12PlusCreatorServer"/> +<label> Fetch v12+ room creation homeserver</label> +<LinkButton InlineText="true" OnClickAsync="FetchV12PlusCreatorServersAsync"> (Execute manually)</LinkButton><br/> +<InputCheckbox @bind-Value="FetchTombstones"/> +<label> Check for tombstone events</label> +<LinkButton InlineText="true" OnClickAsync="FetchTombstoneEventsAsync"> (Execute manually)</LinkButton><br/> +<InputCheckbox @bind-Value="SummarizeLocalMembers"/> +<label> Fetch local member list for small rooms</label> +<LinkButton InlineText="true" OnClickAsync="FetchLocalMemberEventsAsync"> (Execute manually)</LinkButton><br/> +<InputCheckbox @bind-Value="ShowFullResultData"/> +<label> Show full result data (JSON)</label><br/> +<InputCheckbox @bind-Value="EnableMultiPurge"/> +<label> Enable multi-purge mode</label> +@if (EnableMultiPurge) { + <span> </span> + <LinkButton InlineText="true" OnClick="@MultiPurgeInvertSelection">[Invert selection]</LinkButton> + <span> </span> + <details style="display: inline-block;"> + <summary>Edit purge options</summary> + <SynapseRoomShutdownWindowContent Context="@DefaultShutdownContext" Homeserver="Homeserver" EditorOnly="true"/> + </details> +} +else { + <br/> +} +<details> + <summary>Local filtering (slow)</summary> + <SynapseRoomQueryFilter Filter="@Filter"/> +</details> +<LinkButton OnClickAsync="@Search">Search</LinkButton> + +@if (EnableMultiPurge) { + <LinkButton Color="#FF8800" OnClick="@PurgeSelection">Purge selected rooms</LinkButton> +} +<br/> + +@if (Results.Count > 0) { + <p>Found @Results.Count rooms</p> +} + +@foreach (var room in Results) { + <div class="room-list-item"> + @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ + <p> + @if (EnableMultiPurge) { + <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/> + <span> </span> + } + @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { + <span>@room.CanonicalAlias - </span> + } + <span>@room.RoomId</span> + @if (!string.IsNullOrWhiteSpace(room.Name)) { + <span> (@room.Name)</span> + } + <br/> + + @if (!string.IsNullOrWhiteSpace(room.Creator)) { + <span>Created by @room.Creator</span> + <br/> + } + </p> + <p> + <LinkButton OnClickAsync="@(() => DeleteRoom(room))">Delete room</LinkButton> + <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.OriginHomeserver}")">Resync state</LinkButton> + <LinkButton OnClickAsync="@(() => ExportState(room))">@(room.JoinedLocalMembers == 0 ? "Try to export state" : "Export state")</LinkButton> + <LinkButton OnClickAsync="@(() => ForceJoin(room))">Force Join</LinkButton> + </p> + + @{ + List<string?> flags = []; + if (room.JoinedLocalMembers > 0) { + flags.Add(room.JoinRules switch { + "public" => "Public", + "invite" => "Invite only", + "knock" => "Knock", + "restricted" => "Restricted", + "knock_restricted" => "Knock + restricted", + // TODO: default? + null => null, + "" => null, + _ => "unknown join rule: " + room.JoinRules + }); + + if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted"); + if (!room.Federatable) flags.Add("unfederated"); + + flags.Add(room.HistoryVisibility switch { + "world_readable" => "world readable history", + "shared" => "shared history", + "invited" => "history since invite", + "joined" => "history since join", + // TODO: default? + null => null, + "" => null, + _ => "unknown history setting: " + room.HistoryVisibility + }); + + flags.Add(room.GuestAccess switch { + "can_join" => "guests allowed", + "forbidden" => null, + // TODO: default? + null => null, + "" => null, + _ => "unknown guest access: " + room.GuestAccess, + }); + + flags = flags.Where(x => x != null).ToList(); + } + } + <span>@string.Join(", ", flags)</span> + @if (room.JoinedLocalMembers == 0 && flags.Count > 0) { + <span> at the time of leaving</span> + } + <br/> + + <span>@room.StateEvents state events, room version @(room.Version ?? "1")</span><br/> + @if (room.TombstoneEvent is not null) { + var tombstoneContent = room.TombstoneEvent.ContentAs<RoomTombstoneEventContent>()!; + <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span> + <br/> + } + + @{ + var memberSummary = room.MemberSummary; + if (room.LocalMembers is not null) { + memberSummary += $": {string.Join(", ", room.LocalMembers)}"; + } + } + <span>@memberSummary</span><br/> + @if (!string.IsNullOrWhiteSpace(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)) { + <details> + <summary>Room topic</summary> + <pre>@(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)</pre> + </details> + } + @foreach (var ex in room.Exceptions) { + <span style="color: red;">@ex</span> + <br/> + } + @if (ShowFullResultData) { + <details> + <summary>Full result data</summary> + <pre>@room.ToJson(ignoreNull: true)</pre> + </details> + } + </div> +} +@* *@ +@* @if (DeleteRequest.HasValue) { *@ +@* <ModalWindow MinWidth="600" Title="@("Delete " + DeleteRequest.Value.RoomId)" OnCloseClicked="@(() => { DeleteRequest = null; })"> *@ +@* *@ +@* </ModalWindow> *@ +@* } *@ + +@* @foreach (var (roomId, status) in DeleteStatuses) { *@ +@* <ModalWindow Title="@("Delete status for " + roomId)" MinWidth="600"> *@ +@* <pre>@status.ToJson()</pre> *@ +@* </ModalWindow> *@ +@* } *@ + +@foreach (var (roomId, deleteRequest) in DeleteRequests) { + <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => { + DeleteRequests.Remove(roomId); + StateHasChanged(); + })"> + <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> + </ModalWindow> +} + +@code { + + [Parameter] + [SupplyParameterFromQuery(Name = "order_by")] + public string? OrderBy { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "name_search")] + public string? SearchTerm { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "ascending")] + public bool Ascending { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "FetchV12PlusCreatorServer")] + public bool FetchV12PlusCreatorServer { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "SummarizeLocalMembers")] + public bool SummarizeLocalMembers { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "FetchTombstones")] + public bool FetchTombstones { get; set; } = true; + + private List<RoomInfo> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; + + private SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); + + private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = []; + + // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new(); + + private NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext> TaskMap { get; set; } = null!; + + private SynapseRoomShutdownWindowContent.RoomShutdownContext DefaultShutdownContext { get; set; } = new() { + RoomId = "", + DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false } + }; + + public bool ShowFullResultData { + get; + set { + field = value; + StateHasChanged(); + } + } + + public bool EnableMultiPurge { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is not AuthenticatedHomeserverSynapse synapse) { + NavigationManager.NavigateTo("/"); + return; + } + + Homeserver = synapse; + TaskMap = new NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + DeleteRequests = (await TaskMap.ReadCacheMapAsync()).Where(x => x.Value.DeleteId is not null).ToDictionary(); + StateHasChanged(); + } + + protected override Task OnParametersSetAsync() { + OrderBy ??= "name"; + + var execute = false; + + foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) { + switch (key) { + case "RoomIdContains": + Filter.RoomId.Enabled = Filter.RoomId.CheckValueContains = true; + Filter.RoomId.ValueContains = value[0]!; + break; + case "NameContains": + Filter.Name.Enabled = Filter.Name.CheckValueContains = true; + Filter.Name.ValueContains = value[0]!; + break; + case "CanonicalAliasContains": + Filter.CanonicalAlias.Enabled = Filter.CanonicalAlias.CheckValueContains = true; + Filter.CanonicalAlias.ValueContains = value[0]!; + break; + case "VersionContains": + Filter.Version.Enabled = Filter.Version.CheckValueContains = true; + Filter.Version.ValueContains = value[0]!; + break; + case "CreatorContains": + Filter.Creator.Enabled = Filter.Creator.CheckValueContains = true; + Filter.Creator.ValueContains = value[0]!; + break; + case "EncryptionContains": + Filter.Encryption.Enabled = Filter.Encryption.CheckValueContains = true; + Filter.Encryption.ValueContains = value[0]!; + break; + case "JoinRulesContains": + Filter.JoinRules.Enabled = Filter.JoinRules.CheckValueContains = true; + Filter.JoinRules.ValueContains = value[0]!; + break; + case "GuestAccessContains": + Filter.GuestAccess.Enabled = Filter.GuestAccess.CheckValueContains = true; + Filter.GuestAccess.ValueContains = value[0]!; + break; + case "HistoryVisibilityContains": + Filter.HistoryVisibility.Enabled = Filter.HistoryVisibility.CheckValueContains = true; + Filter.HistoryVisibility.ValueContains = value[0]!; + break; + case "Federatable": + Filter.Federation = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; + break; + case "Public": + Filter.Public = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; + break; + case "JoinedMembersGreaterThan": + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedMembers.GreaterThan = int.Parse(value[0]!); + break; + case "JoinedMembersLessThan": + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedMembers.LessThan = int.Parse(value[0]!); + break; + case "JoinedLocalMembersGreaterThan": + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedLocalMembers.GreaterThan = int.Parse(value[0]!); + break; + case "JoinedLocalMembersLessThan": + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedLocalMembers.LessThan = int.Parse(value[0]!); + break; + case "StateEventsGreaterThan": + Filter.StateEvents.Enabled = Filter.StateEvents.CheckGreaterThan = true; + Filter.StateEvents.GreaterThan = int.Parse(value[0]!); + break; + case "StateEventsLessThan": + Filter.StateEvents.Enabled = Filter.StateEvents.CheckLessThan = true; + Filter.StateEvents.LessThan = int.Parse(value[0]!); + break; + case "Execute": + execute = true; + break; + case "order_by": + case "name_search": + case "ascending": + case "FetchV12PlusCreatorServer": + case "SummarizeLocalMembers": + case "FetchTombstones": + break; + default: + Console.WriteLine($"Unknown query parameter: {key}"); + break; + } + } + + StateHasChanged(); + + if (execute) + _ = Search(); + + return Task.CompletedTask; + } + + private async Task Search() { + Results.Clear(); + Console.WriteLine("Starting search... Parameters: " + new { + orderBy = OrderBy!, + dir = Ascending ? "f" : "b", + searchTerm = SearchTerm, + localFilter = Filter, + chunkLimit = 1000, + fetchTombstones = FetchTombstones, + fetchTopics = true, + fetchCreateEvents = true + }.ToJson()); + var searchRooms = Homeserver.Admin.SearchRoomsAsync( + orderBy: OrderBy!, + dir: Ascending ? "f" : "b", + searchTerm: SearchTerm, + localFilter: Filter, + chunkLimit: 1000, + fetchTombstones: FetchTombstones, + fetchTopics: true, + fetchCreateEvents: true + ).GetAsyncEnumerator(); + var joinedRooms = await Homeserver.GetJoinedRooms(); + while (await searchRooms.MoveNextAsync()) { + var room = searchRooms.Current; + + var roomInfo = new RoomInfo { + RoomId = room.RoomId, + Name = room.Name, + CanonicalAlias = room.CanonicalAlias, + Creator = room.Creator, + Version = room.Version, + Encryption = room.Encryption, + Federatable = room.Federatable, + Public = room.Public, + JoinRules = room.JoinRules, + GuestAccess = room.GuestAccess, + HistoryVisibility = room.HistoryVisibility, + StateEvents = room.StateEvents, + JoinedMembers = room.JoinedMembers, + JoinedLocalMembers = room.JoinedLocalMembers, + OriginHomeserver = + Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId + ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty) + : string.Empty + }; + + if (string.IsNullOrWhiteSpace(roomInfo.OriginHomeserver) && FetchV12PlusCreatorServer) { + try { + if (joinedRooms.Any(x => x.RoomId == room.RoomId)) + roomInfo.OriginHomeserver = await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync(); + else roomInfo.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]; + } + catch (MatrixException e) { + roomInfo.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + } + + Results.Add(roomInfo); + + if ((Results.Count <= 200 && Results.Count % 10 == 0 && FetchV12PlusCreatorServer) || Results.Count % 1000 == 0) { + StateHasChanged(); + await Task.Yield(); + await Task.Delay(1); + } + } + + StateHasChanged(); + + if (FetchV12PlusCreatorServer) await FetchV12PlusCreatorServersAsync(false); + if (SummarizeLocalMembers) await FetchLocalMemberEventsAsync(false); + // if (CheckTombstone) await FetchTombstoneEventsAsync(false); + + StateHasChanged(); + } + + private Task DeleteRoom(RoomInfo room, bool executeWithoutConfirmation = false) { + var dc = JsonSerializer.Deserialize<SynapseRoomShutdownWindowContent.RoomShutdownContext>(DefaultShutdownContext.ToJson())!; + dc.RoomId = room.RoomId; + dc.RoomDetails = room; + dc.ExecuteImmediately = executeWithoutConfirmation; + DeleteRequests.TryAdd(room.RoomId, dc); + StateHasChanged(); + + return Task.CompletedTask; + } + + private void PurgeSelection() { + foreach (var room in Results.Where(x => x.MultiPurgeSelected)) { + DeleteRoom(room, true); + } + } + + private readonly Dictionary<string, string> validOrderBy = new() { + { "name", "Room name" }, + { "canonical_alias", "Main alias address" }, + { "joined_members", "Number of members (reversed)" }, + { "joined_local_members", "Number of local members (reversed)" }, + { "version", "Room version" }, + { "creator", "Creator of the room" }, + { "encryption", "End-to-end encryption algorithm" }, + { "federatable", "Is room federated" }, + { "public", "Visibility in room list" }, + { "join_rules", "Join rules" }, + { "guest_access", "Guest access" }, + { "history_visibility", "Visibility of history" }, + { "state_events", "Number of state events" } + }; + + private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { + public List<string>? LocalMembers { get; set; } + public required string OriginHomeserver { get; set; } + + [field: AllowNull, MaybeNull] + public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server"; + + public List<string> Exceptions { get; set; } = []; + public bool MultiPurgeSelected { get; set; } + } + + private async Task ExportState(RoomInfo room) { + try { + var state = await Homeserver.Admin.GetRoomStateAsync(room.RoomId); + var json = state.ToJson(); + await BlazorSaveFileService.SaveFileAsync($"{room.RoomId.Replace(":", "_")}_state.json", System.Text.Encoding.UTF8.GetBytes(json), "application/json"); + } + catch (Exception e) { + Logger.LogError(e, "Failed to export room state for {RoomId}", room.RoomId); + } + } + + private async Task ForceJoin(RoomInfo room) { + try { + await Homeserver.GetRoom(room.RoomId).JoinAsync([Homeserver.ServerName]); + } + catch (Exception e) { + Logger.LogError(e, "Failed to force-join room {RoomId}", room.RoomId); + // await Homeserver.Admin.room + } + } + + private SemaphoreSlim _concurrencyLimiter = new SemaphoreSlim(16, 16); + + private async Task FetchV12PlusCreatorServersAsync() => await FetchV12PlusCreatorServersAsync(true); + + private async Task FetchV12PlusCreatorServersAsync(bool rerender) { + var joinedRooms = await Homeserver.GetJoinedRooms(); + var tasks = Results + .Where(x => string.IsNullOrWhiteSpace(x.OriginHomeserver)) + .Select(async r => { + if (!string.IsNullOrWhiteSpace(r.Creator) && r.Creator.Contains(':')) { + r.OriginHomeserver = r.Creator.Split(':', 2)[1]; + return; + } + + if (r.CreateEvent != null && !string.IsNullOrWhiteSpace(r.CreateEvent.Sender) && r.CreateEvent.Sender.Contains(':')) { + r.OriginHomeserver = r.CreateEvent.Sender.Split(':', 2)[1]; + return; + } + + await _concurrencyLimiter.WaitAsync(); + try { + if (joinedRooms.Any(x => x.RoomId == r.RoomId)) + r.OriginHomeserver = await Homeserver.GetRoom(r.RoomId).GetOriginHomeserverAsync(); + else r.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(r.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]; + } + catch (MatrixException e) { + r.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to get origin homeserver for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(tasks); + + if (rerender) + StateHasChanged(); + } + + private async Task FetchTombstoneEventsAsync() => await FetchTombstoneEventsAsync(true); + + private async Task FetchTombstoneEventsAsync(bool rerender) { + var getTombstoneTasks = Results + .Where(x => x.TombstoneEvent is null) + .Select(async r => { + await _concurrencyLimiter.WaitAsync(); + try { + var state = await Homeserver.Admin.GetRoomStateAsync(r.RoomId, type: "m.room.tombstone"); + var tombstone = state.Events.FirstOrDefault(x => x is { StateKey: "", Type: "m.room.tombstone" }); + if (tombstone is { } tombstoneEvent) { + r.TombstoneEvent = tombstoneEvent; + } + } + catch (MatrixException e) { + r.Exceptions.Add($"While checking for tombstone: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to check tombstone for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(getTombstoneTasks); + + if (rerender) + StateHasChanged(); + } + + private async Task FetchLocalMemberEventsAsync() => await FetchLocalMemberEventsAsync(true); + + private async Task FetchLocalMemberEventsAsync(bool rerender) { + var getLocalMembersTasks = Results + .Where(x => x.LocalMembers is null && x.JoinedLocalMembers is > 0 and < 100) + .Select(async r => { + await _concurrencyLimiter.WaitAsync(); + try { + var members = (await Homeserver.Admin.GetRoomMembersAsync(r.RoomId)).Members.Where(x => x.EndsWith(":" + Homeserver.ServerName)).ToList(); + r.LocalMembers = members; + } + catch (MatrixException e) { + r.Exceptions.Add($"While fetching local members: {e.GetAsObject().ToJson(ignoreNull: true, indent: false)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to fetch local members for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(getLocalMembersTasks); + + if (rerender) + StateHasChanged(); + } + + private void MultiPurgeInvertSelection() { + foreach (var room in Results) { + room.MultiPurgeSelected ^= true; + } + + StateHasChanged(); + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css new file mode 100644
index 0000000..62941e5 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
@@ -0,0 +1,7 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor new file mode 100644
index 0000000..c2446a2 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -0,0 +1,212 @@ +@page "/HSAdmin/Synapse/ResyncState" +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests + +<h3>Resync room state with other server</h3> +<hr/> + +@if (!Executing) { + <p>WARNING: Will likely not work on invite-only/knock rooms! May also mess with history visibility!</p> + <p>If the room is using mjolnir/draupnir, it's probably recommended to set the "via" to the server it's hosted on.</p> + <span>Room ID: </span> + <InputText @bind-Value="@RoomId"></InputText> + <br/> + <span>Via: </span> + <InputText @bind-Value="@Via"></InputText> + <br/> + <LinkButton OnClickAsync="@Execute">Execute</LinkButton> +} + +@if (Executing) { + <p>Execution in progress. DO NOT CLOSE THIS PAGE!</p> +} +@* stage 1 *@ +@if (Stage >= 1) { + @if (Members is null) { + <p>Loading members...</p> + } + else { + <p>Got @Members.Count local members</p> + } +} + +@* stage 2 *@ +@if (Stage == 2) { + <p>Purging room, please wait...</p> + <pre>@DeleteStatus.ToJson(ignoreNull: true)</pre> +} + +@* stage 3 *@ + +@if (Stage == 3) { + <p>Rejoining room, please wait...</p> + <p>Members left to restore: </p> + string members = ""; + foreach (var member in Members) { + members += $"{member.StateKey} ({member.ContentAs<RoomMemberEventContent>()?.ToJson(indent: false, ignoreNull: true)})\n"; + } + + <pre> + @members + </pre> +} + +@if (Stage == 4) { + <p>Execution finished. You may now close the page :)</p> +} + +@if (Error is not null) { + <p style="color: red">Error: @Error.Message</p> + <pre> + @Error.ToString() + </pre> +} + +@code { + + [Parameter] + [SupplyParameterFromQuery] + public string? RoomId { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "via")] + public string? Via { get; set; } + + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + + // Execution flow + private int Stage { get; set; } + private bool Executing { get; set; } + private Exception? Error { get; set; } + + // Stage 1 + private List<StateEventResponse>? Members { get; set; } + + // Stage 2 + private SynapseAdminRoomDeleteStatus? DeleteStatus { get; set; } + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverSynapse hs) return; + Homeserver = hs; + + StateHasChanged(); + } + + private Task Execute() => Execute(0); + + private async Task Execute(int startStage) { + if (string.IsNullOrWhiteSpace(RoomId)) return; + if (string.IsNullOrWhiteSpace(Via)) return; + Executing = true; + StateHasChanged(); + + await ExecuteStages(startStage); + + StateHasChanged(); + } + + private async Task ExecuteStages(int startStage) { + if (startStage <= 1) + if (!await TryGetRoomMembers()) + return; + if (startStage <= 2) + if (!await TryPurgeRoom()) + return; + if (startStage <= 3) + if (!await TryRestoreRoom()) + return; + + Stage = 4; + Executing = false; + StateHasChanged(); + } + + private async Task<bool> TryGetRoomMembers() { + Stage = 1; + try { + Members = (await Homeserver.Admin.GetRoomStateAsync(RoomId, type: RoomMemberEventContent.EventId)) + .Events.Where(m => (m.StateKey?.EndsWith(':' + Homeserver.ServerName) ?? false) && m.ContentAs<RoomMemberEventContent>()!.Membership == "join") + .ToList(); + Console.WriteLine(Members.ToJson(ignoreNull: true)); + StateHasChanged(); + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + + private async Task<bool> TryPurgeRoom() { + Stage = 2; + + try { + var resp = await Homeserver.Admin.DeleteRoom(RoomId, new SynapseAdminRoomDeleteRequest { + Block = true, + Purge = true, + // ForcePurge = true // This causes synapse to early return and not actually purge stuff... + }, waitForCompletion: false); + + while (true) { + // we dont want API failure to break this step + try { + DeleteStatus = await Homeserver.Admin.GetRoomDeleteStatus(resp.DeleteId); + StateHasChanged(); + if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Complete) { + return true; + } + + if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Failed) { + Error = new Exception("Failed to delete room: " + DeleteStatus.ToJson()); + return Executing = false; + } + + await Task.Delay(1000); + } + catch { } + } + + StateHasChanged(); + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + + private async Task<bool> TryRestoreRoom() { + Stage = 3; + try { + await Homeserver.Admin.BlockRoom(RoomId, block: false); + Members = Random.Shared.GetItems(Members.ToArray(), Members.Count).ToList(); + StateHasChanged(); + foreach (var member in Members) { + while (true) { + try { + var hs = member.StateKey == Homeserver.WhoAmI.UserId + ? Homeserver + : await Homeserver.Admin.GetHomeserverForUserAsync(member.StateKey!, TimeSpan.FromMinutes(120)); + await hs.GetRoom(RoomId).JoinAsync([Via], reason: "Reconciling state with " + Via, false); + await hs.GetRoom(RoomId).SendStateEventAsync(RoomMemberEventContent.EventId, member.StateKey, member.RawContent); + Members = Members.Skip(1).ToList(); + StateHasChanged(); + break; + } + catch (Exception e) { + Error = new Exception($"{DateTime.Now:u} Failed to join room: {member.StateKey}, retrying\n", e); + } + } + } + + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor new file mode 100644
index 0000000..54ac800 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
@@ -0,0 +1,243 @@ +@page "/HSAdmin/Synapse/UserQuery" +@using Microsoft.AspNetCore.WebUtilities +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery +@inject ILogger<RoomQuery> Logger + +<h3>Homeserver Administration - User Query</h3> + +<label>Search name: </label> +<InputText @bind-Value="SearchTerm"/><br/> +<label>Order by: </label> +<select @bind="OrderBy"> + @foreach (var item in validOrderBy) { + <option value="@item.Key">@item.Value</option> + } +</select><br/> +<label>Ascending: </label> +<InputCheckbox @bind-Value="Ascending"/><br/> +<details> + <summary> + <span>Local filtering (slow)</span> + </summary> + @* <SynapseRoomQueryFilter Filter="@Filter"/> *@ +</details> +<button class="btn btn-primary" @onclick="Search">Search</button> +<br/> + +@if (Results.Count > 0) { + <p>Found @Results.Count rooms</p> + @* <details> *@ + @* <summary>TSV data (copy/paste)</summary> *@ + @* <pre style="font-size: 0.6em;"> *@ + @* <table> *@ + @* @foreach (var res in Results) { *@ + @* <tr> *@ + @* <td style="padding: 8px;">@res.RoomId@("\t")</td> *@ + @* <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td> *@ + @* <td style="padding: 8px;">@res.Creator@("\t")</td> *@ + @* <td style="padding: 8px;">@res.Name</td> *@ + @* </tr> *@ + @* } *@ + @* </table> *@ + @* </pre> *@ + @* </details> *@ +} + +@foreach (var user in Results) { + <div class="room-list-item"> + <p> + <span>@user.Name</span> + @if (!string.IsNullOrWhiteSpace(user.DisplayName)) { + <span> (@user.DisplayName)</span> + } + <br/> + </p> + <p> + <LinkButton OnClickAsync="@(() => Login(user))">Log in</LinkButton> + @* <LinkButton OnClickAsync="@(() => DeleteRoom(user))">Delete room</LinkButton> *@ + @* <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={user.RoomId}&via={user.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton> *@ + + </p> + + @{ + List<string?> flags = []; + if (user.IsGuest == true) flags.Add("guest"); + if (user.Admin == true) flags.Add("admin"); + if (user.Deactivated == true) flags.Add("deactivated"); + if (user.Erased == true) flags.Add("erased"); + if (user.ShadowBanned == true) flags.Add("shadow banned"); + if (user.Locked == true) flags.Add("locked"); + if (user.Approved == true) flags.Add("approved"); + + if (!string.IsNullOrWhiteSpace(user.UserType)) flags.Add($"type=\"{user.UserType}\""); + + flags = flags.Where(x => x != null).ToList(); + } + <span>@string.Join(", ", flags)</span> + <br/> + + <details> + <summary>Full result data</summary> + <pre>@user.ToJson(ignoreNull: true)</pre> + </details> + </div> +} + +@code { + + [Parameter] + [SupplyParameterFromQuery(Name = "order_by")] + public string? OrderBy { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "name_search")] + public string? SearchTerm { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "ascending")] + public bool Ascending { get; set; } = true; + + private List<SynapseAdminUserListResult.SynapseAdminUserListResultUser> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; + + private SynapseAdminLocalUserQueryFilter Filter { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is not AuthenticatedHomeserverSynapse synapse) { + NavigationManager.NavigateTo("/"); + return; + } + + Homeserver = synapse; + StateHasChanged(); + } + + protected override Task OnParametersSetAsync() { + OrderBy ??= "name"; + + var execute = false; + + foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) { + switch (key) { + // case "RoomIdContains": + // Filter.RoomIdContains = value[0]!; + // break; + // case "NameContains": + // Filter.NameContains = value[0]!; + // break; + // case "CanonicalAliasContains": + // Filter.CanonicalAliasContains = value[0]!; + // break; + // case "VersionContains": + // Filter.VersionContains = value[0]!; + // break; + // case "CreatorContains": + // Filter.CreatorContains = value[0]!; + // break; + // case "EncryptionContains": + // Filter.EncryptionContains = value[0]!; + // break; + // case "JoinRulesContains": + // Filter.JoinRulesContains = value[0]!; + // break; + // case "GuestAccessContains": + // Filter.GuestAccessContains = value[0]!; + // break; + // case "HistoryVisibilityContains": + // Filter.HistoryVisibilityContains = value[0]!; + // break; + // case "Federatable": + // Filter.Federatable = bool.Parse(value[0]!); + // Filter.CheckFederation = true; + // break; + // case "Public": + // Filter.Public = value[0] == "true"; + // Filter.CheckPublic = true; + // break; + // case "JoinedMembersGreaterThan": + // Filter.JoinedMembersGreaterThan = int.Parse(value[0]!); + // break; + // case "JoinedMembersLessThan": + // Filter.JoinedMembersLessThan = int.Parse(value[0]!); + // break; + // case "JoinedLocalMembersGreaterThan": + // Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!); + // break; + // case "JoinedLocalMembersLessThan": + // Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!); + // break; + // case "StateEventsGreaterThan": + // Filter.StateEventsGreaterThan = int.Parse(value[0]!); + // break; + // case "StateEventsLessThan": + // Filter.StateEventsLessThan = int.Parse(value[0]!); + // break; + case "Execute": + execute = true; + break; + default: + Console.WriteLine($"Unknown query parameter: {key}"); + break; + } + } + + if (execute) + _ = Search(); + + return Task.CompletedTask; + } + + private async Task Search() { + Results.Clear(); + var searchRooms = Homeserver.Admin.SearchUsersAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", localFilter: Filter).GetAsyncEnumerator(); + while (await searchRooms.MoveNextAsync()) { + var room = searchRooms.Current; + + Results.Add(room); + + if ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) { + StateHasChanged(); + await Task.Yield(); + await Task.Delay(1); + } + } + + StateHasChanged(); + + StateHasChanged(); + } + + private readonly Dictionary<string, string> validOrderBy = new() { + { "name", "User name" }, + { "is_guest", "Guest status" }, + { "admin", "Admin status" }, + { "user_type", "User type" }, + { "deactivated", "Deactivation status" }, + { "shadow_banned", "Shadow banned status" }, + { "displayname", "Display name" }, + { "avatar_url", "Avatar URL" }, + { "creation_ts", "Creation time" }, + { "last_seen_ts", "Last activity" }, + }; + + private async Task Login(SynapseAdminUserListResult.SynapseAdminUserListResultUser user) { + var loginResult = await Homeserver.Admin.LoginUserAsync(user.Name, TimeSpan.FromDays(1)); + await sessionStore.AddSession(new() { + AccessToken = loginResult.AccessToken, + DeviceId = loginResult.DeviceId, + UserId = loginResult.UserId, + Homeserver = Homeserver.ServerName, + Proxy = Homeserver.Proxy + }); + + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css new file mode 100644
index 0000000..62941e5 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css
@@ -0,0 +1,7 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSEInit.razor b/MatrixUtils.Web/Pages/HSEInit.razor
index cabc671..1eb556a 100644 --- a/MatrixUtils.Web/Pages/HSEInit.razor +++ b/MatrixUtils.Web/Pages/HSEInit.razor
@@ -19,7 +19,7 @@ async Task<UserAuth?> Login() { try { - var result = new UserAuth(await hsProvider.Login("http://localhost:5298", $"{Guid.NewGuid().ToString()}", "")); + var result = new UserAuth(await HsProvider.Login("http://localhost:5298", $"{Guid.NewGuid().ToString()}", "")); if (result == null) { Console.WriteLine($"Failed to login!"); return null; diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index a7619ae..82ee0f2 100644 --- a/MatrixUtils.Web/Pages/Index.razor +++ b/MatrixUtils.Web/Pages/Index.razor
@@ -4,6 +4,7 @@ @using LibMatrix @using ArcaneLibs @using System.Diagnostics +@using LibMatrix.Responses.Federation <PageTitle>Index</PageTitle> @@ -19,23 +20,32 @@ Small collection of tools to do not-so-everyday things. </span> } <hr/> -<form> +<form aria-busy="@Busy"> <table> @foreach (var session in _sessions.OrderByDescending(x => x.UserInfo.RoomCount)) { - var _auth = session.UserAuth; + var auth = session.Auth; <tr class="user-entry"> <td> - <img class="avatar" src="@session.UserInfo.AvatarUrl" crossorigin="anonymous"/> + @if (!string.IsNullOrWhiteSpace(@session.UserInfo?.AvatarUrl)) { + // Console.WriteLine($"Rendering {session.UserInfo.AvatarUrl} with homeserver {session.Homeserver}"); + <MxcAvatar Homeserver="@session.Homeserver" MxcUri="@session.UserInfo.AvatarUrl" Circular="true" Size="4" SizeUnit="em"/> + } + else { + <img class="avatar" src="@_identiconGenerator.GenerateAsDataUri(session.Homeserver.WhoAmI.UserId)"/> + } + @* <img class="avatar" src="@session.UserInfo.AvatarUrl" crossorigin="anonymous"/> *@ </td> <td class="user-info"> <p> - <input type="radio" name="csa" checked="@(_currentSession.AccessToken == _auth.AccessToken)" @onclick="@(() => SwitchSession(_auth))" style="text-decoration-line: unset;"/> - <b>@session.UserInfo.DisplayName</b> on <b>@_auth.Homeserver</b><br/> + <input type="radio" name="csa" checked="@(_currentSession.Auth.AccessToken == auth.AccessToken)" @onclick="@(() => SwitchSession(session.SessionId))" + style="text-decoration-line: unset;"/> + <b>@session.UserInfo.DisplayName</b> on <b>@auth.Homeserver</b><br/> </p> <span style="display: inline-block; width: 128px;">@session.UserInfo.RoomCount rooms</span> - <a style="color: #888888" href="@("/ServerInfo/" + session.Homeserver?.ServerName + "/")">@session.ServerVersion?.Server.Name @session.ServerVersion?.Server.Version</a> - @if (_auth.Proxy != null) { - <span class="badge badge-info"> (proxied via @_auth.Proxy)</span> + <a style="color: #888888" + href="@("/ServerInfo/" + session.Homeserver?.ServerName + "/")">@session.ServerVersion?.Server.Name @session.ServerVersion?.Server.Version</a> + @if (auth.Proxy != null) { + <span class="badge badge-info"> (proxied via @auth.Proxy)</span> } else { <p>Not proxied</p> @@ -44,13 +54,14 @@ Small collection of tools to do not-so-everyday things. <p>T=@session.Homeserver.GetType().FullName</p> <p>D=@session.Homeserver.WhoAmI.DeviceId</p> <p>U=@session.Homeserver.WhoAmI.UserId</p> + <p>S=@session.Homeserver.WhoAmI.UserId</p> } </td> <td> <p> - <LinkButton OnClick="@(() => ManageUser(_auth))">Manage</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(_auth))">Remove</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(_auth, true))">Log out</LinkButton> + <LinkButton OnClickAsync="@(() => ManageUser(session.SessionId))">Manage</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId, true))">Log out</LinkButton> </p> </td> </tr> @@ -70,16 +81,16 @@ Small collection of tools to do not-so-everyday things. <td> <p> @{ - string[] parts = session.UserId.Split(':'); + string[] parts = session.Auth.UserId.Split(':'); } <span>@parts[0][1..]</span> on <span>@parts[1]</span> - @if (!string.IsNullOrWhiteSpace(session.Proxy)) { - <span class="badge badge-info"> (proxied via @session.Proxy)</span> + @if (!string.IsNullOrWhiteSpace(session.Auth.Proxy)) { + <span class="badge badge-info"> (proxied via @session.Auth.Proxy)</span> } </p> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -99,19 +110,19 @@ Small collection of tools to do not-so-everyday things. <td> <p> @{ - string[] parts = session.UserId.Split(':'); + string[] parts = session.Auth.UserId.Split(':'); } <span>@parts[0][1..]</span> on <span>@parts[1]</span> - @if (!string.IsNullOrWhiteSpace(session.Proxy)) { - <span class="badge badge-info"> (proxied via @session.Proxy)</span> + @if (!string.IsNullOrWhiteSpace(session.Auth.Proxy)) { + <span class="badge badge-info"> (proxied via @session.Auth.Proxy)</span> } </p> </td> <td> - <LinkButton OnClick="@(() => Task.Run(()=>NavigationManager.NavigateTo($"/InvalidSession?ctx={session.AccessToken}")))">Re-login</LinkButton> + <LinkButton OnClickAsync="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -127,67 +138,78 @@ Small collection of tools to do not-so-everyday things. private const bool _debug = false; #endif - private class AuthInfo { - public UserAuth? UserAuth { get; set; } + private bool Busy { get; set; } = true; + + private class HomepageSessionInfo : RmuSessionStore.SessionInfo { public UserInfo? UserInfo { get; set; } public ServerVersionResponse? ServerVersion { get; set; } public AuthenticatedHomeserverGeneric? Homeserver { get; set; } } - private readonly List<AuthInfo> _sessions = []; - private readonly List<UserAuth> _offlineSessions = []; - private readonly List<UserAuth> _invalidSessions = []; - private LoginResponse? _currentSession; - int scannedSessions = 0, totalSessions = 1; + private readonly List<HomepageSessionInfo> _sessions = []; + private readonly List<RmuSessionStore.SessionInfo> _offlineSessions = []; + private readonly List<RmuSessionStore.SessionInfo> _invalidSessions = []; + private RmuSessionStore.SessionInfo? _currentSession; + int scannedSessions, totalSessions = 1; private SvgIdenticonGenerator _identiconGenerator = new(); protected override async Task OnInitializedAsync() { Console.WriteLine("Index.OnInitializedAsync"); logger.LogDebug("Initialising index page"); - _currentSession = await RMUStorage.GetCurrentToken(); + + _currentSession = await sessionStore.GetCurrentSession(); _sessions.Clear(); _offlineSessions.Clear(); - var tokens = await RMUStorage.GetAllTokens(); + var sessions = await sessionStore.GetAllSessions(); scannedSessions = 0; - totalSessions = tokens.Count; + totalSessions = sessions.Count; logger.LogDebug("Found {0} tokens", totalSessions); - if (tokens is not { Count: > 0 }) { + if (sessions is not { Count: > 0 }) { Console.WriteLine("No tokens found, trying migration from MRU..."); - await RMUStorage.MigrateFromMRU(); - tokens = await RMUStorage.GetAllTokens(); - if (tokens is not { Count: > 0 }) { + sessions = await sessionStore.GetAllSessions(); + if (sessions is not { Count: > 0 }) { Console.WriteLine("No tokens found"); return; } } List<string> offlineServers = []; - var sema = new SemaphoreSlim(64, 64); + var sema = new SemaphoreSlim(8, 8); var updateSw = Stopwatch.StartNew(); - var tasks = tokens.Select(async token => { + var tasks = sessions.Select(async session => { await sema.WaitAsync(); - if ((!string.IsNullOrWhiteSpace(token.Proxy) && offlineServers.Contains(token.Proxy)) || offlineServers.Contains(token.Homeserver)) { - _offlineSessions.Add(token); - sema.Release(); - scannedSessions++; - return; - } + var token = session.Value.Auth; AuthenticatedHomeserverGeneric hs; try { - hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); + Task<ServerVersionResponse> serverVersionTask = Task.FromResult<ServerVersionResponse>(new() { + Server = new() { + Name = "Unknown", + Version = "0.0.0" + } + }); + try { + hs = await HsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); + serverVersionTask = hs.FederationClient?.GetServerVersionAsync() ?? serverVersionTask!; + } + catch (Exception e) { + logger.LogError("Failed to get info for {0} via {1}: {2}", token.UserId, token.Homeserver, e); + logger.LogError("Continuing with server-less session"); + hs = await HsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy, useGeneric: true, enableServer: false); + } + var joinedRoomsTask = hs.GetJoinedRooms(); var profileTask = hs.GetProfileAsync(hs.WhoAmI.UserId); - var serverVersionTask = hs.FederationClient?.GetServerVersionAsync(); _sessions.Add(new() { + Auth = token, + SessionId = session.Value.SessionId, + Homeserver = hs, UserInfo = new() { - AvatarUrl = string.IsNullOrWhiteSpace((await profileTask).AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(hs.WhoAmI.UserId) : hs.ResolveMediaUri((await profileTask).AvatarUrl), + AvatarUrl = (await profileTask).AvatarUrl, RoomCount = (await joinedRoomsTask).Count, DisplayName = (await profileTask).DisplayName ?? hs.WhoAmI.UserId }, - UserAuth = token, ServerVersion = await (serverVersionTask ?? Task.FromResult<ServerVersionResponse?>(null)!), - Homeserver = hs }); if (updateSw.ElapsedMilliseconds > 25) { updateSw.Restart(); @@ -197,7 +219,7 @@ Small collection of tools to do not-so-everyday things. catch (MatrixException e) { if (e is { ErrorCode: "M_UNKNOWN_TOKEN" }) { logger.LogWarning("Got unknown token error for {0} via {1}", token.UserId, token.Homeserver); - _invalidSessions.Add(token); + _invalidSessions.Add(session.Value); } else { logger.LogError("Failed to get info for {0} via {1}: {2}", token.UserId, token.Homeserver, e); @@ -222,19 +244,22 @@ Small collection of tools to do not-so-everyday things. await Task.WhenAll(tasks); scannedSessions = totalSessions; - await base.OnInitializedAsync(); + Busy = false; + StateHasChanged(); + Console.WriteLine("Index.OnInitializedAsync finished"); } private class UserInfo { - internal string AvatarUrl { get; set; } + internal string? AvatarUrl { get; set; } internal string DisplayName { get; set; } internal int RoomCount { get; set; } } - private async Task RemoveUser(UserAuth auth, bool logout = false) { + private async Task RemoveUser(string sessionId, bool logout = false) { try { if (logout) { - await (await hsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, auth.Proxy)).Logout(); + var auth = (await sessionStore.GetSession(sessionId))?.Auth; + await (await HsProvider.GetAuthenticatedWithToken(auth.Homeserver, auth.AccessToken, auth.Proxy)).Logout(); } } catch (Exception e) { @@ -246,21 +271,19 @@ Small collection of tools to do not-so-everyday things. Console.WriteLine(e); } - await RMUStorage.RemoveToken(auth); - if ((await RMUStorage.GetCurrentToken())?.AccessToken == auth.AccessToken) - await RMUStorage.SetCurrentToken((await RMUStorage.GetAllTokens() ?? throw new InvalidOperationException()).FirstOrDefault()); + await sessionStore.RemoveSession(sessionId); StateHasChanged(); } - private async Task SwitchSession(UserAuth auth) { - Console.WriteLine($"Switching to {auth.Homeserver} {auth.UserId} via {auth.Proxy}"); - await RMUStorage.SetCurrentToken(auth); - _currentSession = auth; + private async Task SwitchSession(string sessionId) { + Console.WriteLine($"Switching to {sessionId}"); + await sessionStore.SetCurrentSession(sessionId); + _currentSession = await sessionStore.GetCurrentSession(); StateHasChanged(); } - private async Task ManageUser(UserAuth auth) { - await SwitchSession(auth); + private async Task ManageUser(string sessionId) { + await sessionStore.SetCurrentSession(sessionId); NavigationManager.NavigateTo("/User/Profile"); } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/InvalidSession.razor b/MatrixUtils.Web/Pages/InvalidSession.razor
index e1a72ea..f86d112 100644 --- a/MatrixUtils.Web/Pages/InvalidSession.razor +++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -6,15 +6,16 @@ <h3>Rory&::MatrixUtils - Invalid session encountered</h3> <p>A session was encountered that is no longer valid. This can happen if you have logged out of the account on another device, or if the access token has expired.</p> -@if (_login is not null) { - <p>It appears that the affected user is @_login.UserId (@_login.DeviceId) on @_login.Homeserver!</p> - <LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton> - <LinkButton OnClick="@(RemoveUser)">Remove</LinkButton> +@if (_auth is not null) { + <p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p> + <LinkButton OnClickAsync="@(OpenRefreshDialog)">Refresh token</LinkButton> + <LinkButton OnClickAsync="@(RemoveUser)">Remove</LinkButton> @if (_showRefreshDialog) { - <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_login.UserId}")"> - <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox><br/> - <LinkButton OnClick="TryLogin">Log in</LinkButton> + <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")"> + <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox> + <br/> + <LinkButton OnClickAsync="TryLogin">Log in</LinkButton> @if (_loginException is not null) { <pre style="color: red;">@_loginException.RawContent</pre> } @@ -29,9 +30,9 @@ else { { [Parameter] [SupplyParameterFromQuery(Name = "ctx")] - public string Context { get; set; } + public string SessionId { get; set; } - private UserAuth? _login { get; set; } + private UserAuth? _auth { get; set; } private bool _showRefreshDialog { get; set; } @@ -40,25 +41,21 @@ else { private MatrixException? _loginException { get; set; } protected override async Task OnInitializedAsync() { - var tokens = await RMUStorage.GetAllTokens(); - if (tokens is null || tokens.Count == 0) { + var tokens = await sessionStore.GetAllSessions(); + if (tokens.Count == 0) { NavigationManager.NavigateTo("/Login"); return; } - _login = tokens.FirstOrDefault(x => x.AccessToken == Context); - - if (_login is null) { - Console.WriteLine($"Could not find {_login} in stored tokens!"); - } + if (tokens.TryGetValue(SessionId, out var session)) + _auth = session.Auth; + else Console.WriteLine($"Could not find {SessionId} in stored sessions!"); await base.OnInitializedAsync(); } private async Task RemoveUser() { - await RMUStorage.RemoveToken(_login!); - if ((await RMUStorage.GetCurrentToken())!.AccessToken == _login!.AccessToken) - await RMUStorage.SetCurrentToken((await RMUStorage.GetAllTokens())?.FirstOrDefault()); + await sessionStore.RemoveSession(SessionId); await OnInitializedAsync(); } @@ -68,30 +65,29 @@ else { await Task.CompletedTask; } - private async Task SwitchSession(UserAuth auth) { - Console.WriteLine($"Switching to {auth.Homeserver} {auth.AccessToken} {auth.UserId}"); - await RMUStorage.SetCurrentToken(auth); + private async Task SwitchSession(string sessionId) { + Console.WriteLine($"Switching to session {sessionId}"); + await sessionStore.SetCurrentSession(sessionId); await OnInitializedAsync(); } private async Task TryLogin() { - if(_login is null) throw new NullReferenceException("Login is null!"); + if (_auth is null) throw new NullReferenceException("Login is null!"); try { - var result = new UserAuth(await hsProvider.Login(_login.Homeserver, _login.UserId, _password)); + var result = new UserAuth(await HsProvider.Login(_auth.Homeserver, _auth.UserId, _password)); if (result is null) { - Console.WriteLine($"Failed to login to {_login.Homeserver} as {_login.UserId}!"); + Console.WriteLine($"Failed to login to {_auth.Homeserver} as {_auth.UserId}!"); return; } + Console.WriteLine($"Obtained access token for {result.UserId}!"); - await RemoveUser(); - await RMUStorage.AddToken(result); - if (result.UserId == (await RMUStorage.GetCurrentToken())?.UserId) - await RMUStorage.SetCurrentToken(result); + await sessionStore.RemoveSession(SessionId); + await sessionStore.AddSession(result); NavigationManager.NavigateTo("/"); } catch (MatrixException e) { - Console.WriteLine($"Failed to login to {_login.Homeserver} as {_login.UserId}!"); + Console.WriteLine($"Failed to login to {_auth.Homeserver} as {_auth.UserId}!"); Console.WriteLine(e); _loginException = e; StateHasChanged(); diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
index b370080..56c8cfe 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
@@ -1,7 +1,7 @@ @using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext @* user header and room list *@ @foreach (var room in Data.SyncWrapper.Rooms) { - <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")"> + <LinkButton OnClickAsync="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")"> @room.RoomName </LinkButton> <br/> @@ -10,6 +10,6 @@ @code { [Parameter] - public ClientContext Data { get; set; } = null!; + public ClientContext Data { get; set; } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
index c680c13..60f850d 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientStatusList.razor
@@ -10,7 +10,7 @@ @code { [Parameter] - public ObservableCollection<ClientContext> Data { get; set; } = null!; + public ObservableCollection<ClientContext> Data { get; set; } protected override void OnInitialized() { Data.CollectionChanged += (_, e) => { diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
index 67dcae5..6a930b1 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/MatrixClient.razor
@@ -25,6 +25,6 @@ @code { [Parameter] - public Index.ClientContext Data { get; set; } = null!; + public Index.ClientContext Data { get; set; } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/Client/Index.razor b/MatrixUtils.Web/Pages/Labs/Client/Index.razor
index ef4a0b9..c6e7d1a 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/Index.razor +++ b/MatrixUtils.Web/Pages/Labs/Client/Index.razor
@@ -40,11 +40,11 @@ } protected override async Task OnInitializedAsync() { - var tokens = await RMUStorage.GetAllTokens(); - var tasks = tokens.Select(async token => { + var tokens = await sessionStore.GetAllSessions(); + var tasks = tokens.Keys.Select(async token => { try { var cc = new ClientContext() { - Homeserver = await RMUStorage.GetSession(token) + Homeserver = await sessionStore.GetHomeserver(token) }; cc.SyncWrapper = new ClientSyncWrapper(cc.Homeserver); diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor
index c0dc8a6..f81afe5 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpace.razor
@@ -52,7 +52,7 @@ NavigationManager.NavigateTo(NavigationManager.Uri.Replace("stage=", ""), true); //"/User/DMSpace/Setup" } DMSpaceRootPage = this; - SetupData.Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); + SetupData.Homeserver ??= await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (SetupData.Homeserver is null) return; try { SetupData.DmSpaceConfiguration = await SetupData.Homeserver.GetAccountDataAsync<DMSpaceConfiguration>("gay.rory.dm_space"); diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index 55e17d6..7199934 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -4,7 +4,8 @@ @using MatrixUtils.LibDMSpace @using MatrixUtils.LibDMSpace.StateEvents @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.EventTypes.Spec.State.Space @using MatrixUtils.Abstractions <b> <u>DM Space setup tool - stage 1: Configure space</u> @@ -25,10 +26,10 @@ <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> Create sub-spaces per user </p> - + <br/> - <LinkButton OnClick="@Disband" Color="#FF0000">Disband</LinkButton> - <LinkButton OnClick="@Execute">Next</LinkButton> + <LinkButton OnClickAsync="@Disband" Color="#FF0000">Disband</LinkButton> + <LinkButton OnClickAsync="@Execute">Next</LinkButton> } else { <p>Discovering spaces, please wait...</p> @@ -77,7 +78,7 @@ else { userRooms.Add(room); } - var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable(); + var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncResultEnumerable(); await foreach (var room in roomChecks) if (room.HasValue) spaces.TryAdd(room.Value.id, room.Value.roomInfo); @@ -108,8 +109,8 @@ else { public async Task<(string id, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) { try { var ri = new RoomInfo(room); - - await foreach(var evt in room.GetFullStateAsync()) + + await foreach (var evt in room.GetFullStateAsync()) ri.StateEvents.Add(evt); var powerLevels = (await ri.GetStateEvent(RoomPowerLevelEventContent.EventId)).TypedContent as RoomPowerLevelEventContent; @@ -117,7 +118,7 @@ else { Console.WriteLine($"No permission to send m.space.child in {room.RoomId}..."); return null; } - + Status = $"Found viable space: {ri.RoomName}"; if (!string.IsNullOrWhiteSpace(SetupData.DmSpaceConfiguration!.DMSpaceId)) { if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is { } dsi) { diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
index be6027a..ed65e94 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -1,6 +1,6 @@ @using LibMatrix.RoomTypes -@using LibMatrix.EventTypes.Spec.State @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using MatrixUtils.Abstractions <b> <u>DM Space setup tool - stage 2: Fix DM room attribution</u> @@ -31,17 +31,19 @@ else { } <br/> -<LinkButton OnClick="@Execute">Next</LinkButton> +<LinkButton OnClickAsync="@Execute">Next</LinkButton> @{ var _offset = 0; } @foreach (var (room, usersList) in duplicateDmRooms) { <ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset"> - <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p> + <p>Found room assigned to multiple users: + <RoomListItem RoomInfo="@room"></RoomListItem> + </p> <p>Users:</p> @foreach (var userProfileResponse in usersList) { - <LinkButton OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> + <LinkButton OnClickAsync="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> <span>Assign to </span> <InlineUserItem User="userProfileResponse"></InlineUserItem> </LinkButton> @@ -54,7 +56,7 @@ else { <ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)"> <RoomListItem RoomInfo="@DmToReassign"></RoomListItem> @foreach (var userProfileResponse in roomMembers[DmToReassign]) { - <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> + <LinkButton OnClickAsync="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> <span>Assign to </span> <InlineUserItem User="userProfileResponse"></InlineUserItem> </LinkButton> @@ -141,12 +143,12 @@ else { } var roomList = new List<RoomInfo>(); - var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); + var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable(); await foreach (var result in tasks) roomList.Add(result); return (userProfile, roomList); // StateHasChanged(); - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var res in results) { SetupData.DMRooms.Add(res.userProfile, res.roomList); // Status = $"Listed {dmRooms.Count} users"; @@ -181,18 +183,18 @@ else { await roomInfo.FetchAllStateAsync(); roomMembers[roomInfo] = new(); // roomInfo.CreationEventContent = await room.GetCreateEventAsync(); - - if(roomInfo.RoomName == room.RoomId) + + if (roomInfo.RoomName == room.RoomId) try { roomInfo.RoomName = await room.GetNameOrFallbackAsync(); } catch { } - var membersEnum = room.GetMembersEnumerableAsync(true); + var membersEnum = room.GetMembersEnumerableAsync("join"); await foreach (var member in membersEnum) if (member.TypedContent is RoomMemberEventContent memberEvent) roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); - + try { string? roomIcon = (await room.GetAvatarUrlAsync())?.Url; if (room is not null) diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
index 09de5d3..686894c 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -1,8 +1,8 @@ @using LibMatrix.RoomTypes -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.Responses @using MatrixUtils.LibDMSpace @using System.Text.Json.Serialization +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using MatrixUtils.Abstractions <b> @@ -59,7 +59,7 @@ else { } <br/> -<LinkButton OnClick="@Execute">Next</LinkButton> +<LinkButton OnClickAsync="@Execute">Next</LinkButton> @code { @@ -115,11 +115,11 @@ else { // }; // } // var roomList = new List<RoomInfo>(); - // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable(); + // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable(); // await foreach (var result in tasks) // roomList.Add(result); // return (userProfile, roomList); - // }).ToAsyncEnumerable(); + // }).ToAsyncResultEnumerable(); // await foreach (var res in results) { // dmRooms.Add(new RoomInfo() { // Room = dmSpaceRoom, @@ -150,7 +150,7 @@ else { } catch { } - var membersEnum = room.GetMembersEnumerableAsync(true); + var membersEnum = room.GetMembersEnumerableAsync("join"); await foreach (var member in membersEnum) if (member.TypedContent is RoomMemberEventContent memberEvent) roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor
index 3392960..441752b 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2.razor
@@ -55,7 +55,7 @@ public RoomListViewData Data { get; set; } = new RoomListViewData(); protected override async Task OnInitializedAsync() { - Data.Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Data.Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Data.Homeserver is null) return; var rooms = await Data.Homeserver.GetJoinedRooms(); Data.GlobalProfile = await Data.Homeserver.GetProfileAsync(Data.Homeserver.WhoAmI.UserId); diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor
index 6483f01..ba994d1 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/MainTabComponents/MainTabSpaceItem.razor
@@ -1,14 +1,14 @@ @using MatrixUtils.Abstractions -<div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" onclick="@SelectSpace"> +<div class="spaceListItem" style="@(SelectedSpace == Space ? "background-color: #FFFFFF33;" : "")" @onclick="@SelectSpace"> <div class="spaceListItemContainer"> @if (IsSpaceOpened()) { - <span onclick="@ToggleSpace">▼ </span> + <span @onclick="@ToggleSpace">▼ </span> } else { - <span onclick="@ToggleSpace">▶ </span> + <span @onclick="@ToggleSpace">▶ </span> } - <MxcImage Circular="true" Height="32" Width="32" Homeserver="Space.Room.Homeserver" MxcUri="@Space.RoomIcon"></MxcImage> + <MxcImage Homeserver="@Homeserver" Circular="true" Height="32" Width="32" Uri="@Space.RoomIcon"></MxcImage> <span class="spaceNameEllipsis">@Space.RoomName</span> </div> @if (IsSpaceOpened()) { @@ -30,6 +30,9 @@ [Parameter] public List<RoomInfo> OpenedSpaces { get; set; } + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + protected override Task OnInitializedAsync() { Space.PropertyChanged += (sender, args) => { StateHasChanged(); }; return base.OnInitializedAsync(); diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index f4cf849..dd217e9 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -22,7 +22,7 @@ @code { [CascadingParameter] - public Index2.RoomListViewData Data { get; set; } = null!; + public Index2.RoomListViewData Data { get; set; } protected override async Task OnInitializedAsync() { Data.Rooms.CollectionChanged += (sender, args) => { @@ -36,7 +36,6 @@ } //debounce StateHasChanged, we dont want to reredner on every key stroke - private CancellationTokenSource _debounceCts = new CancellationTokenSource(); private async Task DebouncedStateHasChanged() { diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor
index f4cf849..79f931b 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2DMsTab.razor
@@ -22,7 +22,7 @@ @code { [CascadingParameter] - public Index2.RoomListViewData Data { get; set; } = null!; + public Index2.RoomListViewData Data { get; set; } protected override async Task OnInitializedAsync() { Data.Rooms.CollectionChanged += (sender, args) => { diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor
index 7ccfae2..99b031a 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2MainTab.razor
@@ -1,6 +1,6 @@ @using MatrixUtils.Abstractions @using System.ComponentModel -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.Space @using MatrixUtils.Web.Pages.Labs.Rooms2.Index2Components.MainTabComponents <h3>RoomsIndex2MainTab</h3> @@ -22,31 +22,32 @@ @* </div> *@ @* </div> *@ -<div> - <div class="row"> - <div class="col-3" style="background-color: #ffffff22;"> - <LinkButton>Uncategorised rooms</LinkButton> - @foreach (var space in GetTopLevelSpaces()) { - @* @RecursingSpaceChildren(space) *@ - <MainTabSpaceItem Space="space" OpenedSpaces="OpenedSpaces" @bind-SelectedSpace="SelectedSpace" /> - } - </div> - <div class="col-9" style="background-color: #ff00ff66;"> - <p>Placeholder for rooms list...</p> - @if (SelectedSpace != null) { - foreach (var room in GetSpaceChildRooms(SelectedSpace)) { - <p>@room.RoomName</p> +<CascadingValue Name="Homeserver" Value="@Data.Homeserver"> + <div> + <div class="row"> + <div class="col-3" style="background-color: #ffffff22;"> + <LinkButton>Uncategorised rooms</LinkButton> + @foreach (var space in GetTopLevelSpaces()) { + @* @RecursingSpaceChildren(space) *@ + <MainTabSpaceItem Space="space" OpenedSpaces="OpenedSpaces" @bind-SelectedSpace="SelectedSpace"/> + } + </div> + <div class="col-9" style="background-color: #ff00ff66;"> + <p>Placeholder for rooms list...</p> + @if (SelectedSpace != null) { + foreach (var room in GetSpaceChildRooms(SelectedSpace)) { + <p>@room.RoomName</p> + } } - } + </div> </div> </div> -</div> - +</CascadingValue> @code { [CascadingParameter] - public Index2.RoomListViewData Data { get; set; } = null!; + public Index2.RoomListViewData Data { get; set; } protected override async Task OnInitializedAsync() { Data.Rooms.CollectionChanged += (sender, args) => { @@ -118,7 +119,7 @@ var childSpaces = children.Where(x => x.RoomType == "m.space").ToList(); return childSpaces; } - + private List<RoomInfo> GetSpaceChildRooms(RoomInfo space) { var children = GetSpaceChildren(space); var childRooms = children.Where(x => x.RoomType != "m.space").ToList(); diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor
index 91f228d..33c310a 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2SyncContainer.razor
@@ -2,11 +2,11 @@ @using LibMatrix.Responses @using MatrixUtils.Abstractions @using System.Diagnostics -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.Extensions @using LibMatrix.Utilities @using System.Collections.ObjectModel @using ArcaneLibs +@using LibMatrix.EventTypes.Spec.State.Space @inject ILogger<RoomsIndex2SyncContainer> logger <pre>RoomsIndex2SyncContainer</pre> @foreach (var (name, value) in _statusList) { @@ -16,7 +16,7 @@ @code { [Parameter] - public Index2.RoomListViewData Data { get; set; } = null!; + public Index2.RoomListViewData Data { get; set; } private SyncHelper syncHelper; @@ -113,7 +113,7 @@ statusd.Status = $"{roomId} already known with {room.StateEvents?.Count ?? 0} state events"; } else { - statusd.Status = $"Eencountered new room {roomId}!"; + statusd.Status = $"Encountered new room {roomId}!"; room = new RoomInfo(Data.Homeserver!.GetRoom(roomId), roomData.State?.Events); Data.Rooms.Add(room); } diff --git a/MatrixUtils.Web/Pages/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 6c869ac..38ede74 100644 --- a/MatrixUtils.Web/Pages/LoginPage.razor +++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -22,8 +22,29 @@ <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox> </span> <br/> -<LinkButton OnClick="@AddRecord">Add account to queue</LinkButton> -<LinkButton OnClick="@(() => Login(newRecordInput))">Log in</LinkButton> +<LinkButton OnClickAsync="@AddRecord">Add account to queue</LinkButton> +<LinkButton OnClickAsync="@(() => Login(newRecordInput))">Log in</LinkButton> +<br/> +<br/> + + +<h4>Add with access token</h4> +<hr/> + +<span style="display: block;"> + <label>Homeserver:</label> + <FancyTextBox @bind-Value="@newRecordInput.Homeserver"></FancyTextBox> +</span> +<span style="display: block;"> + <label>Access token:</label> + <FancyTextBox @bind-Value="@newRecordInput.Password" IsPassword="true"></FancyTextBox> +</span> +<span style="display: block"> + <label>Proxy (<a href="https://cgit.rory.gay/matrix/MxApiExtensions.git">MxApiExtensions</a> or similar):</label> + <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox> +</span> +<br/> +<LinkButton OnClickAsync="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton> <br/> <br/> @@ -47,7 +68,7 @@ </thead> @foreach (var record in records) { var r = record; - <tr style="background-color: @(LoggedInSessions.Any(x => x.UserId == $"@{r.Username}:{r.Homeserver}" && x.Proxy == r.Proxy) ? "green" : "unset")"> + <tr style="background-color: @(LoggedInSessions.Any(x => x.Value.Auth.UserId == $"@{r.Username}:{r.Homeserver}" && x.Value.Auth.Proxy == r.Proxy) ? "green" : "unset")"> <td style="border-width: 1px;"> <FancyTextBox @bind-Value="@r.Username"></FancyTextBox> </td> @@ -80,14 +101,14 @@ } </table> <br/> -<LinkButton OnClick="@LoginAll">Log in</LinkButton> +<LinkButton OnClickAsync="@LoginAll">Log in</LinkButton> @code { readonly List<LoginStruct> records = new(); private LoginStruct newRecordInput = new(); - List<UserAuth>? LoggedInSessions { get; set; } = new(); + Dictionary<string, RmuSessionStore.SessionInfo> LoggedInSessions { get; set; } = new(); async Task LoginAll() { var loginTasks = records.Select(Login); @@ -97,10 +118,10 @@ async Task Login(LoginStruct record) { if (!records.Contains(record)) records.Add(record); - if (LoggedInSessions.Any(x => x.UserId == $"@{record.Username}:{record.Homeserver}" && x.Proxy == record.Proxy)) return; + if (LoggedInSessions.Any(x => x.Value.Auth.UserId == $"@{record.Username}:{record.Homeserver}" && x.Value.Auth.UserId == record.Proxy)) return; StateHasChanged(); try { - var result = new UserAuth(await hsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) { + var result = new UserAuth(await HsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) { Proxy = record.Proxy }; if (result == null) { @@ -110,8 +131,8 @@ Console.WriteLine($"Obtained access token for {result.UserId}!"); - await RMUStorage.AddToken(result); - LoggedInSessions = await RMUStorage.GetAllTokens(); + await sessionStore.AddSession(result); + LoggedInSessions = await sessionStore.GetAllSessions(); } catch (Exception e) { Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!"); @@ -123,7 +144,7 @@ } private async Task FileChanged(InputFileChangeEventArgs obj) { - LoggedInSessions = await RMUStorage.GetAllTokens(); + LoggedInSessions = await sessionStore.GetAllSessions(); Console.WriteLine(JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true })); @@ -141,7 +162,7 @@ } private async Task AddRecord() { - LoggedInSessions = await RMUStorage.GetAllTokens(); + LoggedInSessions = await sessionStore.GetAllSessions(); records.Add(newRecordInput); newRecordInput = new(); } @@ -156,4 +177,27 @@ internal Exception? Exception { get; set; } } + private async Task AddWithAccessToken(LoginStruct record) { + try { + var session = await HsProvider.GetAuthenticatedWithToken(record.Homeserver, record.Password, record.Proxy); + if (session == null) { + Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!"); + return; + } + + await sessionStore.AddSession(new UserAuth() { + UserId = session.WhoAmI.UserId, + AccessToken = session.AccessToken, + Proxy = record.Proxy, + DeviceId = session.WhoAmI.DeviceId + }); + LoggedInSessions = await sessionStore.GetAllSessions(); + } + catch (Exception e) { + Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!"); + Console.WriteLine(e); + record.Exception = e; + } + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
index 9218c8c..17dd554 100644 --- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor +++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -1,7 +1,7 @@ @page "/Moderation/UserRoomHistory/{UserId}" -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.RoomTypes @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using MatrixUtils.Abstractions <h3>UserRoomHistory</h3> @@ -19,6 +19,7 @@ else { else if (checkedRooms.Count > 1) { <p>Done!</p> } + @foreach (var (state, rooms) in matchingStates) { <u>@state</u> <br/> @@ -44,11 +45,11 @@ else { private AuthenticatedHomeserverGeneric? currentHs { get; set; } protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - var sessions = await RMUStorage.GetAllTokens(); - foreach (var userAuth in sessions) { - var session = await RMUStorage.GetSession(userAuth); + var sessions = await sessionStore.GetAllSessions(); + foreach (var userAuth in sessions.Keys) { + var session = await sessionStore.GetHomeserver(userAuth); if (session is not null) { hss.Add(session); StateHasChanged(); @@ -71,13 +72,14 @@ else { _semaphoreSlim.Release(); return; //abort if changed } + matchingStates.Clear(); foreach (var homeserver in hss) { currentHs = homeserver; var rooms = await homeserver.GetJoinedRooms(); rooms.RemoveAll(x => checkedRooms.Contains(x.RoomId)); checkedRooms.AddRange(rooms.Select(x => x.RoomId)); - var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncEnumerable(); + var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncResultEnumerable(); await foreach (var (room, state) in tasks) { if (state is null) continue; if (!matchingStates.ContainsKey(state.Membership)) @@ -97,8 +99,10 @@ else { return; //abort if changed } } + StateHasChanged(); } + currentHs = null; StateHasChanged(); _semaphoreSlim.Release(); diff --git a/MatrixUtils.Web/Pages/Rooms/Create.razor b/MatrixUtils.Web/Pages/Rooms/Create.razor
index f2dfb01..021ad18 100644 --- a/MatrixUtils.Web/Pages/Rooms/Create.razor +++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -3,11 +3,9 @@ @using System.Reflection @using ArcaneLibs.Extensions @using LibMatrix -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses @using MatrixUtils.Web.Classes.RoomCreationTemplates -@using Microsoft.AspNetCore.Components.Forms @* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@ <h3>Room Manager - Create Room</h3> @@ -89,7 +87,7 @@ <tr> <td>Room icon:</td> <td> - <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> + @* <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> *@ <div style="display: inline-block; vertical-align: middle;"> <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/> <InputFile OnChange="RoomIconFilePicked"></InputFile> @@ -134,7 +132,7 @@ } else { <details> - <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Allow.Count) allow rules</summary> + <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Allow.Count) allow rules</summary> @* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@ </details> } @@ -144,7 +142,7 @@ } else { <details> - <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerACLEventContent).Deny.Count) deny rules</summary> + <summary>@((creationEvent["m.room.server_acls"].TypedContent as RoomServerAclEventContent).Deny.Count) deny rules</summary> @* <StringListEditor @bind-Items="@serverAcl.Allow"></StringListEditor> *@ </details> } @@ -256,11 +254,11 @@ private RoomHistoryVisibilityEventContent? historyVisibility => creationEvent?["m.room.history_visibility"].TypedContent as RoomHistoryVisibilityEventContent; private RoomGuestAccessEventContent? guestAccessEvent => creationEvent?["m.room.guest_access"].TypedContent as RoomGuestAccessEventContent; - private RoomServerACLEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerACLEventContent; + private RoomServerAclEventContent? serverAcl => creationEvent?["m.room.server_acls"].TypedContent as RoomServerAclEventContent; private RoomAvatarEventContent? roomAvatarEvent => creationEvent?["m.room.avatar"].TypedContent as RoomAvatarEventContent; protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { diff --git a/MatrixUtils.Web/Pages/Rooms/Create2.razor b/MatrixUtils.Web/Pages/Rooms/Create2.razor new file mode 100644
index 0000000..4a29847 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Create2.razor
@@ -0,0 +1,147 @@ +@page "/Rooms/Create2" +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Pages.Rooms.RoomCreateComponents +@inject ILogger<Create2> logger +@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@ + +<h3>Room Manager - Create Room</h3> + +@if (Ready) { + <style> + table.table-top-first-tr tr td:first-child { + vertical-align: top; + } + </style> + <table class="table-top-first-tr"> + @if (roomBuilder is RoomUpgradeBuilder roomUpgrade) { + <RoomCreateUpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@StateHasChanged" OldRoom="@PreviousRoom" /> + } + else { + @* <tr style="padding-bottom: 16px;"> *@ + @* <td>Preset:</td> *@ + @* <td> *@ + @* @if (Presets is null) { *@ + @* <p style="color: red;">Presets is null!</p> *@ + @* } *@ + @* else { *@ + @* <p style="color: red;">Support for presets is currently disabled!</p> *@ + @* $1$ <InputSelect @bind-Value="@RoomPreset"> #1# *@ + @* $1$ @foreach (var createRoomRequest in Presets) { #1# *@ + @* $1$ <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> #1# *@ + @* $1$ } #1# *@ + @* $1$ </InputSelect> #1# *@ + @* } *@ + @* </td> *@ + @* </tr> *@ + } + <RoomCreateBasicRoomInfoOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreateCreateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreatePrivacyOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreatePermissionsOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreateMembershipOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + @* Initial states, should remain at bottom *@ + <RoomCreateInitialStateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + </table> + <LinkButton OnClickAsync="@CreateRoom">Create room</LinkButton> +} + +<RoomCreateStateDisplay @bind-RoomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged"/> + +@if (_matrixException is not null) { + <ModalWindow Title="@("Matrix exception: " + _matrixException.ErrorCode)"> + <pre> + @_matrixException.Message + </pre> + </ModalWindow> +} + +@code { + +#region State + + [Parameter, SupplyParameterFromQuery(Name = "previousRoomId")] + public string? PreviousRoomId { get; set; } + + public GenericRoom? PreviousRoom { get; set; } + + private bool Ready { get; set; } + + private RoomBuilder roomBuilder { get; set; } = new(); + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private MatrixException? _matrixException { get; set; } + +#endregion + +#region Presets + + private Dictionary<string, CreateRoomRequest>? Presets { get; set; } = new(); + // private string RoomPreset { + // get => Presets.ContainsValue(roomBuilder) ? Presets.First(x => x.Value == roomBuilder).Key : "Not a preset"; + // set { + // roomBuilder = Presets[value]; + // JsonChanged(); + // StateHasChanged(); + // } + // } + +#endregion + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; + if (!string.IsNullOrWhiteSpace(PreviousRoomId)) { + roomBuilder = new RoomUpgradeBuilder(); + PreviousRoom = Homeserver.GetRoom(PreviousRoomId); + } + + roomBuilder.ServerAcls.Allow = ["*"]; + roomBuilder.ServerAcls.Deny = []; + + // foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { + // Console.WriteLine($"Found room creation template in class: {x.FullName}"); + // var instance = (IRoomCreationTemplate)Activator.CreateInstance(x); + // Presets[instance.Name] = instance.CreateRoomRequest; + // } + // + // Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); + + // if (!Presets.ContainsKey("Default")) { + // Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}"); + // } + // else RoomPreset = "Default"; + + Ready = true; + StateHasChanged(); + if (roomBuilder is RoomUpgradeBuilder roomUpgrade) { + // await roomUpgrade.ImportAsync().ConfigureAwait(false); + StateHasChanged(); + } + } + + protected override bool ShouldRender() { + if (roomBuilder.Type == "") + roomBuilder.Type = null; // Reset to null if empty, so it doesn't get sent as an empty string + var result = base.ShouldRender(); + logger.LogInformation("ShouldRender: " + result); + return result; + } + + private async Task CreateRoom() { + Console.WriteLine("Create room"); + Console.WriteLine(roomBuilder.ToJson()); + roomBuilder.AdditionalCreationContent["gay.rory.created_using"] = "Rory&::MatrixUtils (https://mru.rory.gay)"; + try { + var newRoom = await roomBuilder.Create(Homeserver); + } + catch (MatrixException e) { + _matrixException = e; + } + } + +} diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor
index 28c4de2..115c903 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -13,9 +13,7 @@ <p>@Status2</p> <LinkButton href="/Rooms/Create">Create new room</LinkButton> -<CascadingValue TValue="AuthenticatedHomeserverGeneric" Value="Homeserver"> - <RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents"></RoomList> -</CascadingValue> +<RoomList Rooms="Rooms" GlobalProfile="@GlobalProfile" @bind-StillFetching="RenderContents" Homeserver="@Homeserver"></RoomList> @code { @@ -68,14 +66,14 @@ // SyncHelper profileSyncHelper; protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; // var rooms = await Homeserver.GetJoinedRooms(); // SemaphoreSlim _semaphore = new(160, 160); GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); var filter = await Homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetBasicRoomInfo); - var filterData = await Homeserver.GetFilterAsync(filter); + // var filterData = await Homeserver.GetFilterAsync(filter); // Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room))); // foreach (var stateType in filterData.Room?.State?.Types ?? []) { @@ -99,7 +97,8 @@ syncHelper = new SyncHelper(Homeserver, logger) { Timeout = 30000, FilterId = filter, - MinimumDelay = TimeSpan.FromMilliseconds(5000) + MinimumDelay = TimeSpan.FromMilliseconds(5000), + UseMsc4222StateAfter = true }; // profileSyncHelper = new SyncHelper(Homeserver, logger) { // Timeout = 10000, @@ -108,9 +107,9 @@ // }; // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); - RunSyncLoop(syncHelper); + _ = RunSyncLoop(syncHelper); // RunSyncLoop(profileSyncHelper); - RunQueueProcessor(); + _ = RunQueueProcessor(); await base.OnInitializedAsync(); } @@ -122,7 +121,7 @@ try { while (queue.Count == 0) { Console.WriteLine("Queue is empty, waiting..."); - await Task.Delay(isInitialSync ? 100 : 2500); + await Task.Delay(isInitialSync ? 1000 : 2500); } Console.WriteLine($"Queue no longer empty after {renderTimeSw.Elapsed}!"); @@ -131,16 +130,16 @@ isInitialSync = false; while (maxUpdates-- > 0 && queue.TryDequeue(out var queueEntry)) { var (roomId, roomData) = queueEntry; - Console.WriteLine($"Dequeued room {roomId}"); + // Console.WriteLine($"Dequeued room {roomId}"); RoomInfo room; if (Rooms.Any(x => x.Room.RoomId == roomId)) { room = Rooms.First(x => x.Room.RoomId == roomId); - Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events"); + // Console.WriteLine($"QueueWorker: {roomId} already known with {room.StateEvents?.Count ?? 0} state events"); } else { - Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); - room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events); + // Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); + room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events); Rooms.Add(room); } @@ -149,12 +148,16 @@ throw new InvalidDataException("Somehow this is null???"); } - if (roomData.State?.Events is { Count: > 0 }) - room.StateEvents.MergeStateEventLists(roomData.State.Events); - else { + if (roomData is { StateAfter.Events.Count: > 0 }) + room.StateEvents!.MergeStateEventLists(roomData.StateAfter.Events); + else Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); - } + if (maxUpdates % 100 == 0) { + Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}"); + StateHasChanged(); + await Task.Yield(); + } // await Task.Delay(100); } @@ -170,7 +173,7 @@ } } - private bool RenderContents { get; set; } = false; + private bool RenderContents { get; set; } private string _status; @@ -178,7 +181,8 @@ get => _status; set { _status = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -188,7 +192,8 @@ get => _status2; set { _status2 = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -200,34 +205,34 @@ var syncs = syncHelper.EnumerateSyncAsync(); await foreach (var sync in syncs) { - Console.WriteLine("trying sync"); - if (sync is null) continue; - var filter = await Homeserver.GetFilterAsync(syncHelper.FilterId); Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!"; - if (sync?.Rooms?.Join != null) + if (sync.Rooms?.Join != null) foreach (var joinedRoom in sync.Rooms.Join) - if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) { - joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId); + if (joinedRoom.Value.StateAfter?.Events?.Count > 0) { + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI.UserId); // We can't trust servers to give us what we ask for, and this ruins performance // Thanks, Conduit. - joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false); - if (filter.Room?.State?.NotSenders?.Any() ?? false) - joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false); + if (filter is { Room.State.Types.Count: > 0 }) + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false); + if (filter is { Room.State.NotSenders.Count: > 0 }) + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender!) ?? false); queue.Enqueue(joinedRoom); } - if (sync.Rooms.Leave is { Count: > 0 }) + if (sync.Rooms?.Leave is { Count: > 0 }) foreach (var leftRoom in sync.Rooms.Leave) if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key)); Status = $"Got {Rooms.Count} rooms so far! {queue.Count} entries in processing queue... " + - $"{sync?.Rooms?.Join?.Count ?? 0} new updates!"; + $"{sync.Rooms?.Join?.Count ?? 0} new updates!"; - Status2 = $"Next batch: {sync.NextBatch}"; + Status2 = $"Next batch: {sync?.NextBatch}"; + StateHasChanged(); + await Task.Yield(); } } diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index b7ebae2..92c6ca5 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -1,124 +1,152 @@ @page "/Rooms/{RoomId}/Policies" @using LibMatrix @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.EventTypes.Spec.State.Policy @using System.Diagnostics @using LibMatrix.RoomTypes @using System.Collections.Frozen +@using System.Collections.Immutable @using System.Reflection +@using System.Text.Json @using ArcaneLibs.Attributes +@using ArcaneLibs.Blazor.Components.Services @using LibMatrix.EventTypes +@using LibMatrix.EventTypes.Interop.Draupnir +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using SpawnDev.BlazorJS.WebWorkers +@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents +@using SpawnDev.BlazorJS +@inject WebWorkerService WebWorkerService +@inject ILogger<PolicyList> logger +@inject BlazorJSRuntime JsRuntime -@using MatrixUtils.Web.Shared.PolicyEditorComponents - -<h3>Policy list editor - Editing @RoomId</h3> -<hr/> -@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ -<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> - -@if (Loading) { - <p>Loading...</p> -} -else if (PolicyEventsByType is not { Count: > 0 }) { - <p>No policies yet</p> +@if (!IsInitialised) { + <p>Connecting to homeserver...</p> } else { - @foreach (var (type, value) in PolicyEventsByType) { - <p> - @(GetValidPolicyEventsByType(type).Count) active, - @(GetInvalidPolicyEventsByType(type).Count) invalid - (@value.Count total) - @(GetPolicyTypeName(type).ToLower()) - </p> + <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader> + @if (Loading) { + <p>Loading...</p> } + // else if (PolicyEventsByType is not { Count: > 0 }) { + @* <p>No policies yet</p> *@ + // } + else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); + @foreach (var value in PolicyCollections.Values.OrderByDescending(x => x.TotalCount)) { + <p> + @value.ActivePolicies.Count active, + @value.RemovedPolicies.Count removed + (@value.TotalCount total) + @value.Name.ToLower() + </p> + } - @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) { - <details> - <summary> - <span> - @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") - </span> - <hr style="margin: revert;"/> - </summary> - <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;"> - @{ - var policies = GetValidPolicyEventsByType(type); - var invalidPolicies = GetInvalidPolicyEventsByType(type); - // enumerate all properties with friendly name - var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) - .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) - .ToFrozenSet(); - var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); - } - <thead> - <tr> - @foreach (var name in propNames) { - <th style="border-width: 1px">@name</th> - } - <th style="border-width: 1px">Actions</th> - </tr> - </thead> - <tbody style="border-width: 1px;"> - @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { - <tr> - @{ - var typedContent = policy.TypedContent!; - var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => props.Any(y => y.Name == x.Name)) - .ToFrozenSet(); - Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); - } - @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) { - <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td> - } - <td> - <div style="display: ruby;"> - @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton> - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> - @if (policy.IsLegacyType) { - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> - } - } - </div> - </td> - </tr> - } - </tbody> - </table> - <details> - <summary> - <u> - @("Invalid " + GetPolicyTypeName(type).ToLower()) - </u> - </summary> - <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;"> - <thead> - <tr> - <th style="border-width: 1px">State key</th> - <th style="border-width: 1px">Json contents</th> - </tr> - </thead> - <tbody> - @foreach (var policy in invalidPolicies) { - <tr> - <td>@policy.StateKey</td> - <td> - <pre>@policy.RawContent.ToJson(true, false)</pre> - </td> - </tr> - } - </tbody> - </table> - </details> - </details> - } -} + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans + </p> + } + + @if (RedundantBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @RedundantBans.Value.ActivePolicies.Count redundant bans + </p> + } + + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); + + // var renderSw2 = Stopwatch.StartNew(); + // IOrderedEnumerable<Type> policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count); + // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}"); + + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@DuplicateBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } -@if (CurrentlyEditingEvent is not null) { - <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal> + @if (RedundantBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@RedundantBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } + + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent> + } + + // foreach (var type in policiesByType) { + @* foreach (var type in (List<Type>) []) { *@ + @* <details> *@ + @* <summary> *@ + @* <span> *@ + @* @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") *@ + @* </span> *@ + @* <hr style="margin: revert;"/> *@ + @* </summary> *@ + @* <table class="table table-striped table-hover table-bordered align-middle"> *@ + @* @{ *@ + @* var renderSw3 = Stopwatch.StartNew(); *@ + @* var policies = GetValidPolicyEventsByType(type); *@ + @* var invalidPolicies = GetInvalidPolicyEventsByType(type); *@ + @* // enumerate all properties with friendly name *@ + @* var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@ + @* .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) *@ + @* .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) *@ + @* .ToFrozenSet(); *@ + @* var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); *@ + @* *@ + @* var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@ + @* .Where(x => props.Any(y => y.Name == x.Name)) *@ + @* .ToFrozenSet(); *@ + @* logger.LogInformation($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); *@ + @* logger.LogInformation($"Filtered policies and got properties in {renderSw3.GetElapsedAndRestart()}"); *@ + @* } *@ + @* <thead> *@ + @* <tr> *@ + @* @foreach (var name in propNames) { *@ + @* <th>@name</th> *@ + @* } *@ + @* <th>Actions</th> *@ + @* </tr> *@ + @* </thead> *@ + @* <tbody> *@ + @* @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { *@ + @* <PolicyListRowComponent PolicyInfo="@policy" Room="@Room"></PolicyListRowComponent> *@ + @* } *@ + @* </tbody> *@ + @* </table> *@ + @* <details> *@ + @* <summary> *@ + @* <u> *@ + @* @("Invalid " + GetPolicyTypeName(type).ToLower()) *@ + @* </u> *@ + @* </summary> *@ + @* <table class="table table-striped table-hover"> *@ + @* <thead> *@ + @* <tr> *@ + @* <th>State key</th> *@ + @* <th>Json contents</th> *@ + @* </tr> *@ + @* </thead> *@ + @* <tbody> *@ + @* @foreach (var policy in invalidPolicies) { *@ + @* <tr> *@ + @* <td>@policy.StateKey</td> *@ + @* <td> *@ + @* <pre>@policy.RawContent.ToJson(true, false)</pre> *@ + @* </td> *@ + @* </tr> *@ + @* } *@ + @* </tbody> *@ + @* </table> *@ + @* </details> *@ + @* </details> *@ + // } + + // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed); + } } @code { @@ -129,112 +157,472 @@ else { private const bool Debug = false; #endif + private bool IsInitialised { get; set; } = false; private bool Loading { get; set; } = true; - //get room list - // - sync withroom list filter - // Type = support.feline.msc3784 - //support.feline.policy.lists.msc.v1 [Parameter] - public string RoomId { get; set; } = null!; + public required string RoomId { get; set; } - private bool _enableAvatars; - private StateEventResponse? _currentlyEditingEvent; - - // static readonly Dictionary<string, string?> Avatars = new(); - // static readonly Dictionary<string, RemoteHomeserver> Servers = new(); + [Parameter, SupplyParameterFromQuery] + public bool RenderEventInfo { + get; + set { + field = value; + StateHasChanged(); + } + } - // private static List<StateEventResponse> PolicyEvents { get; set; } = new(); private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); - private StateEventResponse? CurrentlyEditingEvent { - get => _currentlyEditingEvent; + public StateEventResponse? ServerPolicyToMakePermanent { + get; set { - _currentlyEditingEvent = value; + field = value; StateHasChanged(); } } - // public bool EnableAvatars { - // get => _enableAvatars; - // set { - // _enableAvatars = value; - // if (value) GetAllAvatars(); - // } - // } + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private GenericRoom Room { get; set; } = null!; + private RoomPowerLevelEventContent PowerLevels { get; set; } = null!; + public bool CurrentUserIsDraupnir { get; set; } - private AuthenticatedHomeserverGeneric Homeserver { get; set; } - private GenericRoom Room { get; set; } - private RoomPowerLevelEventContent PowerLevels { get; set; } + public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = []; + + private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + // event types, unnamed + // private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes + // .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + // + // private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes + // .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); + + Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new(); + PolicyCollection? DuplicateBans { get; set; } + PolicyCollection? RedundantBans { get; set; } protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); await base.OnInitializedAsync(); - Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!; + Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; if (Homeserver is null) return; Room = Homeserver.GetRoom(RoomId!); - PowerLevels = (await Room.GetPowerLevelsAsync())!; - await LoadStatesAsync(); - Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); + IsInitialised = true; + StateHasChanged(); + await Task.WhenAll( + Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }), + Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>(DraupnirProtectedRoomsData.EventId)) is not null; }) + ); + StateHasChanged(); + await LoadStateAsync(firstLoad: true); + Loading = false; + logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed); } - private async Task LoadStatesAsync() { + private async Task LoadStateAsync(bool firstLoad = false) { + // preload workers in task pool + // await Task.WhenAll(Enumerable.Range(0, WebWorkerService.MaxWorkerCount).Select(async _ => (await WebWorkerService.TaskPool.GetWorkerAsync()).WhenReady).ToList()); + var taskPoolReadyTask = WebWorkerService.TaskPool.SetWorkerCount(WebWorkerService.MaxWorkerCount); + var sw = Stopwatch.StartNew(); + // Loading = true; + // var states = Room.GetFullStateAsync(); + var states = await Room.GetFullStateAsListAsync(); + // PolicyEventsByType.Clear(); + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); + + foreach (var type in KnownPolicyTypes) { + if (!PolicyCollections.ContainsKey(type)) { + var filterPropSw = Stopwatch.StartNew(); + // enumerate all properties with friendly name + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) + .ToFrozenSet(); + + var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenDictionary(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName(), x => x); + logger.LogInformation("{Count} proxy safe props found in {TypeFullName} ({TimeSpan})", proxySafeProps?.Count, type.FullName, filterPropSw.Elapsed); + PolicyCollections.Add(type, new() { + Name = type.GetFriendlyNamePluralOrNull() ?? type.FullName ?? type.Name, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = proxySafeProps + }); + } + } + + var count = 0; + var parseSw = Stopwatch.StartNew(); + foreach (var evt in states) { + var mappedType = evt.MappedType; + if (count % 100 == 0) + logger.LogInformation("Processing state #{Count:000000} {EvtType} @ {SwElapsed} (took {ParseSwElapsed:c} so far to process)", count, evt.Type, sw.Elapsed, parseSw.Elapsed); + count++; + + if (!mappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + + var collection = PolicyCollections[mappedType]; + + var key = (evt.Type, evt.StateKey!); + var policyInfo = new PolicyCollection.PolicyInfo { + Policy = evt, + MadeRedundantBy = [], + DuplicatedBy = [] + }; + if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue<string>())) { + collection.ActivePolicies.Remove(key); + if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) { + if (StateEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue; + collection.RemovedPolicies[key] = policyInfo; + } + } + else { + collection.RemovedPolicies.Remove(key); + if (!collection.ActivePolicies.TryAdd(key, policyInfo)) { + if (StateEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue; + collection.ActivePolicies[key] = policyInfo; + } + } + } + + logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed); + foreach (var collection in PolicyCollections) { + logger.LogInformation("Policy collection {KeyFullName} has {ActivePoliciesCount} active and {RemovedPoliciesCount} removed policies.", collection.Key.FullName, collection.Value.ActivePolicies.Count, collection.Value.RemovedPolicies.Count); + } + + await Task.Delay(1); + + Loading = false; + StateHasChanged(); + await Task.Delay(100); + + // return; + logger.LogInformation("LoadStatesAsync: Scanning for redundant policies..."); + + var scanSw = Stopwatch.StartNew(); + // var allPolicyInfos = PolicyCollections.Values + // .SelectMany(x => x.ActivePolicies.Values) + // .ToArray(); + // var allPolicies = allPolicyInfos + // .Select<PolicyCollection.PolicyInfo, (PolicyCollection.PolicyInfo PolicyInfo, PolicyRuleEventContent TypedContent)>(x => (x, (x.Policy.TypedContent as PolicyRuleEventContent)!)) + // .ToList(); + // var hashPolicies = allPolicies + // .Where(x => x.TypedContent.IsHashedRule()) + // .ToList(); + // var wildcardPolicies = allPolicies + // .Except(hashPolicies) // hashed policies cannot be wildcards + // .Where(x => x.TypedContent.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent) + // .ToList(); + // var nonWildcardPolicies = allPolicies + // // .Except(wildcardPolicies) + // .Where(x => !x.TypedContent!.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent) + // .ToList(); + // Console.WriteLine($"Got {allPolicies.Count} total policies, {wildcardPolicies.Count} wildcard policies. Time spent: {scanSw.Elapsed}"); + // int i = 0; + // int hits = 0; + // int redundant = 0; + // int duplicates = 0; + + // foreach (var (policyInfo, policyContent) in allPolicies) { + // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) { + // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event + // if (StateEvent.TypeKeyPairMatches(policyInfo.Policy, otherPolicyInfo.Policy)) { + // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson()); + // continue; // same type and state key + // } + // // if(!policyContent.IsHashedRule()) + // } + // + // if (++i % 100 == 0) { + // Console.WriteLine($"Processed {i} policies in {scanSw.Elapsed}"); + // await Task.Delay(1); + // } + // } + + int scanningPolicyCount = 0; + var aggregatedPolicies = PolicyCollections.Values + .Aggregate(new List<StateEventResponse>(), (acc, val) => { + acc.AddRange(val.ActivePolicies.Select(x => x.Value.Policy)); + return acc; + }); + Console.WriteLine($"Scanning for redundant policies in {aggregatedPolicies.Count} total policies... ({scanSw.Elapsed})"); + List<Task<List<PolicyCollection.PolicyInfo>>> tasks = []; + // try to save some load... + var policiesJson = JsonSerializer.Serialize(aggregatedPolicies); + var policiesJsonMarshalled = JsRuntime.ReturnMe<SpawnDev.BlazorJS.JSObjects.String>(policiesJson); + var ranges = Enumerable.Range(0, aggregatedPolicies.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount); + await taskPoolReadyTask; + tasks.AddRange(ranges.Select(range => WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJsonMarshalled, range.First(), range.Last()))); + + Console.WriteLine($"Main: started {tasks.Count} workers in {scanSw.Elapsed}"); + // tasks.Add(CheckDuplicatePoliciesAsync(allPolicyInfos, range.First() .. range.Last())); + + // var allPolicyEvents = aggregatedPolicies.Select(x => x.Policy).ToList(); + + DuplicateBans = new() { + Name = "Duplicate bans", + ViewType = PolicyCollection.SpecialViewType.Duplicates, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + RedundantBans = new() { + Name = "Redundant bans", + ViewType = PolicyCollection.SpecialViewType.Redundant, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + var allPolicyInfos = PolicyCollections.Values + .SelectMany(x => x.ActivePolicies.Values) + .ToArray(); + + await foreach (var modifiedPolicyInfos in tasks.ToAsyncResultEnumerable()) { + if (modifiedPolicyInfos.Count == 0) continue; + var applySw = Stopwatch.StartNew(); + // Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}"); + foreach (var modifiedPolicyInfo in modifiedPolicyInfos) { + var original = allPolicyInfos.First(p => p.Policy.EventId == modifiedPolicyInfo.Policy.EventId); + original.DuplicatedBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.DuplicatedBy.Any(y => StateEvent.Equals(x, y))).ToList(); + original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => StateEvent.Equals(x, y))).ToList(); + modifiedPolicyInfo.DuplicatedBy = modifiedPolicyInfo.MadeRedundantBy = []; // Early dereference + if (original.DuplicatedBy.Count > 0) { + if (!DuplicateBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + DuplicateBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } + + if (original.MadeRedundantBy.Count > 0) { + if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } + // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}"); + } + + Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})"); + } + + Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}"); + + // // scan for wildcard matches + // foreach (var policy in allPolicies) { + // var matchingPolicies = wildcardPolicies + // .Where(x => + // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy) + // && x.Item2.EntityMatches(policy.TypedContent.Entity!) + // ) + // .ToList(); + // + // if (matchingPolicies.Count > 0) { + // logger.LogInformation($"{i} Got {matchingPolicies.Count} hits for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}"); + // foreach (var match in matchingPolicies) { + // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy); + // } + // + // hits++; + // redundant += matchingPolicies.Count; + // + // if (hits % 5 == 0) + // StateHasChanged(); + // } + // else { + // //logger.LogInformation("Sleeping..."); + // await Task.Delay(1); + // } + // + // i++; + // } + // + // i = 0; + // // scan for exact duplicates + // foreach (var policy in allPolicies) { + // var matchingPolicies = allPolicies + // .Where(x => + // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy) + // && ( + // x.Item2.IsHashedRule() + // ? x.Item2.EntityMatches(policy.Item2.Entity) + // : x.Item2!.Entity == policy.Item2.Entity! + // ) + // ) + // .ToList(); + // + // if (matchingPolicies.Count > 0) { + // logger.LogInformation($"{i} Got {matchingPolicies.Count} duplicates for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}"); + // foreach (var match in matchingPolicies) { + // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy); + // } + // + // hits++; + // duplicates += matchingPolicies.Count; + // + // if (hits % 5 == 0) + // StateHasChanged(); + // } + // else { + // //logger.LogInformation("Sleeping..."); + // await Task.Delay(1); + // } + // + // i++; + // } + // + // logger.LogInformation($"LoadStatesAsync: Found {hits} ({redundant} redundant, {duplicates} duplicates) redundant policies in {sw.Elapsed}"); + // StateHasChanged(); + } + + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson.ValueOf()); + Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.ValueOf().Length} bytes of JSON ({policies!.Count} policies)"); + return await CheckDuplicatePoliciesAsync(policies!, start .. end); + } + + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson); + Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON ({policies!.Count} policies)"); + return await CheckDuplicatePoliciesAsync(policies!, start .. end); + } + + [return: WorkerTransfer] + private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, int start, int end) + => CheckDuplicatePoliciesAsync(policies, start .. end); + + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, Range range) { + var sw = Stopwatch.StartNew(); + var jsConsole = App.Host.Services.GetService<JsConsoleService>()!; + Console.WriteLine($"Processing policies in range {range} ({range.GetOffsetAndLength(policies.Count).Length}) with {policies.Count} total policies"); + var allPolicies = policies + .Select(x => (Event: x, TypedContent: (x.TypedContent as PolicyRuleEventContent)!)) + .ToList(); + var toCheck = allPolicies[range]; + var modifiedPolicies = new List<PolicyCollection.PolicyInfo>(); + + foreach (var (policyEvent, policyContent) in toCheck) { + List<StateEventResponse> duplicatedBy = []; + List<StateEventResponse> madeRedundantBy = []; + + foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) { + if (policyEvent == otherPolicyEvent) continue; // same event + if (StateEvent.TypeKeyPairMatches(policyEvent, otherPolicyEvent)) { + // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson()); + Console.WriteLine($"Sanity check failed: Found same type and state key for two different policies: {policyEvent.RawContent.ToJson()} and {otherPolicyEvent.RawContent.ToJson()}"); + continue; // same type and state key + } + + // if(!policyContent.IsHashedRule()) + if (!string.IsNullOrWhiteSpace(policyContent.Entity) && policyContent.Entity == otherPolicyContent.Entity) { + // Console.WriteLine($"Found duplicate policy: {policyEvent.EventId} is duplicated by {otherPolicyEvent.EventId}"); + duplicatedBy.Add(otherPolicyEvent); + } + } + + if (duplicatedBy.Count > 0 || madeRedundantBy.Count > 0) { + var summary = $"Policy {policyEvent.EventId} is:"; + if (duplicatedBy.Count > 0) + summary += $"\n- Duplicated by {duplicatedBy.Count} policies: {string.Join(", ", duplicatedBy.Select(x => x.EventId))}"; + if (madeRedundantBy.Count > 0) + summary += $"\n- Made redundant by {madeRedundantBy.Count} policies: {string.Join(", ", madeRedundantBy.Select(x => x.EventId))}"; + // Console.WriteLine(summary); + await jsConsole.Info(summary); + await Task.Delay(1); + modifiedPolicies.Add(new() { + Policy = policyEvent, + DuplicatedBy = duplicatedBy, + MadeRedundantBy = madeRedundantBy + }); + } + + // await Task.Delay(1); + } + + await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}"); + + return modifiedPolicies; + } + + // the old one: + private async Task LoadStatesAsync(bool firstLoad = false) { + await LoadStateAsync(firstLoad); + return; + var sw = Stopwatch.StartNew(); Loading = true; - var states = Room.GetFullStateAsync(); - PolicyEventsByType.Clear(); - await foreach (var state in states) { - if (state is null) continue; + // var states = Room.GetFullStateAsync(); + var states = await Room.GetFullStateAsListAsync(); + // PolicyEventsByType.Clear(); + + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); + + foreach (var type in KnownPolicyTypes) { + if (!PolicyEventsByType.ContainsKey(type)) + PolicyEventsByType.Add(type, new List + <StateEventResponse>(16000)); + } + + int count = 0; + + foreach (var state in states) { + var _spsw = Stopwatch.StartNew(); + TimeSpan e1, e2, e3, e4, e5, e6, t; if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; - if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); - PolicyEventsByType[state.MappedType].Add(state); + e1 = _spsw.Elapsed; + var targetPolicies = PolicyEventsByType[state.MappedType]; + e2 = _spsw.Elapsed; + if (!firstLoad && targetPolicies.FirstOrDefault(x => StateEvent.TypeKeyPairMatches(x, state)) is { } evt) { + e3 = _spsw.Elapsed; + if (StateEvent.Equals(evt, state)) { + if (count % 100 == 0) { + await Task.Delay(10); + await Task.Yield(); + } + + e4 = _spsw.Elapsed; + logger.LogInformation("[E] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={Zero:c},t={SpswElapsed:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, TimeSpan.Zero, _spsw.Elapsed); + continue; + } + + e4 = _spsw.Elapsed; + targetPolicies.Remove(evt); + e5 = _spsw.Elapsed; + targetPolicies.Add(state); + e6 = _spsw.Elapsed; + t = _spsw.Elapsed; + logger.LogInformation("[M] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={E5:c}, e6={E6:c},t={TimeSpan1:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, e5, e6, t); + } + else { + targetPolicies.Add(state); + t = _spsw.Elapsed; + logger.LogInformation("[N] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={Zero:c}, e4={TimeSpan1:c}, e5={Zero1:c}, e6={TimeSpan2:c}, t={TimeSpan3:c})", count++, state.Type, sw.Elapsed, e1, e2, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, t); + } + + // await Task.Delay(10); + // await Task.Yield(); } + logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed); + Loading = false; StateHasChanged(); + await Task.Delay(10); + await Task.Yield(); + logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed); } - // private async Task GetAllAvatars() { - // // if (!_enableAvatars) return; - // Console.WriteLine("Getting avatars..."); - // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList(); - // Console.WriteLine($"Got {users.Count} users!"); - // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList()); - // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!"); - // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable(); - // await foreach (var server in homeserverTasks) { - // if (server is null) continue; - // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList(); - // await Task.WhenAll(profileTasks); - // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } }); - // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) { - // // if (profile is null) continue; - // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) { - // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl); - // Avatars.TryAdd(profile.Key, url); - // } - // else Avatars.TryAdd(profile.Key, null); - // } - // - // StateHasChanged(); - // } - // } - // - // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) { - // try { - // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid)); - // } - // catch { - // return null; - // } - // } - private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; - private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList(); - - private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList(); + // private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<StateEventResponse> GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is null or { Count: 0 }).ToList(); private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() ?? type.GetCustomAttributes<MatrixEventAttribute>() @@ -242,27 +630,34 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; - private async Task RemovePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { }); - PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); - await LoadStatesAsync(); - } + public struct PolicyCollection { + public required string Name { get; init; } + public SpecialViewType ViewType { get; init; } + public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count; - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); - CurrentlyEditingEvent = null; - await LoadStatesAsync(); - } + public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; } - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { - policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; - await LoadStatesAsync(); - } + // public Dictionary<(string Type, string StateKey), StateEventResponse> InvalidPolicies { get; set; } + public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; } + public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + public class PolicyInfo { + public required StateEventResponse Policy { get; init; } + public required List<StateEventResponse> MadeRedundantBy { get; set; } + public required List<StateEventResponse> DuplicatedBy { get; set; } + } - // event types, unnamed - private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes - .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + public enum SpecialViewType { + None, + Duplicates, + Redundant, + } + } + + // private struct PolicyStats { + // public int Active { get; set; } + // public int Invalid { get; set; } + // public int Removed { get; set; } + // } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs new file mode 100644
index 0000000..0106c6e --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -0,0 +1,144 @@ +using LibMatrix; +using LibMatrix.EventTypes.Interop.Draupnir; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using SpawnDev.BlazorJS.WebWorkers; + +namespace MatrixUtils.Web.Pages.Rooms; + +public partial class PolicyList { + +#region Draupnir interop + + private SemaphoreSlim ss = new(16, 16); + + private async Task DraupnirKickMatching(StateEventResponse policy) { + try { + var content = policy.TypedContent! as PolicyRuleEventContent; + if (content is null) return; + if (string.IsNullOrWhiteSpace(content.Entity)) return; + + var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId); + var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList(); + + ActiveKicks.Add(policy, rooms.Count); + StateHasChanged(); + await Task.Delay(500); + + // for (int i = 0; i < 12; i++) { + // _ = WebWorkerService.TaskPool.Invoke(WasteCpu); + // } + + // static async Task runKicks(string roomId, PolicyRuleEventContent content) { + // Console.WriteLine($"Checking {roomId}..."); + // // Console.WriteLine($"Checking {room.RoomId}..."); + // // + // // try { + // // var members = await room.GetMembersListAsync(); + // // foreach (var member in members) { + // // var membership = member.ContentAs<RoomMemberEventContent>(); + // // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; + // // if (membership?.Membership is "leave" or "ban") continue; + // // + // // if (content.EntityMatches(member.StateKey!)) + // // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // // } + // // } + // // finally { + // // Console.WriteLine($"Finished checking {room.RoomId}..."); + // // } + // } + // + // try { + // var tasks = rooms.Select(room => WebWorkerService.TaskPool.Invoke(runKicks, room.RoomId, content)).ToList(); + // + // await Task.WhenAll(tasks); + // } + // catch (Exception e) { + // Console.WriteLine(e); + // } + + await NastyInternalsPleaseIgnore.ExecuteKickWithWasmWorkers(WebWorkerService, Homeserver, policy, data.Rooms); + // await Task.Run(async () => { + // foreach (var room in rooms) { + // try { + // Console.WriteLine($"Checking {room.RoomId}..."); + // var members = await room.GetMembersListAsync(); + // foreach (var member in members) { + // var membership = member.ContentAs<RoomMemberEventContent>(); + // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (content.EntityMatches(member.StateKey!)) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + // ActiveKicks[policy]--; + // StateHasChanged(); + // } + // finally { + // Console.WriteLine($"Finished checking {room.RoomId}..."); + // } + // } + // }); + } + finally { + ActiveKicks.Remove(policy); + StateHasChanged(); + await Task.Delay(500); + } + } + +#region Nasty, nasty internals, please ignore! + + private static class NastyInternalsPleaseIgnore { + public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List<string> roomIds) { + try { + // var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal, hs.WellKnownUris.Client, hs.AccessToken, roomId, content.Entity)).ToList(); + var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal2, hs.WellKnownUris, hs.AccessToken, roomId, evt)).ToList(); + // workerService.TaskPool.Invoke(ExecuteKickInternal, hs.BaseUrl, hs.AccessToken, roomIds, content.Entity); + await Task.WhenAll(tasks); + } + catch (Exception e) { + Console.WriteLine(e); + } + } + + private static async Task ExecuteKickInternal(string homeserverBaseUrl, string accessToken, string roomId, string entity) { + try { + Console.WriteLine("args: " + string.Join(", ", homeserverBaseUrl, accessToken, roomId, entity)); + Console.WriteLine($"Checking {roomId}..."); + var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken); + Console.WriteLine($"Got HS..."); + var room = hs.GetRoom(roomId); + Console.WriteLine($"Got room..."); + var members = await room.GetMembersListAsync(); + Console.WriteLine($"Got members..."); + // foreach (var member in members) { + // var membership = member.ContentAs<RoomMemberEventContent>(); + // if (member.StateKey == hs.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (entity == member.StateKey) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + } + catch (Exception e) { + Console.WriteLine(e); + } + } + + private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { + Console.WriteLine($"Checking {roomId}..."); + Console.WriteLine(policy.EventId); + } + } + +#endregion + +#endregion + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor new file mode 100644
index 0000000..5d5bb5d --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -0,0 +1,240 @@ +@page "/Rooms/{RoomId}/Policies2" +@using LibMatrix +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Diagnostics +@using LibMatrix.RoomTypes +@using System.Collections.Frozen +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix.EventTypes +@using LibMatrix.EventTypes.Spec.State.RoomInfo + +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +<h3>Policy list editor - Editing @RoomId</h3> +<hr/> +@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ +<LinkButton OnClickAsync="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> + +@if (Loading) { + <p>Loading...</p> +} +else if (PolicyEventsByType is not { Count: > 0 }) { + <p>No policies yet</p> +} +else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); + @foreach (var (type, value) in PolicyEventsByType) { + <p> + @(GetValidPolicyEventsByType(type).Count) active, + @(GetInvalidPolicyEventsByType(type).Count) invalid + (@value.Count total) + @(GetPolicyTypeName(type).ToLower()) + </p> + } + + Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}"); + + @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) { + <details> + <summary> + <span> + @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") + </span> + <hr style="margin: revert;"/> + </summary> + <div class="flex-grid"> + @{ + var policies = GetValidPolicyEventsByType(type); + var invalidPolicies = GetInvalidPolicyEventsByType(type); + // enumerate all properties with friendly name + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) + .ToFrozenSet(); + var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + + var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenSet(); + Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); + } + @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { + <div class="flex-item"> + @{ + var typedContent = policy.TypedContent!; + } + @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) { + <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td> + } + <div style="display: ruby;"> + @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { + <LinkButton OnClickAsync="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton> + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> + @if (policy.IsLegacyType) { + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> + } + + @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) { + <LinkButton OnClickAsync="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton> + @if (CurrentUserIsDraupnir) { + <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> + } + } + else { + <p>meow</p> + } + } + else { + <p>No permission to modify</p> + } + </div> + </div> + } + </div> + <details> + <summary> + <u> + @("Invalid " + GetPolicyTypeName(type).ToLower()) + </u> + </summary> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>State key</th> + <th>Json contents</th> + </tr> + </thead> + <tbody> + @foreach (var policy in invalidPolicies) { + <tr> + <td>@policy.StateKey</td> + <td> + <pre>@policy.RawContent.ToJson(true, false)</pre> + </td> + </tr> + } + </tbody> + </table> + </details> + </details> + } + + Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); +} + +@if (CurrentlyEditingEvent is not null) { + <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal> +} + +@code { + +#if DEBUG + private const bool Debug = true; +#else + private const bool Debug = false; +#endif + + private bool Loading { get; set; } = true; + + [Parameter] + public string RoomId { get; set; } + + private bool _enableAvatars; + private StateEventResponse? _currentlyEditingEvent; + private StateEventResponse? _serverPolicyToMakePermanent; + + private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); + + private StateEventResponse? CurrentlyEditingEvent { + get => _currentlyEditingEvent; + set { + _currentlyEditingEvent = value; + StateHasChanged(); + } + } + + private StateEventResponse? ServerPolicyToMakePermanent { + get => _serverPolicyToMakePermanent; + set { + _serverPolicyToMakePermanent = value; + StateHasChanged(); + } + } + + private AuthenticatedHomeserverGeneric Homeserver { get; set; } + private GenericRoom Room { get; set; } + private RoomPowerLevelEventContent PowerLevels { get; set; } + private bool CurrentUserIsDraupnir { get; set; } + + protected override async Task OnInitializedAsync() { + var sw = Stopwatch.StartNew(); + await base.OnInitializedAsync(); + Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; + if (Homeserver is null) return; + Room = Homeserver.GetRoom(RoomId!); + PowerLevels = (await Room.GetPowerLevelsAsync())!; + CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; + await LoadStatesAsync(); + Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); + } + + private async Task LoadStatesAsync() { + Loading = true; + var states = Room.GetFullStateAsync(); + PolicyEventsByType.Clear(); + await foreach (var state in states) { + if (state is null) continue; + if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); + PolicyEventsByType[state.MappedType].Add(state); + } + + Loading = false; + StateHasChanged(); + } + + private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + + private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + + private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + + private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() + ?? type.GetCustomAttributes<MatrixEventAttribute>() + .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + + private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + + private async Task RemovePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { }); + PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + await LoadStatesAsync(); + } + + private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent); + CurrentlyEditingEvent = null; + await LoadStatesAsync(); + } + + private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { + policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; + await LoadStatesAsync(); + } + + private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + // event types, unnamed + private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes + .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + + private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes + .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css new file mode 100644
index 0000000..d224737 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
@@ -0,0 +1,32 @@ +th { + border-width: 1px; +} + +table { + width: fit-content; + border-width: 1px; + vertical-align: middle; +} + +.flex-grid { + display: grid; + /*grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));*/ + /*// fit based on content max width*/ + grid-template-columns: repeat(auto-fill, minmax(min-content, 1fr)); + + gap: 10px; +} + +.flex-item { + /*flex: 1 1 30%;*/ + /*margin: 0.25rem;*/ + /*position: relative;*/ + /*display: flex;*/ + /*flex-direction: column;*/ + min-width: 0; + word-wrap: break-word; + background-color: #fff1; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: .5rem +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor new file mode 100644
index 0000000..932e0fe --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -0,0 +1,74 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.RoomTypes +<details> + <summary> + <span> + @($"{PolicyCollection.Name}: {PolicyCollection.TotalCount} policies") + </span> + <hr style="margin: revert;"/> + </summary> + <table class="table table-striped table-hover table-bordered align-middle"> + <thead> + <tr> + <th>Actions</th> + @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) { + <th>@name</th> + } + </tr> + </thead> + <tbody> + @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) { + <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent> + } + </tbody> + </table> + @if (RenderInvalidSection) { + <details> + <summary> + <u> + @("Invalid " + PolicyCollection.Name.ToLower()) + </u> + </summary> + <table class="table table-striped table-hover table-bordered align-middle"> + <thead> + <tr> + <th>State key</th> + <th>Json contents</th> + </tr> + </thead> + <tbody> + @foreach (var policy in PolicyCollection.RemovedPolicies.Values) { + <tr> + <td>@policy.Policy.StateKey</td> + <td> + <pre>@policy.Policy.RawContent.ToJson(true, false)</pre> + </td> + </tr> + } + </tbody> + </table> + </details> + } +</details> + +@code { + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + [Parameter] + public required GenericRoom Room { get; set; } + + [Parameter] + public bool RenderEventInfo { get; set; } + + [Parameter] + public bool RenderInvalidSection { get; set; } = true; + + protected override bool ShouldRender() { + // if (PolicyCollection is null) return false; + + return true; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor new file mode 100644
index 0000000..8585561 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
@@ -0,0 +1,88 @@ +@using LibMatrix +@using LibMatrix.EventTypes.Common +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents +<h3>Policy list editor - Editing @(RoomName ?? Room.RoomId)</h3> +@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) { + <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span> +} +@if (!string.IsNullOrWhiteSpace(RoomAlias)) { + <span>Alias: @RoomAlias</span> +} +<hr/> +@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ +<LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy +</LinkButton> +<LinkButton OnClickAsync="@(() => { + MassCreatePolicies = true; + return Task.CompletedTask; + })">Create many new policies +</LinkButton> +<LinkButton OnClickAsync="@(() => ReloadStateAsync())">Refresh</LinkButton> + +@if (CurrentlyEditingEvent is not null) { + <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal> +} + +@if (MassCreatePolicies) { + <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { + MassCreatePolicies = false; + // _ = LoadStatesAsync(); + })"></MassPolicyEditorModal> +} +<br/> +<InputCheckbox Value="@RenderEventInfo" ValueChanged="@RenderEventInfoChanged" ValueExpression="@(() => RenderEventInfo)"/> +<span> Render event info</span> + +@code { + + [Parameter] + public required GenericRoom Room { get; set; } + + [Parameter] + public required Func<Task> ReloadStateAsync { get; set; } + + [Parameter] + public required bool RenderEventInfo { get; set; } + + [Parameter] + public required EventCallback<bool> RenderEventInfoChanged { get; set; } + + private string? RoomName { get; set; } + private string? RoomAlias { get; set; } + private string? DraupnirShortcode { get; set; } + + private StateEventResponse? CurrentlyEditingEvent { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool MassCreatePolicies { + get; + set { + field = value; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() { + await Task.WhenAll( + Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }), + Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), + Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }) + ); + + StateHasChanged(); + } + + private async Task UpdatePolicyAsync(StateEventResponse evt) { + Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implemented!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor new file mode 100644
index 0000000..cd432c9 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
@@ -0,0 +1,218 @@ +@using System.Reflection +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +@if (_isInitialized && IsVisible) { + <tr id="@PolicyInfo.Policy.EventId"> + <td> + <div style="display: flex; flex-direction: row; gap: 0.5em;"> + @* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@ + @if (true) { + <LinkButton OnClickAsync="@(() => { + IsEditing = true; + return Task.CompletedTask; + })">Edit + </LinkButton> + <LinkButton OnClickAsync="@RemovePolicyAsync">Remove</LinkButton> + @if (Policy.IsLegacyType) { + <LinkButton OnClickAsync="@RemovePolicyAsync">Update type</LinkButton> + } + + @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) { + <LinkButton OnClickAsync="@ConvertToAclAsync">Convert to ACL</LinkButton> + } + + @* @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(Policy.Type)) { *@ + @* <LinkButton OnClickAsync="@(() => { *@ + @* ServerPolicyToMakePermanent = Policy; *@ + @* return Task.CompletedTask; *@ + @* })">Make permanent *@ + @* </LinkButton> *@ + @* @if (CurrentUserIsDraupnir) { *@ + @* <LinkButton Color="@(ActiveKicks.ContainsKey(Policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(Policy))">Kick *@ + @* users @(ActiveKicks.TryGetValue(Policy, out var kick) ? $"({kick})" : null) *@ + @* </LinkButton> *@ + @* } *@ + // } + } + else { + <p>No permission to modify</p> + } + </div> + </td> + @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) { + if (prop.Name == "Entity") { + <td> + <span>@TruncateMxid(TypedContent.Entity)</span> + @foreach (var dup in PolicyInfo.DuplicatedBy) { + <br/> + <span>Duplicated by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span> + } + @foreach (var dup in PolicyInfo.MadeRedundantBy) { + <br/> + <span>Also matched by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span> + } + @if (RenderEventInfo) { + <br/> + <pre style="margin-bottom: unset;"> + @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTimestamp + </pre> + } + </td> + } + else { + <td>@prop.GetGetMethod()?.Invoke(TypedContent, null)</td> + } + } + </tr> + + @if (IsEditing) { + <PolicyEditorModal PolicyEvent="@Policy" OnClose="@(() => IsEditing = false)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal> + } + @* TODO: Implement ability to turn ACLs into wildcards *@ + @*@if (ServerPolicyToMakePermanent is not null) { + <ModalWindow Title="Make policy permanent"> + + </ModalWindow> + }*@ +} + + + +@code { + + [Parameter] + public PolicyList.PolicyCollection.PolicyInfo PolicyInfo { get; set; } + + [Parameter] + public GenericRoom Room { get; set; } = null!; + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + [Parameter] + public bool RenderEventInfo { get; set; } + + [Parameter] + public required Action PolicyCollectionStateHasChanged { get; set; } + + private StateEventResponse Policy => PolicyInfo.Policy; + + private bool IsEditing { + get; + set { + field = value; + _isDirty = true; + StateHasChanged(); + } + } + + public bool IsVisible { + get; + set { + field = value; + _isDirty = true; + } + } = true; + + private PolicyRuleEventContent TypedContent { get; set; } + + private bool _isDirty = true; + private bool _isInitialized; + + protected override bool ShouldRender() => _isDirty; + + protected override void OnParametersSet() { + TypedContent = Policy.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy must have a typed content of type PolicyRuleEventContent."); + _isDirty = true; + _isInitialized = true; + // Console.WriteLine($"ParametersSet {Policy.StateKey}"); + } + + private static string TruncateMxid(string? mxid) { + if (string.IsNullOrWhiteSpace(mxid)) return mxid; + var parts = mxid.Split(':', 2); + if (parts[0].Length > 50) + parts[0] = parts[0][..50] + "[...]"; + + if (parts is [_, { Length: > 50 }]) + parts[1] = parts[1][..50] + "[...]"; + + return parts.Length == 1 ? parts[0] : $"{parts[0]}:{parts[1]}"; + } + + private async Task RemovePolicyAsync() { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { }); + bool shouldUpdateVisibility = true; + PolicyCollection.ActivePolicies.Remove((Policy.Type, Policy.StateKey)); + PolicyCollection.RemovedPolicies.Add((Policy.Type, Policy.StateKey), PolicyInfo); + if (PolicyInfo.DuplicatedBy.Count > 0) { + foreach (var evt in PolicyInfo.DuplicatedBy) { + var matchingEntry = PolicyCollection.ActivePolicies + .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.DuplicatedBy.RemoveAll(x => StateEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} duplicates from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + if (PolicyCollection.ViewType == PolicyList.PolicyCollection.SpecialViewType.Duplicates && matchingEntry.DuplicatedBy.Count == 0) { + PolicyCollection.ActivePolicies.Remove((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey)); + PolicyCollection.RemovedPolicies.Add((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey), matchingEntry); + Console.WriteLine($"Also removed {matchingEntry.Policy.EventId} as it is now redundant"); + } + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (PolicyInfo.MadeRedundantBy.Count > 0) { + foreach (var evt in PolicyInfo.MadeRedundantBy) { + var matchingEntry = PolicyCollection.ActivePolicies + .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => StateEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} redundants from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (shouldUpdateVisibility) { + IsVisible = false; + StateHasChanged(); + } + // PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + // await LoadStatesAsync(); + } + + private async Task UpdatePolicyAsync(StateEventResponse evt) { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, Policy.RawContent); + // CurrentlyEditingEvent = null; + // await LoadStatesAsync(); + } + + private async Task UpgradePolicyAsync() { + Policy.RawContent["gay.rory.matrixutils.upgraded_from_type"] = Policy.Type; + // await LoadStatesAsync(); + } + + private async Task ConvertToAclAsync() { + if (Policy.RawContent.ContainsKey("entity")) { + var newContent = Policy.ContentAs<ServerPolicyRuleEventContent>(); + newContent!.Entity = newContent.Entity!.Replace("@*:", ""); + await Room.SendStateEventAsync(ServerPolicyRuleEventContent.EventId, newContent.GetDraupnir2StateKey(), newContent); + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey!, new { }); + IsVisible = false; + StateHasChanged(); + } + else { + throw new InvalidOperationException("Policy event must contain an 'entity' field to convert to ACL."); + } + } + + private string Anchor(string anchor) { + return $"{NavigationManager.Uri.Split('#')[0]}#{anchor}"; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor new file mode 100644
index 0000000..52c5f30 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -0,0 +1,238 @@ +@page "/PolicyLists" +@using ArcaneLibs +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes +@using LibMatrix.EventTypes.Common +@using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using LibMatrix.RoomTypes +@inject ILogger<Index> logger +<h3> + <span>Policy lists </span> + <LinkButton OnClickAsync="@(() => { + ShowPolicyListCreationWindow = true; + return Task.CompletedTask; + })"> + <span class="oi oi-plus" aria-hidden="true"> Create</span> + </LinkButton> +</h3> + + +@if (!string.IsNullOrWhiteSpace(Status)) { + <p>@Status</p> +} +@if (!string.IsNullOrWhiteSpace(Status2)) { + <p>@Status2</p> +} +<hr/> + +<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading"> + <thead> + <tr> + <th>Room name</th> + <th>Policies</th> + <th/> + </tr> + </thead> + <tbody> + @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) { + <tr> + <td style="padding-right: 24px;"> + <span>@room.RoomName</span> + @if (room.IsLegacy) { + <span style="color: red;"> (legacy)</span> + } + <br/> + @if (!string.IsNullOrWhiteSpace(room.Shortcode)) { + <span style="font-size: 0.8em;">@room.Shortcode</span> + } + else { + <span style="color: red;">(no shortcode)</span> + } + </td> + <td> + <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.User) ?? 0) user policies</span><br/> + <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Server) ?? 0) server policies</span><br/> + <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Room) ?? 0) room policies</span><br/> + </td> + <td> + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")"> + <span class="oi oi-pencil" aria-hidden="true"> View/edit policies</span> + </LinkButton> + </td> + </tr> + } + </tbody> +</table> + +@if (ShowPolicyListCreationWindow && Homeserver != null) { + <ModalWindow Title="New policy list"> + @if (!string.IsNullOrWhiteSpace(_roomBuilder.Avatar.Url)) { + <MxcAvatar Homeserver="@Homeserver" MxcUri="@_roomBuilder.Avatar.Url" Circular="true" Size="4" SizeUnit="em"/> + } + else { + <img class="avatar" style="height: 4em; width: 4em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/> + } + <div style="display: inline-block; vertical-align: middle; padding-left: 1em;"> + <FancyTextBox @bind-Value="@_roomBuilder.Name.Name"></FancyTextBox> + <br/> + <span>#</span> + <FancyTextBox @bind-Value="@_roomBuilder.AliasLocalPart"></FancyTextBox> + <span>:@Homeserver!.ServerName</span> + <br/> + <FancyTextBox @bind-Value="@_roomBuilder.Avatar.Url"></FancyTextBox> + <InputFile OnChange="@RoomIconFilePicked"></InputFile> + </div> + <br/> + + <span>Bot shortcode: </span> + <FancyTextBox @bind-Value="@_shortcodeEvent.Shortcode"></FancyTextBox> + <br/> + <LinkButton OnClickAsync="@CreatePolicyList">Create</LinkButton> + + </ModalWindow> +} + +@code { + + private List<RoomInfo> Rooms { get; } = []; + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; + + isLoading = true; + Status = "Fetching rooms..."; + List<Task> _tasks = []; + await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) { + // roomsByType.Add(room); + Status2 = $"Found {room.RoomId} (MSC3784)..."; + _tasks.Add(Task.Run(async () => { + Rooms.Add(await RoomInfo.FromRoom(room)); + StateHasChanged(); + })); + } + + await Task.WhenAll(_tasks); + + isLoading = false; + Status = ""; + Status2 = ""; + } + + private async Task ScanLegacyLists() { + isLoading = true; + Status = "Searching for legacy lists..."; + var rooms = (await Homeserver.GetJoinedRooms()) + .Where(x => !Rooms.Any(y => y.Room.RoomId == x.RoomId)) + .Select(async room => { + var state = await room.GetFullStateAsListAsync(); + var policies = state + .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type)) + .ToList(); + if (policies.Count == 0) return null; + Status2 = $"Found legacy list {room.RoomId}..."; + return await RoomInfo.FromRoom(room, state, true); + }).ToAsyncResultEnumerable(); + + await foreach (var room in rooms) { + if (room is not null) { + Rooms.Add(room); + StateHasChanged(); + } + } + + isLoading = false; + Status = ""; + Status2 = ""; + } + + private string? Status { + get; + set { + field = value; + StateHasChanged(); + } + } + + private string? Status2 { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool ShowPolicyListCreationWindow { + get; + set { + field = value; + StateHasChanged(); + } + } = true; + + private class RoomInfo { + public GenericRoom Room { get; set; } + public string RoomName { get; set; } + public string? Shortcode { get; set; } + public Dictionary<PolicyType, int?> PolicyCounts { get; set; } + public bool IsLegacy { get; set; } + + public enum PolicyType { + User, + Room, + Server + } + + public static async Task<RoomInfo> FromRoom(GenericRoom room, List<StateEventResponse>? state = null, bool legacy = false) { + state ??= await room.GetFullStateAsListAsync(); + return new RoomInfo() { + Room = room, + IsLegacy = legacy, + RoomName = await room.GetNameAsync() + ?? (await room.GetCanonicalAliasAsync())?.Alias + ?? (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode + ?? room.RoomId, + Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode, + PolicyCounts = new() { + { PolicyType.User, state.Count(x => PolicyRoom.UserPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Server, state.Count(x => PolicyRoom.ServerPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Room, state.Count(x => PolicyRoom.RoomPolicyEventTypes.Contains(x.Type)) } + } + }; + } + } + + private readonly RoomBuilder _roomBuilder = new() { + Type = "support.feline.policy.lists.msc.v1", + Name = new() { Name = "New policy list" }, + AliasLocalPart = "policies" + }; + + private readonly MjolnirShortcodeEventContent _shortcodeEvent = new() { + Shortcode = "policy-list" + }; + + private bool isLoading = true; + + private static readonly SvgIdenticonGenerator IdenticonGenerator = new(); + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { + var res = await Homeserver!.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + _roomBuilder.Avatar.Url = res; + StateHasChanged(); + } + + private async Task CreatePolicyList() { + var room = await _roomBuilder.Create(Homeserver!); + Status = $"Created policy list {room.RoomId} ({room.GetNameAsync()})"; + await room.SendStateEventAsync(MjolnirShortcodeEventContent.EventId, _shortcodeEvent); + NavigationManager.NavigateTo($"/Rooms/{room.RoomId}/Policies"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor new file mode 100644
index 0000000..c1ee202 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
@@ -0,0 +1,52 @@ +@using ArcaneLibs +@using LibMatrix.Helpers +<tr> + <td>Room name:</td> + <td> + <FancyTextBox @bind-Value="@roomBuilder.Name.Name"></FancyTextBox> + </td> +</tr> +<tr> + <td>Room alias:</td> + <td> + <InputLocalPart Sigil="#" ServerName="@Homeserver.ServerName" @bind-LocalPart="@roomBuilder.AliasLocalPart"></InputLocalPart> + </td> +</tr> +<tr> + <td>Room icon:</td> + <td> + @if (!string.IsNullOrWhiteSpace(roomBuilder.Avatar.Url)) { + <MxcAvatar Homeserver="Homeserver" MxcUri="@roomBuilder.Avatar.Url" Size="3" SizeUnit="em" Circular="true"/> + } + else { + <img class="avatar" style="height: 3em; width: 3em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/> + } + <div style="display: inline-block; vertical-align: middle;"> + <FancyTextBox @bind-Value="@roomBuilder.Avatar.Url"></FancyTextBox> + <br/> + <SimpleFilePicker OnFilePicked="@RoomIconFilePicked"></SimpleFilePicker> + </div> + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private static readonly SvgIdenticonGenerator IdenticonGenerator = new(); + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { + var res = await Homeserver.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + roomBuilder.Avatar.Url = res; + PageStateHasChanged(); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor new file mode 100644
index 0000000..3f4a73d --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
@@ -0,0 +1,92 @@ +@using Blazored.LocalStorage +@using LibMatrix.Helpers +@inject ILocalStorageService LocalStorage +<tr> + <td>Room type:</td> + <td> + @if (RoomTypes.ContainsKey(roomBuilder.Type ?? "")) { + <InputSelect @bind-Value="@roomBuilder.Type"> + @foreach (var type in RoomTypes) { + <option value="@type.Key">@type.Value</option> + } + <option value="custom">Custom ...</option> + </InputSelect> + } + else { + <FancyTextBox @bind-Value="@roomBuilder.Type"></FancyTextBox> + } + + <span> version </span> + @if (Capabilities is null) { + <span style="color: #888;">Loading...</span> + } + else { + <InputSelect @bind-Value="@roomBuilder.Version"> + @foreach (var version in Capabilities.Capabilities.RoomVersions!.Available!) { + <option value="@version.Key">@version.Key (@version.Value)</option> + } + </InputSelect> + } + </td> +</tr> +<tr> + <td style="vertical-align: top;">Allow attribution:</td> + <td> + <InputCheckbox @bind-Value="@AllowAttribution"/> + <span>Allow attribution to Rory&::MatrixUtils</span> + <LinkButton InlineText="true" OnClick="@(() => ShowAttributionInfo = true)">?</LinkButton> + </td> +</tr> + +@if (ShowAttributionInfo) { + <ModalWindow Title="Allow attribution to Rory&::MatrixUtils" + OnCloseClicked="@(() => ShowAttributionInfo = false)"> + <span>This will add the following to the room creation content:</span> + <br/> + <pre>{ "gay.rory.created_using": "Rory&::MatrixUtils (https://mru.rory.gay)" }</pre> + <span>This is not visible to users unless they manually inspect the room's create event source.</span> + </ModalWindow> +} + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private AuthenticatedHomeserverGeneric.CapabilitiesResponse? Capabilities { get; set; } + + private bool ShowAttributionInfo { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool AllowAttribution { + get; + set { + field = value; + _ = LocalStorage.SetItemAsync("rmu.room_create.allow_attribution", value); + } + } = true; + + protected override async Task OnInitializedAsync() { + Capabilities = await Homeserver.GetCapabilitiesAsync(); + roomBuilder.Version = Capabilities.Capabilities.RoomVersions!.Default; + AllowAttribution = await LocalStorage.GetItemAsync<bool?>("rmu.room_create.allow_attribution") ?? true; + } + + private static Dictionary<string, string> RoomTypes { get; } = new() { + { "", "Room" }, + { "m.space", "Space" }, + { "support.feline.policy.lists.msc.v1", "Policy list" } + }; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor new file mode 100644
index 0000000..99facbf --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
@@ -0,0 +1,83 @@ +@using System.Text.Json +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.Helpers +<tr> + <td style="vertical-align: top;">Initial room state:</td> + <td> + @foreach (var (displayName, events) in new Dictionary<string, List<StateEvent>>() { + { "Important room state (before final access rules)", roomBuilder.ImportantState }, + { "Additional room state (after final access rules)", roomBuilder.InitialState }, + }) { + <details open> + + @code + { + // private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; + } + + @* <summary>@displayName: @events.Count(x => !ImplementedStates.Contains(x.Type)) events</summary> *@ + <summary>@displayName: @events.Count events</summary> + <LinkButton OnClick="@(() => { + events.Clear(); + StateHasChanged(); + })">Remove all + </LinkButton> + <LinkButton OnClick="@(() => { + events.Insert(0, new() { + Type = "", + StateKey = "", + RawContent = new(), + }); + StateHasChanged(); + })">Add new event + </LinkButton> + <br/> + @if (events.Count > 1000) { + <span style="color: red;">Warning: Too many initial state events! (more than 1000) - Please use the save/load feature in the state panel instead.</span> + } + else { + int i = 0; + @foreach (var initialState in events) { + <div id="@(initialState.Type + "/" + initialState.StateKey)"> + <span>Event @(++i) (@GetRemoveButton(events, initialState))</span> + <br/> + @* <FancyTextBox Multiline="true" Value="@initialState.ToJson(ignoreNull: true)" *@ + @* ValueChanged="@(json => { *@ + @* if (string.IsNullOrWhiteSpace(json)) *@ + @* events.Remove(initialState); *@ + @* else *@ + @* events.Replace(initialState, JsonSerializer.Deserialize<StateEvent>(json)); *@ + @* StateHasChanged(); *@ + @* })"></FancyTextBox> *@ + <FancyTextBoxLazyJson T="StateEvent" Value="@initialState" ValueChanged="@(evt => { events.Replace(initialState, evt); })"></FancyTextBoxLazyJson> + <br/> + </div> + } + } + </details> + } + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private RenderFragment GetRemoveButton(List<StateEvent> events, StateEvent initialState) { + return @<span> + <LinkButton InlineText="true" OnClick="@(() => { + events.Remove(initialState); + PageStateHasChanged(); + })">Remove</LinkButton> + </span>; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor new file mode 100644
index 0000000..6e300d4 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor
@@ -0,0 +1,60 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +<tr> + <td>Invited members:</td> + <td> + <details> + <summary>@roomBuilder.Invites.Count members</summary> + <LinkButton OnClickAsync="@InviteAllSessions" InlineText="true">Invite all logged in accounts</LinkButton> + <br/> + @foreach (var member in roomBuilder.Invites) { + <FancyTextBox Value="@member.Key" ValueChanged="@(val => roomBuilder.Invites.ChangeKey(member.Key, val))"/> + @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@ + <span>: </span> + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Invites[member.Key] = val)"/> + <br/> + } + </details> + </td> +</tr> +<tr> + <td>Banned members:</td> + <td> + <details> + <summary>@roomBuilder.Bans.Count members</summary> + <br/> + @foreach (var member in roomBuilder.Bans) { + @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@ + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans.ChangeKey(member.Key, val))"/> + <span>: </span> + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans[member.Key] = val)"/> + } + </details> + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private async Task InviteAllSessions() { + var sessions = await sessionStore.GetAllSessions(); + foreach (var session in sessions) { + if (roomBuilder.Invites.ContainsKey(session.Value.Auth.UserId) || session.Value.Auth.UserId == Homeserver!.WhoAmI.UserId) continue; + Console.WriteLine("Inviting " + session.Value.Auth.UserId); + roomBuilder.Invites.Add(session.Value.Auth.UserId, null); + Console.WriteLine("--"); + } + + Console.WriteLine("Got all sessions, invited: " + string.Join(", ", roomBuilder.Invites.Keys)); + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor new file mode 100644
index 0000000..94e9638 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor
@@ -0,0 +1,19 @@ +@using LibMatrix.Helpers +<div style="border-left: solid 1px white; padding-left: 8px; margin-left: 8px;"> + <span>Policy list upgrade type:</span> + <InputSelect @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.UpgradeType"> + <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Move">Move policy list (copy policies)</option> + <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition">Transition policy list (new list)</option> + </InputSelect> + <br/> +</div> + +@code { + + [Parameter] + public required RoomUpgradeBuilder roomUpgrade { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor new file mode 100644
index 0000000..ba28b82 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor
@@ -0,0 +1,123 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +<tr> + <td>Permissions:</td> + <details> + <summary> + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + <span>@(roomBuilder.AdditionalCreators.Count + 1) creators, </span> + } + <span>@roomBuilder.PowerLevels.Users.Count members, @roomBuilder.PowerLevels.Events.Count events</span> + </summary> + + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + <span style="border-bottom: #444;">Creators:</span> + <br/> + <span>@Homeserver.WhoAmI.UserId (you - to change, visit <a href="/">the homepage</a>.)</span> + <br/> + + <StringListEditor @bind-Items="@roomBuilder.AdditionalCreators"></StringListEditor> + <br/> + } + + <span style="border-bottom: #444;">Events:</span><br/> + @foreach (var eventType in roomBuilder.PowerLevels.Events.Keys) { + var _event = eventType; + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Events.Remove(_event); + StateHasChanged(); + })">- + </LinkButton> + <div style="display: inline-flex;"> + <FancyTextBox Formatter="@GetPermissionFriendlyName" + Value="@_event" + ValueChanged="val => { roomBuilder.PowerLevels.Events.ChangeKey(_event, val); }"> + </FancyTextBox> + <span>:</span> + </div> + </td> + <td> + <input type="number" value="@roomBuilder.PowerLevels.Events[_event]" + @oninput="val => { roomBuilder.PowerLevels.Events[_event] = int.Parse(val.Value.ToString()); }" + @onfocusout="@(() => { roomBuilder.PowerLevels.Events = roomBuilder.PowerLevels.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/> + </td> + </tr> + } + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Events[""] = 0; + StateHasChanged(); + })">+ + </LinkButton> + </td> + </tr> + + <span style="border-bottom: #444;">Users:</span><br/> + @foreach (var user in roomBuilder.PowerLevels.Users.Keys) { + var _user = user; + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Users.Remove(_user); + StateHasChanged(); + })">- + </LinkButton> + <div style="display: inline-flex;"> + <FancyTextBox Value="@_user" + ValueChanged="val => { roomBuilder.PowerLevels.Users.ChangeKey(_user, val); }"> + </FancyTextBox> + <span>:</span> + </div> + </td> + <td> + <input type="number" value="@roomBuilder.PowerLevels.Users[_user]" + @oninput="val => { roomBuilder.PowerLevels.Users[_user] = int.Parse(val.Value.ToString()); }" + @onfocusout="@(() => { roomBuilder.PowerLevels.Users = roomBuilder.PowerLevels.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/> + </td> + </tr> + } + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Users[""] = 0; + StateHasChanged(); + })">+ + </LinkButton> + </td> + </tr> + </details> +</tr> + + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private string GetPermissionFriendlyName(string key) => key switch { + "m.reaction" => "Send reaction", + "m.room.avatar" => "Change room icon", + "m.room.canonical_alias" => "Change room alias", + "m.room.encryption" => "Enable encryption", + "m.room.history_visibility" => "Change history visibility", + "m.room.name" => "Change room name", + "m.room.power_levels" => "Change power levels", + "m.room.tombstone" => "Upgrade room", + "m.room.topic" => "Change room topic", + "m.room.pinned_events" => "Pin events", + "m.room.server_acl" => "Change server ACLs", + "org.matrix.msc4284.policy" => "Change policy server", + "m.room.guest_access" => "Change guest access", + _ => key + }; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor new file mode 100644
index 0000000..76f61c4 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor
@@ -0,0 +1,70 @@ +@using LibMatrix.Helpers +<tr> + <td style="padding-top: 16px;">Join rules:</td> + <td style="padding-top: 16px;"> + <InputSelect @bind-Value="@roomBuilder.JoinRules.JoinRuleValue"> + <option value="public">Anyone can join</option> + <option value="invite">Invite only</option> + <option value="knock">Ask to join</option> + <option value="restricted">Invite only (or mutual room)</option> + <option value="knock_restricted">Ask to join (or mutual room)</option> + </InputSelect> + </td> +</tr> +<tr> + <td>History visibility:</td> + <td> + <InputSelect @bind-Value="@roomBuilder.HistoryVisibility.HistoryVisibility"> + <option value="invited">Since invite</option> + <option value="joined">Since join</option> + <option value="shared">Since room creation (members only)</option> + <option value="world_readable">World readable (everyone)</option> + </InputSelect> + </td> +</tr> +<tr> + <td>Guest access:</td> + <td> + <InputCheckbox @bind-Value="roomBuilder.GuestAccess.IsGuestAccessEnabled"/> + <span>Allow guests to join</span> + <LinkButton InlineText="true" href="https://spec.matrix.org/v1.15/client-server-api/#guest-access" target="_blank">?</LinkButton> + </td> +</tr> +<tr> + <td>Server ACLs:</td> + <td> + @if (roomBuilder.ServerAcls?.Allow is null) { + <p>No allow rules exist!</p> + <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Allow = ["*"]; })">Create sane defaults</LinkButton> + } + else { + <details> + <summary>@(roomBuilder.ServerAcls.Allow?.Count) allow rules</summary> + <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Allow"></StringListEditor> + </details> + } + @if (roomBuilder.ServerAcls?.Deny is null) { + <p>No deny rules exist!</p> + <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Deny = []; })">Create sane defaults</LinkButton> + } + else { + <details> + <summary>@(roomBuilder.ServerAcls.Deny?.Count) deny rules</summary> + <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Deny"></StringListEditor> + </details> + } + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor new file mode 100644
index 0000000..eb373ba --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor
@@ -0,0 +1,65 @@ +@using System.Text.Json +@using System.Text.Json.Nodes +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +@inject BlazorSaveFileService SaveFileService +<div + style="position: fixed; top: 56px; right: 0; width: fit-content; max-width: 25%; height: calc(100vh - 56px); overflow: auto; background-color: #2c3054; padding-right: 32px; border-left: 1px solid #ccc;"> + <details open> + <summary>RoomBuilder state</summary> + <InputCheckbox @bind-Value="@ShowNullInState"/> + <span>Show null values</span><br/> + <LinkButton OnClickAsync="@SaveFile">Save</LinkButton> + <SimpleFilePicker OnFilePicked="@LoadFile"/> + <br/> + <pre> + @RoomBuilder.ToJson(ignoreNull: !ShowNullInState) + </pre> + </details> +</div> + +@code { + + [Parameter] + public required RoomBuilder RoomBuilder { get; set; } + + [Parameter] + public required EventCallback<RoomBuilder> RoomBuilderChanged { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + private bool ShowNullInState { get; set; } + + private async Task SaveFile() { + Console.WriteLine("Saving room builder state to file..."); + await SaveFileService.SaveFileAsync("room-builder.json", RoomBuilder.ToJson(), "application/json"); + } + + private async Task LoadFile(InputFileChangeEventArgs e) { + if (!RoomBuilderChanged.HasDelegate) throw new InvalidOperationException("RoomBuilderChanged must have a delegate."); + if (e.FileCount == 0) return; + Console.WriteLine("Loading room builder state from file..."); + var stream = e.File.OpenReadStream(4 * 1024 * 1024 * 1024L); + var json = await JsonSerializer.DeserializeAsync<JsonObject>(stream); + if (json is null) { + Console.WriteLine("Failed to deserialize JSON from file."); + return; + } + + if (json.ContainsKey(nameof(RoomUpgradeBuilder.UpgradeOptions))) { + Console.WriteLine("Got room upgrade builder state."); + RoomBuilder = json.Deserialize<RoomUpgradeBuilder>(); + } + else { + Console.WriteLine("Got room builder state."); + RoomBuilder = json.Deserialize<RoomBuilder>(); + } + + await RoomBuilderChanged.InvokeAsync(RoomBuilder); + PageStateHasChanged(); + Console.WriteLine("Room builder state loaded from file."); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor new file mode 100644
index 0000000..d4c4bfe --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor
@@ -0,0 +1,51 @@ +@using LibMatrix.Helpers +@using LibMatrix.RoomTypes +<tr> + <td>Room upgrade options</td> + <td> + @* <details> *@ + @* <summary>Upgrading from @roomUpgrade.OldRoom.RoomId</summary> *@ + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InviteMembers"></InputCheckbox> + <span>Invite members</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InvitePowerlevelUsers"></InputCheckbox> + <span>Invite users with powerlevels</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateBans"></InputCheckbox> + <span>Copy bans (do not use with moderation bots!)</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateEmptyStateEvents"></InputCheckbox> + <span>Include empty state events</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.UpgradeUnstableValues"></InputCheckbox> + <span>Update unstable namespaced values to spec versions (experimental)</span> + <br/> + @if (roomUpgrade.Type == "support.feline.policy.lists.msc.v1") { + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable"></InputCheckbox> + <span>Enable MSC4321 support</span> + <br/> + @if (roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable) { + <RoomCreateMsc4321UpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@PageStateHasChanged"/> + } + } + <LinkButton OnClickAsync="@(async () => { + await roomUpgrade.ImportAsync(OldRoom); + PageStateHasChanged(); + })">Apply + </LinkButton> + @* </details> *@ + </td> +</tr> + +@code { + + [Parameter] + public required GenericRoom OldRoom { get; set; } + + [Parameter] + public required RoomUpgradeBuilder roomUpgrade { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 01ab1c4..fc9c9bf 100644 --- a/MatrixUtils.Web/Pages/Rooms/Space.razor +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -2,11 +2,15 @@ @using LibMatrix.RoomTypes @using ArcaneLibs.Extensions @using LibMatrix +@using MatrixUtils.Abstractions <h3>Room manager - Viewing Space</h3> +<span>Add new room to space: </span> +<FancyTextBox @bind-Value="@NewRoomId"></FancyTextBox> +<button onclick="@AddNewRoom">Add</button> <button onclick="@JoinAllRooms">Join all rooms</button> @foreach (var room in Rooms) { - <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem> + <RoomListItem RoomInfo="room" ShowOwnProfile="true"></RoomListItem> } @@ -27,11 +31,12 @@ private GenericRoom? Room { get; set; } private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>(); - private List<GenericRoom> Rooms { get; } = new(); + private List<RoomInfo> Rooms { get; } = new(); private List<string> ServersInSpace { get; } = new(); + private string? NewRoomId { get; set; } protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; Room = hs.GetRoom(RoomId.Replace('~', '.')); @@ -43,7 +48,18 @@ var roomId = stateEvent.StateKey; var room = hs.GetRoom(roomId); if (room is not null) { - Rooms.Add(room); + Task.Run(async () => { + try { + Rooms.Add(new(Room, await room.GetFullStateAsListAsync())); + } + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) { + Rooms.Add(new(Room) { + RoomName = "M_FORBIDDEN" + }); + } + } + }); } break; } @@ -96,8 +112,38 @@ // List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList(); // await Task.WhenAll(tasks); foreach (var room in Rooms) { - await room.JoinAsync(ServersInSpace.ToArray()); + await JoinRecursive(room.Room.RoomId); } } + private async Task JoinRecursive(string roomId) { + var room = Room!.Homeserver.GetRoom(roomId); + if (room is null) return; + try { + await room.JoinAsync(ServersInSpace.Take(10).ToArray()); + var joined = false; + while (!joined) { + var ce = await room.GetCreateEventAsync(); + if(ce is null) continue; + if (ce.Type == "m.space") { + var children = room.AsSpace().GetChildrenAsync(false); + await foreach (var child in children) { + JoinRecursive(child.RoomId); + } + } + joined = true; + await Task.Delay(1000); + } + } + catch (Exception e) { + Console.WriteLine(e); + } + + } + + private async Task AddNewRoom() { + if (string.IsNullOrWhiteSpace(NewRoomId)) return; + await Room.AsSpace().AddChildByIdAsync(NewRoomId); + } + } diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 6110b83..51cb265 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -43,7 +43,7 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; RoomId = RoomId.Replace('~', '.'); await LoadStatesAsync(); @@ -53,7 +53,7 @@ private DateTime _lastUpdate = DateTime.Now; private async Task LoadStatesAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); var StateLoaded = 0; var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
index 7c31136..c8b87d3 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
@@ -70,7 +70,7 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; await LoadStatesAsync(); Console.WriteLine("Policy list editor initialized!"); @@ -80,7 +80,7 @@ private async Task LoadStatesAsync() { var StateLoaded = 0; - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); await foreach (var _ev in response) { diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
index e6b1248..2af819b 100644 --- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -2,7 +2,8 @@ @using MatrixUtils.Web.Shared.TimelineComponents @using LibMatrix @using LibMatrix.EventTypes.Spec -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Responses <h3>RoomManagerTimeline</h3> <hr/> <p>Loaded @Events.Count events...</p> @@ -27,7 +28,7 @@ protected override async Task OnInitializedAsync() { Console.WriteLine("RoomId: " + RoomId); - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; var room = Homeserver.GetRoom(RoomId); MessagesResponse? msgs = null; diff --git a/MatrixUtils.Web/Pages/ServerInfo.razor b/MatrixUtils.Web/Pages/ServerInfo.razor
index e6f1f16..3da93f2 100644 --- a/MatrixUtils.Web/Pages/ServerInfo.razor +++ b/MatrixUtils.Web/Pages/ServerInfo.razor
@@ -1,6 +1,7 @@ @page "/ServerInfo/{Homeserver}" @using LibMatrix.Responses @using ArcaneLibs.Extensions +@using LibMatrix.Responses.Federation <h3>Server info for @Homeserver</h3> <hr/> @if (ServerVersionResponse is not null) { @@ -78,7 +79,7 @@ protected override async Task OnParametersSetAsync() { if (Homeserver is not null) { - var rhs = await hsProvider.GetRemoteHomeserver(Homeserver); + var rhs = await HsProvider.GetRemoteHomeserver(Homeserver); ServerVersionResponse = await (rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)); ClientVersionsResponse = await rhs.GetClientVersionsAsync(); } diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor new file mode 100644
index 0000000..7740596 --- /dev/null +++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -0,0 +1,119 @@ +@page "/StreamTest" +@inject ILogger<Index> logger +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo + +<PageTitle>StreamText</PageTitle> +@if (Homeserver is not null) { + <p>Got homeserver @Homeserver.ServerName</p> + + @* <img src="@ResolvedUri" @ref="imgElement"/> *@ + @* <StreamedImage Stream="@Stream"/> *@ + + <br/> + @foreach (var stream in Streams.OrderBy(x => x.GetHashCode())) { + <StreamedImage Stream="@stream" style="width: 12em; height: 12em; object-fit: cover;"/> + } +} + +@code +{ + private string? _resolvedUri; + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private string? ResolvedUri { + get => _resolvedUri; + set { + _resolvedUri = value; + StateHasChanged(); + } + } + + ElementReference imgElement { get; set; } + public Stream? Stream { get; set; } + public List<Stream> Streams { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + + //await InitOld(); + await Init2(); + + await base.OnInitializedAsync(); + } + + private async Task Init2() { + var roomState = await Homeserver.GetRoom("!dSMpkVKGgQHlgBDSpo:matrix.org").GetFullStateAsListAsync(); + var members = roomState.Where(x => x.Type == RoomMemberEventContent.EventId).ToList(); + Console.WriteLine($"Got {members.Count()} members"); + var ss = new SemaphoreSlim(1, 1); + foreach (var stateEventResponse in members) { + // Console.WriteLine(stateEventResponse.ToJson()); + var mc = stateEventResponse.TypedContent as RoomMemberEventContent; + if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) { + var uri = mc.AvatarUrl[6..].Split('/'); + var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}"; + // Homeserver.GetMediaStreamAsync(mc?.AvatarUrl).ContinueWith(async x => { + // await ss.WaitAsync(); + // var stream = x.Result; + // Streams.Add(stream); + // StateHasChanged(); + await Task.Delay(100); + // ss.Release(); + // }); + try { + Homeserver.ClientHttpClient.GetStreamAsync(url).ContinueWith(async x => { + // await ss.WaitAsync(); + var stream = x.Result; + Streams.Add(stream); + StateHasChanged(); + // await Task.Delay(100); + // ss.Release(); + }); + } + catch (Exception e) { + Console.WriteLine(e); + } + } + } + } + + private async Task InitOld() { + // var value = "mxc://rory.gay/AcFYcSpVXhEwbejrPVQrRUqt"; + // var value = "mxc://rory.gay/oqfCjIUVTAObSQbnMFekQvYR"; + var value = "mxc://feline.support/LUslNRVIYfeyCdRElqkkumKP"; + var uri = value[6..].Split('/'); + var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}"; + // var res = Homeserver.ClientHttpClient.GetAsync(url); + // var res2 = Homeserver.ClientHttpClient.GetAsync(url); + // var tasks = Enumerable.Range(1, 128) + // .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url+$"?width={x*128}&height={x*128}")) + // .ToAsyncResultEnumerable(); + await foreach (var result in GetStreamsDelayed(url)) { + Streams.Add(result); + // await Task.Delay(100); + StateHasChanged(); + } + + // var stream = await (await res).Content.ReadAsStreamAsync(); + // Stream = await (await res2).Content.ReadAsStreamAsync(); + StateHasChanged(); + + // await JSRuntime.streamImage(stream, imgElement); + } + + private async IAsyncEnumerable<Stream> GetStreamsDelayed(string url) { + for (int i = 0; i < 32; i++) { + var tasks = Enumerable.Range(1, 4) + .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url + $"?width={x * 128}&height={x * 128}&r={Random.Shared.Next(100000)}")) + .ToAsyncResultEnumerable(); + await foreach (var result in tasks) { + yield return result; + } + // var resp = await Homeserver.ClientHttpClient.GetAsync(url + $"?width={i * 128}&height={i * 128}"); + // yield return await resp.Content.ReadAsStreamAsync(); + // await Task.Delay(250); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor new file mode 100644
index 0000000..cb56a40 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor
@@ -0,0 +1,70 @@ +@page "/Tools/Debug/JoinRoom" +@using System.Collections.ObjectModel +<h3>Join room</h3> +<hr/> +<span>Room ID: </span> +<InputText @bind-Value="@RoomId"></InputText> +<br/> +<span>Via server(s), comma separated: </span> +<InputText @bind-Value="@Servers"></InputText> +<br/> +<span>Unblock room (Synapse): </span> +<InputCheckbox @bind-Value="@Unblock"></InputCheckbox> +<br/> +<LinkButton OnClickAsync="@Join">Join</LinkButton> +<br/><br/> +@foreach (var line in Log) { + <pre>@line</pre> + <br/> +} + +@code { + AuthenticatedHomeserverGeneric? hs { get; set; } + ObservableCollection<string> Log { get; set; } = new ObservableCollection<string>(); + + [Parameter, SupplyParameterFromQuery(Name = "roomId")] + public string? RoomId { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "via")] + public string? Servers { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "unblock")] + public bool Unblock { get; set; } = false; + + protected override async Task OnInitializedAsync() { + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + Log.CollectionChanged += (sender, args) => StateHasChanged(); + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task Join() { + if (string.IsNullOrWhiteSpace(RoomId)) return; + var room = hs.GetRoom(RoomId); + Log.Add("Got room object..."); + + if (Unblock && hs is AuthenticatedHomeserverSynapse synapse) { + try { + await synapse.Admin.BlockRoom(RoomId, false); + Log.Add($"Synapse: unblocked room"); + } + catch (Exception e) { + Log.Add($"Synapse: failed to unblock room: {e}"); + } + } + + try { + await room.JoinAsync(Servers?.Split(','), checkIfAlreadyMember: false); + Log.Add("Joined room!"); + } + catch (Exception e) { + Log.Add(e.ToString()); + } + + Log.Add("Done!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
index 841552e..c40fa0b 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
@@ -1,11 +1,11 @@ -@page "/Tools/LeaveRoom" +@page "/Tools/Debug/LeaveRoom" @using System.Collections.ObjectModel <h3>Leave room</h3> <hr/> <span>Room ID: </span> <InputText @bind-Value="@RoomId"></InputText> <br/> -<LinkButton OnClick="@Leave">Leave</LinkButton> +<LinkButton OnClickAsync="@Leave">Leave</LinkButton> <br/><br/> @foreach (var line in Log) { <p>@line</p> @@ -17,7 +17,7 @@ public string? RoomId { get; set; } protected override async Task OnInitializedAsync() { - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; Log.CollectionChanged += (sender, args) => StateHasChanged(); diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
index 6e87926..dd8a801 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
@@ -92,7 +92,7 @@ lines.ToList().ForEach(async line => { await sem.WaitAsync(); try { - homeservers.Add((await hsResolver.ResolveHomeserverFromWellKnown(line)).Client); + homeservers.Add((await HsResolver.ResolveHomeserverFromWellKnown(line)).Client); StateHasChanged(); } catch (Exception e) { diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 11d35f1..067036e 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -17,7 +17,7 @@ </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -39,7 +39,7 @@ private string newRoomId { get; set; } protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; StateHasChanged(); @@ -48,13 +48,13 @@ } private async Task Execute() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; var oldRoom = hs.GetRoom(roomId); var newRoom = hs.GetRoom(newRoomId); var members = await oldRoom.GetMembersListAsync(); - var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncEnumerable(); - // var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable(); + var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncResultEnumerable(); + // var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable(); await foreach (var a in tasks) { if (!string.IsNullOrWhiteSpace(a)) { log.Add(a); @@ -90,7 +90,7 @@ private async Task TryFetchUsers() { try { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; var room = hs.GetRoom(roomId); var members = await room.GetMembersListAsync(); diff --git a/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
index 263879b..7abb3d2 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
@@ -45,7 +45,7 @@ protected override async Task OnInitializedAsync() { Status = "Getting homeserver..."; - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; var syncHelper = new SyncHelper(hs) { diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index e68bb9a..a0abcd4 100644 --- a/MatrixUtils.Web/Pages/Tools/Index.razor +++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -12,6 +12,7 @@ <a href="/Tools/User/MassRoomJoin">Join room across all session</a><br/> <a href="/Tools/User/CopyPowerlevel">Copy highest powerlevel across all session</a><br/> <a href="/Tools/User/ViewAccountData">View account data</a><br/> +<a href="/Tools/User/StickerManager">Manage custom stickers and emojis</a><br/> <h4 class="tool-category">Room tools</h4> <hr/> @@ -24,12 +25,13 @@ <a href="/Tools/Moderation/UserTrace">Trace user across rooms</a><br/> <a href="/tools/Moderation/MassCMEBan">Mass write policies to Community Moderation Effort</a><br/> <a href="/tools/Moderation/RoomIntersections">Find rooms with common users</a><br/> -<a href="/tools/Moderation/DraupnirProtectedRoomsEditor">Edit Draupnir protected rooms set</a><br/> +<a href="/tools/Moderation/Draupnir/ProtectedRoomsEditor">Draupnir: edit protected rooms set</a><br/> <h4 class="tool-category">Debugging tools</h4> <hr/> <a href="/Tools/Debug/SpaceDebug">Debug space relationships</a><br/> +<a href="/Tools/Debug/JoinRoom">Join room by ID</a><br/> <a href="/Tools/Debug/LeaveRoom">Leave room by ID</a><br/> <a href="/Tools/Debug/MediaLocator">Locate lost media</a><br/> <a href="/Tools/Debug/MigrateRoom">Migrate users from a split room to a new room</a><br/> diff --git a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
index ddd7b15..8ba160a 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
@@ -1,45 +1,73 @@ -@page "/Tools/KnownHomeserverList" +@page "/Tools/Info/KnownHomeserverList" @using ArcaneLibs.Extensions +@using LibMatrix.RoomTypes +@using SpawnDev.BlazorJS.WebWorkers +@inject WebWorkerService workerService <h3>Known Homeserver List</h3> <hr/> @if (!IsFinished) { <p> - <b>Loading...</b> + <b>Loading... @RoomCount rooms remaining to process...</b> </p> } -@foreach (var (homeserver, members) in counts.OrderByDescending(x => x.Value)) { - <p>@homeserver - @members</p> +@{ + var shownCounts = counts.OrderByDescending(x => x.Value).AsEnumerable(); + if (!IsFinished && counts.Count > 500) { + shownCounts = shownCounts.Where(x => x.Value > 5); + } +} +@foreach (var (homeserver, members) in shownCounts.ToList()) { + <p>@homeserver - @members users</p> } <hr/> @code { Dictionary<string, List<string>> homeservers { get; set; } = new(); + Dictionary<string, int> counts { get; set; } = new(); + // List<HomeserverInfo> Homeservers = new(); bool IsFinished { get; set; } + // HomeserverInfoQueryProgress QueryProgress { get; set; } = new(); AuthenticatedHomeserverGeneric? hs { get; set; } + int RoomCount { get; set; } = 0; protected override async Task OnInitializedAsync() { - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - var fetchTasks = (await hs.GetJoinedRooms()).Select(x=>x.GetMembersByHomeserverAsync()).ToAsyncEnumerable(); + var ss = new SemaphoreSlim(32, 32); + var rooms = await hs.GetJoinedRooms(); + RoomCount = rooms.Count; + var fetchTasks = rooms.Select(roomId => workerService.TaskPool.Invoke(() => InternalGetMembersByHomeserver(hs.WellKnownUris.Client, hs.AccessToken, roomId.RoomId))).ToList().ToAsyncResultEnumerable(); + // var fetchTasks = rooms.Select(async x => { + // await ss.WaitAsync(); + // var res = await x.GetMembersByHomeserverAsync(); + // ss.Release(); + // return res; + // }).ToAsyncResultEnumerable(); await foreach (var result in fetchTasks) { foreach (var (resHomeserver, resMembers) in result) { if (!homeservers.TryAdd(resHomeserver, resMembers)) { homeservers[resHomeserver].AddRange(resMembers); } + counts[resHomeserver] = homeservers[resHomeserver].Count; } - // StateHasChanged(); + + RoomCount--; + StateHasChanged(); // await Task.Delay(250); + await Task.Yield(); } foreach (var resHomeserver in homeservers.Keys) { homeservers[resHomeserver] = homeservers[resHomeserver].Distinct().ToList(); counts[resHomeserver] = homeservers[resHomeserver].Count; + StateHasChanged(); + await Task.Yield(); } IsFinished = true; @@ -48,4 +76,10 @@ await base.OnInitializedAsync(); } + private static async Task<Dictionary<string, List<string>>> InternalGetMembersByHomeserver(string homeserverBaseUrl, string accessToken, string roomId) { + var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken); + var room = hs.GetRoom(roomId); + return await room.GetMembersByHomeserverAsync(); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index de0bfe7..bfd5fd3 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -1,24 +1,24 @@ @page "/Tools/Info/PolicyListActivity" @using LibMatrix.EventTypes.Spec.State.Policy @using System.Diagnostics +@using System.Reflection +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes @using LibMatrix.RoomTypes @using LibMatrix.EventTypes.Common +@using LibMatrix.Filters - -@if (RoomData.Count == 0) -{ +@* <ActivityGraph Data="TestData"/> *@ +@if (RoomData.Count == 0) { <p>Loading...</p> } else - foreach (var room in RoomData) - { + foreach (var room in RoomData) { <h3>@room.Key</h3> - @foreach (var year in room.Value.OrderBy(x => x.Key)) - { - <h5>@year.Key</h5> - <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" - RLabel="removed" GLabel="new" BLabel="updated policies"> - </ActivityGraph> + @foreach (var year in room.Value.OrderBy(x => x.Key)) { + <span>@year.Key</span> + <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" RLabel="removed" GLabel="new" BLabel="updated policies"/> } } @@ -29,59 +29,26 @@ else public Dictionary<DateOnly, ActivityGraph.RGB> TestData { get; set; } = new(); - public ActivityGraph.RGB MaxValue { get; set; } = new() - { + public ActivityGraph.RGB MaxValue { get; set; } = new() { R = 255, G = 255, B = 255 }; public Dictionary<string, Dictionary<int, Dictionary<DateOnly, ActivityGraph.RGB>>> RoomData { get; set; } = new(); - protected override async Task OnInitializedAsync() - { + protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); await base.OnInitializedAsync(); - Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!; + Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; if (Homeserver is null) return; - //random test data - for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(Random.Shared.Next(5))) - { - TestData[i] = new() - { - R = (int)(Random.Shared.NextSingle() * 255), - G = (int)(Random.Shared.NextSingle() * 255), - B = (int)(Random.Shared.NextSingle() * 255) - }; - } - - StateHasChanged(); - // return; - var rooms = await Homeserver.GetJoinedRooms(); - // foreach (var room in rooms) - // { - // var type = await room.GetRoomType(); - // if (type == "support.feline.policy.lists.msc.v1") - // { - // Console.WriteLine($"{room.RoomId} is policy list by type"); - // FilteredRooms.Add(room); - // } - // else if(await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) - // { - // Console.WriteLine($"{room.RoomId} is policy list by shortcode"); - // FilteredRooms.Add(room); - // } - // } - var roomFilterTasks = rooms.Select(async room => - { + var roomFilterTasks = rooms.Select(async room => { var type = await room.GetRoomType(); - if (type == "support.feline.policy.lists.msc.v1") - { + if (type == "support.feline.policy.lists.msc.v1") { Console.WriteLine($"{room.RoomId} is policy list by type"); return room; } - else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) - { + else if (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId) is not null) { Console.WriteLine($"{room.RoomId} is policy list by shortcode"); return room; } @@ -99,60 +66,74 @@ else Console.WriteLine($"Filtered {FilteredRooms.Count} rooms in {sw.ElapsedMilliseconds}ms"); } - public async Task FetchRoomHistory(GenericRoom room) - { + public async Task FetchRoomHistory(GenericRoom room) { var roomName = await room.GetNameOrFallbackAsync(); - if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId; - if (!RoomData.ContainsKey(roomName)) - { - RoomData[roomName] = new(); - } + if (string.IsNullOrWhiteSpace(roomName)) roomName = room.RoomId; + if (!RoomData.ContainsKey(roomName)) { + RoomData[roomName] = new(); + } + + //use timeline + var types = StateEventResponse.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))); + var filter = new SyncFilter.EventFilter(types: types.SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName)).ToList()); + var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 2500, filter: filter.ToJson(indent: false, ignoreNull: true)); + await foreach (var response in timeline) { + Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline"); + if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?"); + foreach (var message in response.Chunk) { + if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + //OriginServerTs to datetime + var dt = DateTimeOffset.FromUnixTimeMilliseconds(message.OriginServerTs!.Value).DateTime; + var date = new DateOnly(dt.Year, dt.Month, dt.Day); + if (!RoomData[roomName].ContainsKey(date.Year)) { + RoomData[roomName][date.Year] = new(); + } - //use timeline - var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000); - await foreach (var response in timeline) - { - Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline"); - if (response.State.Count != 0) throw new Exception("Why the hell did we receive state events?"); - foreach (var message in response.Chunk) - { - if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; - //OriginServerTs to datetime - var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime; - var date = new DateOnly(dt.Year, dt.Month, dt.Day); - if (!RoomData[roomName].ContainsKey(date.Year)) - { - RoomData[roomName][date.Year] = new(); - } - - if (!RoomData[roomName][date.Year].ContainsKey(date)) - { - // Console.WriteLine($"Adding {date} to {roomName}"); - RoomData[roomName][date.Year][date] = new(); - } - - var rgb = RoomData[roomName][date.Year][date]; - if (message.RawContent?.Count == 0) rgb.R++; - else if (string.IsNullOrWhiteSpace(message.Unsigned?.ReplacesState)) rgb.G++; - else rgb.B++; - RoomData[roomName][date.Year][date] = rgb; + if (!RoomData[roomName][date.Year].ContainsKey(date)) { + // Console.WriteLine($"Adding {date} to {roomName}"); + RoomData[roomName][date.Year][date] = new(); } - var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() - { - R = Math.Max(current.R, next.Average(x => x.Value.R)), - G = Math.Max(current.G, next.Average(x => x.Value.G)), - B = Math.Max(current.B, next.Average(x => x.Value.B)) - }); - MaxValue = new ActivityGraph.RGB( - r: Math.Max(max.R, Math.Max(max.G, max.B)), - g: Math.Max(max.R, Math.Max(max.G, max.B)), - b: Math.Max(max.R, Math.Max(max.G, max.B))); - Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); - StateHasChanged(); - await Task.Delay(100); + var rgb = RoomData[roomName][date.Year][date]; + if (message.RawContent is { Count: 0 } or null) rgb.R++; + else if (!message.Unsigned?.ContainsKey("replaces_state") ?? true) rgb.G++; + else rgb.B++; + RoomData[roomName][date.Year][date] = rgb; } + } + + var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() { + R = Math.Max(current.R, next.Average(x => x.Value.R)), + G = Math.Max(current.G, next.Average(x => x.Value.G)), + B = Math.Max(current.B, next.Average(x => x.Value.B)) + }); + MaxValue = new ActivityGraph.RGB( + r: Math.Max(max.R, Math.Max(max.G, max.B)), + g: Math.Max(max.R, Math.Max(max.G, max.B)), + b: Math.Max(max.R, Math.Max(max.G, max.B))); + Console.WriteLine($"Max value is {MaxValue.R} {MaxValue.G} {MaxValue.B}"); + StateHasChanged(); + await Task.Yield(); } + private readonly struct StateEventEntry { + public required DateTime Timestamp { get; init; } + public required StateEventTransition State { get; init; } + public required StateEventResponse Event { get; init; } + public required StateEventResponse? Previous { get; init; } + + public void Deconstruct(out StateEventTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + transition = State; + evt = Event; + prev = Previous; + } + } + + private enum StateEventTransition : byte { + None, + Add, + Update, + Remove + } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index 3b68bfa..dc5333b 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -4,14 +4,15 @@ @using System.Collections.ObjectModel @using LibMatrix @using System.Collections.Frozen -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>User Trace</h3> <hr/> <p>Users: </p> <InputTextArea @bind-Value="@UserIdString"></InputTextArea> <br/> -<InputText @bind-Value="@ImportFromRoomId"></InputText><LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton> +<InputText @bind-Value="@ImportFromRoomId"></InputText> +<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton> <details> <summary>Rooms to be searched (@rooms.Count)</summary> @@ -21,7 +22,7 @@ } </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <details> @@ -44,9 +45,7 @@ @foreach (var (userId, events) in matches) { <p> <span>@userId.PadRight(col1Width)</span> - @foreach (var @event in events) { - -} + @foreach (var @event in events) { } </p> } </pre> @@ -73,20 +72,20 @@ protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; rooms.CollectionChanged += (sender, args) => StateHasChanged(); - var sessions = await RMUStorage.GetAllTokens(); + var sessions = await sessionStore.GetAllSessions(); foreach (var userAuth in sessions) { - var session = await RMUStorage.GetSession(userAuth); - if (session is not null) { - var sessionRooms = await session.GetJoinedRooms(); + var homeserver = await sessionStore.GetHomeserver(userAuth.Key); + if (homeserver is not null) { + var sessionRooms = await homeserver.GetJoinedRooms(); foreach (var room in sessionRooms) { rooms.Add(room); } StateHasChanged(); - log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}"); + log.Add($"Got {sessionRooms.Count} rooms for {userAuth.Value.Auth.UserId}"); } } @@ -97,7 +96,7 @@ rooms = new ObservableCollection<GenericRoom>(distinctRooms); rooms.CollectionChanged += (sender, args) => StateHasChanged(); - var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync(false))).ToAsyncEnumerable(); + var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync())).ToAsyncResultEnumerable(); await foreach (var (room, state) in stateTasks) { roomMembers.Add(room, state); @@ -106,7 +105,7 @@ log.Add($"Done fetching members!"); - UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x)); + UserIDs.RemoveAll(x => sessions.Any(y => y.Value.Auth.UserId == x)); StateHasChanged(); Console.WriteLine("Rerendered!"); diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
index 8f4b4dd..16a3853 100644 --- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -1,24 +1,21 @@ @page "/Tools/InviteCounter" -@using ArcaneLibs.Extensions -@using LibMatrix.RoomTypes @using System.Collections.ObjectModel -@using LibMatrix -@using System.Collections.Frozen -@using LibMatrix.EventTypes.Spec.State -@using MatrixUtils.Abstractions +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Filters <h3>User Trace</h3> <hr/> <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> <details> <summary>Results</summary> - @foreach (var (userId, events) in invites.OrderByDescending(x=>x.Value).ToList()) { + @foreach (var (userId, events) in invites.OrderByDescending(x => x.Value).ToList()) { <p>@userId: @events</p> } </details> @@ -32,16 +29,15 @@ private ObservableCollection<string> log { get; set; } = new(); private Dictionary<string, int> invites { get; set; } = new(); private AuthenticatedHomeserverGeneric hs { get; set; } - + [Parameter, SupplyParameterFromQuery(Name = "room")] public string roomId { get; set; } - protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -49,22 +45,21 @@ private async Task<string> Execute() { var room = hs.GetRoom(roomId); - var events = room.GetManyMessagesAsync(limit: int.MaxValue); + var filter = new SyncFilter.EventFilter(types: ["m.room.member"]); + var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(indent: false, ignoreNull: true)); await foreach (var resp in events) { var all = resp.State.Concat(resp.Chunk); foreach (var evt in all) { - if(evt.Type != RoomMemberEventContent.EventId) continue; + if (evt.Type != RoomMemberEventContent.EventId) continue; var content = evt.TypedContent as RoomMemberEventContent; - if(content.Membership != "invite") continue; - if(!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0; + if (content.Membership != "invite") continue; + if (!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0; invites[evt.Sender]++; } log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline"); } - - - + StateHasChanged(); return ""; diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
index cbbca9e..5b0f510 100644 --- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -1,19 +1,13 @@ @page "/Tools/MassCMEBan" -@using ArcaneLibs.Extensions -@using LibMatrix.RoomTypes @using System.Collections.ObjectModel -@using LibMatrix -@using System.Collections.Frozen -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.EventTypes.Spec.State.Policy -@using MatrixUtils.Abstractions <h3>User Trace</h3> <hr/> <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> @@ -33,7 +27,7 @@ protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; StateHasChanged(); diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor new file mode 100644
index 0000000..1ff97c8 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,138 @@ +@page "/Moderation/DraupnirProtectedRoomsEditor" +@page "/Tools/Moderation/DraupnirProtectedRoomsEditor" +@page "/Tools/Moderation/Draupnir/ProtectedRoomsEditor" +@using LibMatrix +@using LibMatrix.EventTypes.Interop.Draupnir +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.RoomTypes +<h3>Edit Draupnir protected rooms</h3> +<hr/> +<p><b>Note:</b> You will need to restart Draupnir after applying changes!</p> +<p>Minor note: This <i>should</i> also work with Mjolnir, but this hasn't been tested, and as such functionality cannot be guaranteed.</p> + +@if (data is not null) { + <div class="row"> + <div class="col-12"> + <details> + <summary>Currently protected room IDs</summary> + <ul> + @foreach (var room in data.Rooms) { + <li>@room</li> + } + </ul> + </details> + <hr/> + <h4>Tickyboxes</h4> + <table class="table"> + <thead> + <tr> + <th></th> @* Checkbox column *@ + <th>Kick?</th> @* PL > kick *@ + <th>Ban?</th> @* PL > ban *@ + <th>ACL?</th> @* PL > m.room.server_acls event *@ + <th>Room ID</th> + <th>Room name</th> + </tr> + </thead> + <tbody> + @foreach (var room in Rooms.OrderBy(x => x.RoomName)) { + <tr> + <td> + <input type="checkbox" @bind="room.IsProtected"/> + </td> + <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> + <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> + <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td> + <td>@room.Room.RoomId</td> + <td>@room.RoomName</td> + </tr> + } + </tbody> + </table> + </div> + </div> +} +<br/> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> + + +@code { + private DraupnirProtectedRoomsData data { get; set; } = new(); + private List<EditorRoomInfo> Rooms { get; set; } = new(); + private AuthenticatedHomeserverGeneric hs { get; set; } + + protected override async Task OnInitializedAsync() { + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId); + StateHasChanged(); + var tasks = (await hs.GetJoinedRooms()).Select(async room => { + var plTask = room.GetPowerLevelsAsync(); + var roomNameTask = room.GetNameOrFallbackAsync(); + var EditorRoomInfo = new EditorRoomInfo { + Room = room, + IsProtected = data.Rooms.Contains(room.RoomId), + RoomName = await roomNameTask, + PowerLevels = await plTask + }; + + Rooms.Add(EditorRoomInfo); + StateHasChanged(); + return Task.CompletedTask; + }).ToList(); + await Task.WhenAll(tasks); + await Task.Delay(500); + + foreach (var protectedRoomId in data.Rooms) { + if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue; + var room = hs.GetRoom(protectedRoomId); + var editorRoomInfo = new EditorRoomInfo { + Room = room, + IsProtected = true + }; + + try { + var pl = await room.GetPowerLevelsAsync(); + editorRoomInfo.PowerLevels = pl; + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}"); + } + + try { + editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync(); + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get name for {room.RoomId}: {e}"); + } + + try { + var membership = await room.GetStateEventOrNullAsync(hs.UserId); + if (membership is not null) { + editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}"; + } + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}"); + } + + Rooms.Add(editorRoomInfo); + } + + StateHasChanged(); + } + + private class EditorRoomInfo { + public GenericRoom Room { get; set; } + public bool IsProtected { get; set; } + public string RoomName { get; set; } + public RoomPowerLevelEventContent PowerLevels { get; set; } + } + + private async Task Apply() { + Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId))); + data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList(); + await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index 805bd40..9b0266c 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -1,7 +1,7 @@ -@page "/Moderation/DraupnirProtectedRoomsEditor" -@page "/Tools/Moderation/DraupnirProtectedRoomsEditor" +@page "/Tools/Moderation/Draupnir/ProtectionsEditor" @using System.Text.Json.Serialization -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.RoomTypes <h3>Edit Draupnir protected rooms</h3> <hr/> @@ -38,7 +38,7 @@ </td> <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> - <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerACLEventContent.EventId) ? "X" : "")</td> + <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td> <td>@room.Room.RoomId</td> <td>@room.RoomName</td> </tr> @@ -49,7 +49,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { @@ -58,7 +58,7 @@ private AuthenticatedHomeserverGeneric hs { get; set; } protected override async Task OnInitializedAsync() { - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms"); StateHasChanged(); @@ -78,6 +78,43 @@ }).ToList(); await Task.WhenAll(tasks); await Task.Delay(500); + + foreach (var protectedRoomId in data.Rooms) { + if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue; + var room = hs.GetRoom(protectedRoomId); + var editorRoomInfo = new EditorRoomInfo { + Room = room, + IsProtected = true + }; + + try { + var pl = await room.GetPowerLevelsAsync(); + editorRoomInfo.PowerLevels = pl; + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}"); + } + + try { + editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync(); + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get name for {room.RoomId}: {e}"); + } + + try { + var membership = await room.GetStateEventOrNullAsync(hs.UserId); + if (membership is not null) { + editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}"; + } + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}"); + } + + Rooms.Add(editorRoomInfo); + } + StateHasChanged(); } diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor new file mode 100644
index 0000000..69a9048 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -0,0 +1,139 @@ +@page "/Tools/Moderation/Draupnir/WatchedListsEditor" +@using System.Text.Json.Serialization +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.RoomTypes +<h3>Edit Draupnir protected rooms</h3> +<hr/> +<p><b>Note:</b> You will need to restart Draupnir after applying changes!</p> +<p>Minor note: This <i>should</i> also work with Mjolnir, but this hasn't been tested, and as such functionality cannot be guaranteed.</p> + +@if (data is not null) { + <div class="row"> + <div class="col-12"> + <h4>Current rooms</h4> + <ul> + @foreach (var room in data.Rooms) { + <li>@room</li> + } + </ul> + <hr/> + <h4>Tickyboxes</h4> + <table class="table"> + <thead> + <tr> + <th></th> @* Checkbox column *@ + <th>Kick?</th> @* PL > kick *@ + <th>Ban?</th> @* PL > ban *@ + <th>ACL?</th> @* PL > m.room.server_acls event *@ + <th>Room ID</th> + <th>Room name</th> + </tr> + </thead> + <tbody> + @foreach (var room in Rooms.OrderBy(x => x.RoomName)) { + <tr> + <td> + <input type="checkbox" @bind="room.IsProtected"/> + </td> + <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> + <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td> + <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td> + <td>@room.Room.RoomId</td> + <td>@room.RoomName</td> + </tr> + } + </tbody> + </table> + </div> + </div> +} +<br/> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> + + +@code { + private DraupnirProtectedRoomsData data { get; set; } = new(); + private List<EditorRoomInfo> Rooms { get; set; } = new(); + private AuthenticatedHomeserverGeneric hs { get; set; } + + protected override async Task OnInitializedAsync() { + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms"); + StateHasChanged(); + var tasks = (await hs.GetJoinedRooms()).Select(async room => { + var plTask = room.GetPowerLevelsAsync(); + var roomNameTask = room.GetNameOrFallbackAsync(); + var EditorRoomInfo = new EditorRoomInfo { + Room = room, + IsProtected = data.Rooms.Contains(room.RoomId), + RoomName = await roomNameTask, + PowerLevels = await plTask + }; + + Rooms.Add(EditorRoomInfo); + StateHasChanged(); + return Task.CompletedTask; + }).ToList(); + await Task.WhenAll(tasks); + await Task.Delay(500); + + foreach (var protectedRoomId in data.Rooms) { + if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue; + var room = hs.GetRoom(protectedRoomId); + var editorRoomInfo = new EditorRoomInfo { + Room = room, + IsProtected = true + }; + + try { + var pl = await room.GetPowerLevelsAsync(); + editorRoomInfo.PowerLevels = pl; + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}"); + } + + try { + editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync(); + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get name for {room.RoomId}: {e}"); + } + + try { + var membership = await room.GetStateEventOrNullAsync(hs.UserId); + if (membership is not null) { + editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}"; + } + } + catch (MatrixException e) { + Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}"); + } + + Rooms.Add(editorRoomInfo); + } + + StateHasChanged(); + } + + private class DraupnirProtectedRoomsData { + [JsonPropertyName("rooms")] + public List<string> Rooms { get; set; } = new(); + } + + private class EditorRoomInfo { + public GenericRoom Room { get; set; } + public bool IsProtected { get; set; } + public string RoomName { get; set; } + public RoomPowerLevelEventContent PowerLevels { get; set; } + } + + private async Task Apply() { + Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId))); + data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList(); + await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor new file mode 100644
index 0000000..5ad9de4 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -0,0 +1,192 @@ +@page "/Tools/Moderation/FindUsersByRegex" +@using System.Collections.Frozen +@using ArcaneLibs.Extensions +@using LibMatrix.RoomTypes +@using System.Collections.ObjectModel +@using System.Text.RegularExpressions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Filters +@using LibMatrix.Helpers +<h3>Find users by regex</h3> +<hr/> + +<p>Users (regex): </p> +<InputTextArea @bind-Value="@UserIdString"></InputTextArea> + +<LinkButton OnClickAsync="Execute">Execute</LinkButton> +<br/> +<LinkButton OnClickAsync="RemoveKicks">Remove kicks</LinkButton> +<LinkButton OnClickAsync="RemoveBans">Remove bans</LinkButton> +<br/> + + +<details> + <summary>Results</summary> + @foreach (var (userId, events) in matches) { + <h4>@userId</h4> + <ul> + @foreach (var match in events) { + <li> + <ul> + <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li> + <li>Membership: @(match.Event.RawContent.ToJson(indent: false)) (sent by @match.Event.Sender)</li> + </ul> + </li> + } + </ul> + } +</details> + +<br/> +@foreach (var line in log.Reverse()) { + <pre>@line</pre> +} + +@code { + + private ObservableCollection<string> log { get; set; } = new(); + + // List<RoomInfo> rooms { get; set; } = new(); + List<GenericRoom> rooms { get; set; } = []; + Dictionary<string, List<Match>> matches = new(); + + private string UserIdString { + get => string.Join("\n", UserIDs); + set => UserIDs = value.Split("\n").Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); + } + + private List<string> UserIDs { get; set; } = new(); + + private AuthenticatedHomeserverGeneric hs { get; set; } + + protected override async Task OnInitializedAsync() { + log.CollectionChanged += (sender, args) => StateHasChanged(); + log.Add("Authenticating"); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task<string> Execute() { + log.Add("Constructing sync helper..."); + var sh = new SyncHelper(hs) { + Filter = new SyncFilter() { + AccountData = new(types: []), + Presence = new(types: []), + Room = new() { + AccountData = new(types: []), + Ephemeral = new(types: []), + State = new(types: [RoomMemberEventContent.EventId]), + Timeline = new(types: []), + IncludeLeave = false + }, + } + }; + + log.Add("Starting sync..."); + var res = await sh.SyncAsync(); + + log.Add("Got sync response, parsing..."); + + var roomNames = (await Task.WhenAll((await hs.GetJoinedRooms()).Select(async room => { return (room.RoomId, await room.GetNameOrFallbackAsync()); }).ToList())).ToFrozenDictionary(x => x.Item1, x => x.Item2); + + foreach (var userIdRegex in UserIDs) { + var regex = new Regex(userIdRegex, RegexOptions.Compiled); + log.Add($"Searching for {regex}:"); + foreach (var (roomId, joinedRoom) in res.Rooms.Join) { + log.Add($"- Checking room {roomId}..."); + foreach (var evt in joinedRoom.State.Events) { + if (evt.StateKey is null) continue; + if (evt.Type is not RoomMemberEventContent.EventId) continue; + + if (regex.IsMatch(evt.StateKey)) { + log.Add($" - Found match in {roomId} for {evt.StateKey}"); + if (!matches.ContainsKey(evt.StateKey)) { + matches[evt.StateKey] = new(); + } + + var room = hs.GetRoom(roomId); + matches[evt.StateKey].Add(new Match { + Room = room, + Event = evt, + RoomName = roomNames[roomId] + }); + } + } + } + } + + log.Add("Done!"); + + StateHasChanged(); + + return ""; + } + + public string? ImportFromRoomId { get; set; } + + private async Task DoImportFromRoomId() { + try { + if (ImportFromRoomId is null) return; + var room = rooms.FirstOrDefault(x => x.RoomId == ImportFromRoomId); + UserIdString = string.Join("\n", (await room.GetMembersListAsync()).Select(x => x.StateKey)); + } + catch (Exception e) { + Console.WriteLine(e); + log.Add("Could not fetch members list!\n" + e.ToString()); + } + + StateHasChanged(); + } + + private class Match { + public GenericRoom Room; + public StateEventResponse Event; + public string RoomName { get; set; } + } + + private async IAsyncEnumerable<Match> GetMatches(string userId) { + var results = rooms.Select(async room => { + var state = await room.GetStateEventOrNullAsync(room.RoomId, userId); + if (state is not null) { + return new Match { + Room = room, + Event = state, + RoomName = await room.GetNameOrFallbackAsync() + }; + } + + return null; + }).ToAsyncResultEnumerable(); + await foreach (var result in results) { + if (result is not null) { + yield return result; + } + } + } + + private Task RemoveKicks() { + foreach (var (userId, matches) in matches) { + matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "leave" && x.Event.Sender != x.Event.StateKey); + } + + matches.RemoveAll((x, y) => y.Count == 0); + StateHasChanged(); + return Task.CompletedTask; + } + + private Task RemoveBans() { + foreach (var (userId, matches) in matches) { + matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "ban" && x.Event.Sender != x.Event.StateKey); + } + + matches.RemoveAll((x, y) => y.Count == 0); + StateHasChanged(); + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 2123d4d..ac68e3d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -1,19 +1,21 @@ @page "/Tools/Moderation/InviteCounter" @using System.Collections.ObjectModel -@using LibMatrix.EventTypes.Spec.State +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Filters <h3>Invite counter</h3> <hr/> <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> <details> <summary>Results</summary> - @foreach (var (userId, events) in invites.OrderByDescending(x=>x.Value).ToList()) { + @foreach (var (userId, events) in invites.OrderByDescending(x => x.Value).ToList()) { <p>@userId: @events</p> } </details> @@ -27,16 +29,15 @@ private ObservableCollection<string> log { get; set; } = new(); private Dictionary<string, int> invites { get; set; } = new(); private AuthenticatedHomeserverGeneric hs { get; set; } - + [Parameter, SupplyParameterFromQuery(Name = "room")] public string roomId { get; set; } - protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -44,22 +45,21 @@ private async Task<string> Execute() { var room = hs.GetRoom(roomId); - var events = room.GetManyMessagesAsync(limit: int.MaxValue); + var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] }; + var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false)); await foreach (var resp in events) { var all = resp.State.Concat(resp.Chunk); foreach (var evt in all) { - if(evt.Type != RoomMemberEventContent.EventId) continue; + if (evt.Type != RoomMemberEventContent.EventId) continue; var content = evt.TypedContent as RoomMemberEventContent; - if(content.Membership != "invite") continue; - if(!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0; - invites[evt.Sender]++; + if (content?.Membership != "invite") continue; + invites.TryAdd(evt.Sender!, 0); + invites[evt.Sender!]++; } log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline"); } - - - + StateHasChanged(); return ""; diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index ea1e5f6..605890d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,13 +1,14 @@ @page "/Tools/Moderation/MassCMEBan" @using System.Collections.ObjectModel @using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.RoomTypes <h3>User Trace</h3> <hr/> <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> @@ -17,19 +18,19 @@ } @code { + // TODO: Properly implement page to be more useful private ObservableCollection<string> log { get; set; } = new(); private AuthenticatedHomeserverGeneric hs { get; set; } - + [Parameter, SupplyParameterFromQuery(Name = "room")] public string roomId { get; set; } - protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -37,33 +38,41 @@ private async Task<string> Execute() { var room = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev"); - // var room = hs.GetRoom("!yf7OpOiRDXx6zUGpT6:conduit.rory.gay"); - var users = roomId.Split("\n").Select(x => x.Trim()).Where(x=>x.StartsWith('@')).ToList(); - foreach (var user in users) { - var exists = false; - try { - exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity); - } catch (Exception e) { - log.Add($"Failed to get {user}"); - } + // var room = hs.GetRoom("!IVSjKMsVbjXsmUTuRR:rory.gay"); + var users = roomId.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList(); + var tasks = users.Select(x => ExecuteBan(room, x)).ToList(); + await Task.WhenAll(tasks); + + StateHasChanged(); + + return ""; + } - if (!exists) { + private async Task ExecuteBan(GenericRoom room, string user) { + var exists = false; + try { + exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity); + } + catch (Exception e) { + log.Add($"Failed to get {user}"); + } + + if (!exists) { + try { var evt = await room.SendStateEventAsync(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'), new UserPolicyRuleEventContent() { Entity = user, - Reason = "spam (invite)", + Reason = "spam", Recommendation = "m.ban" }); log.Add($"Sent {evt.EventId} to ban {user}"); } - else { - log.Add($"User {user} already exists"); + catch (Exception e) { + log.Add($"Failed to ban {user}: {e}"); } } - - - StateHasChanged(); - - return ""; + else { + log.Add($"User {user} already exists"); + } } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index e5ba004..11c4a80 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -1,31 +1,162 @@ @page "/Tools/Moderation/MembershipHistory" +@using System.Collections.Frozen @using System.Collections.ObjectModel +@using System.Diagnostics +@using System.Text.Json +@using ArcaneLibs.Extensions @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Filters +@{ + var sw = Stopwatch.StartNew(); + Console.WriteLine("Start render"); +} <h3>Membership history viewer</h3> <hr/> - <br/> <span>Room ID: </span> -<InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> -<p><InputCheckbox @bind-Value="ChronologicalOrder"/> Chronological order</p> +<InputText @bind-Value="@RoomId"></InputText> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> +<p> + <span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span> + <span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span> +</p> <p> <span>Show </span> - <InputCheckbox @bind-Value="ShowJoins"/> joins - <InputCheckbox @bind-Value="ShowLeaves"/> leaves - <InputCheckbox @bind-Value="ShowUpdates"/> profile updates - <InputCheckbox @bind-Value="ShowKnocks"/> knocks - <InputCheckbox @bind-Value="ShowInvites"/> invites - <InputCheckbox @bind-Value="ShowKicks"/> kicks - <InputCheckbox @bind-Value="ShowBans"/> bans + <span><InputCheckbox @bind-Value="ShowJoins"/> joins</span> + <span><InputCheckbox @bind-Value="ShowLeaves"/> leaves</span> + <span><InputCheckbox @bind-Value="ShowKnocks"/> knocks</span> + <span><InputCheckbox @bind-Value="ShowInvites"/> invites</span> + <span><InputCheckbox @bind-Value="ShowBans"/> bans</span> </p> <p> - <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = false; })">Hide all</LinkButton> - <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = true; })">Show all</LinkButton> - <LinkButton OnClick="@(async () => { ShowJoins ^= true; ShowLeaves ^= true; ShowUpdates ^= true; ShowKnocks ^= true; ShowInvites ^= true; ShowKicks ^= true; ShowBans ^= true; })">Toggle all</LinkButton> + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; + StateHasChanged(); + })">Hide all + </LinkButton> + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; + StateHasChanged(); + })">Show all + </LinkButton> + <LinkButton OnClickAsync="@(async () => { + ShowJoins ^= true; + ShowLeaves ^= true; + ShowKnocks ^= true; + ShowInvites ^= true; + ShowBans ^= true; + StateHasChanged(); + })">Toggle all + </LinkButton> </p> <p> + <span><InputCheckbox @bind-Value="DoDisambiguate"/> Disambiguate </span> + @if (DoDisambiguate) { + <span><InputCheckbox @bind-Value="DisambiguateKicks"/> kicks</span> + <span><InputCheckbox @bind-Value="DisambiguateUnbans"/> unbans</span> + <span><InputCheckbox @bind-Value="DisambiguateProfileUpdates"/> profile updates</span> + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="DisambiguateInviteActions"/> + invite actions + </summary> + <span><InputCheckbox @bind-Value="DisambiguateInviteAccepted"/> accepted</span> + <span><InputCheckbox @bind-Value="DisambiguateInviteRejected"/> rejected</span> + <span><InputCheckbox @bind-Value="DisambiguateInviteRetracted"/> retracted</span> + </details> + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="DisambiguateKnockActions"/> + knock actions + </summary> + <span><InputCheckbox @bind-Value="DisambiguateKnockAccepted"/> accepted</span> + <span><InputCheckbox @bind-Value="DisambiguateKnockRejected"/> rejected</span> + <span><InputCheckbox @bind-Value="DisambiguateKnockRetracted"/> retracted</span> + </details> + } +</p> +@if (DoDisambiguate) { + <p> + <span>Show </span> + @if (DisambiguateKicks) { + <span><InputCheckbox @bind-Value="ShowKicks"/> kicks</span> + } + @if (DisambiguateUnbans) { + <span><InputCheckbox @bind-Value="ShowUnbans"/> unbans</span> + } + @if (DisambiguateProfileUpdates) { + <span><InputCheckbox @bind-Value="ShowProfileUpdates"/> profile updates</span> + } + @if (DisambiguateInviteActions) { + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="ShowInviteActions"/> + invite actions + </summary> + @if (DisambiguateInviteAccepted) { + <span><InputCheckbox @bind-Value="ShowInviteAccepted"/> accepted</span> + } + + @if (DisambiguateInviteRejected) { + <span><InputCheckbox @bind-Value="ShowInviteRejected"/> rejected</span> + } + + @if (DisambiguateInviteRetracted) { + <span><InputCheckbox @bind-Value="ShowInviteRetracted"/> retracted</span> + } + </details> + } + @if (DisambiguateKnockActions) { + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="ShowKnockActions"/> + knock actions + </summary> + @if (DisambiguateKnockAccepted) { + <span><InputCheckbox @bind-Value="ShowKnockAccepted"/> accepted</span> + } + + @if (DisambiguateKnockRejected) { + <span><InputCheckbox @bind-Value="ShowKnockRejected"/> rejected</span> + } + + @if (DisambiguateKnockRetracted) { + <span><InputCheckbox @bind-Value="ShowKnockRetracted"/> retracted</span> + } + </details> + } + </p> + + <p> + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; + StateHasChanged(); + })">Un-disambiguate all + </LinkButton> + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; + StateHasChanged(); + })">Disambiguate all + </LinkButton> + <LinkButton OnClickAsync="@(async () => { + DisambiguateProfileUpdates ^= true; + DisambiguateKicks ^= true; + DisambiguateUnbans ^= true; + DisambiguateInviteAccepted ^= true; + DisambiguateInviteRejected ^= true; + DisambiguateInviteRetracted ^= true; + DisambiguateKnockAccepted ^= true; + DisambiguateKnockRejected ^= true; + DisambiguateKnockRetracted ^= true; + DisambiguateKnockActions ^= true; + DisambiguateInviteActions ^= true; + StateHasChanged(); + })">Toggle all + </LinkButton> + </p> +} +<p> <span>Sender: </span> <InputSelect @bind-Value="Sender"> <option value="">All</option> @@ -44,92 +175,121 @@ </InputSelect> </p> - +@{ Console.WriteLine($"Rendering took {sw.Elapsed} for {Memberships.Count} items"); } <br/> -<details> +<details open> <summary>Results</summary> @{ - Dictionary<string, StateEventResponse> previousMemberships = []; - var filteredMemberships = Memberships.AsEnumerable(); - if (ChronologicalOrder) { - filteredMemberships = filteredMemberships.Reverse(); - } - if(!string.IsNullOrWhiteSpace(Sender)) { - filteredMemberships = filteredMemberships.Where(x => x.Sender == Sender); - } - if(!string.IsNullOrWhiteSpace(User)) { - filteredMemberships = filteredMemberships.Where(x => x.StateKey == User); - } - - @foreach (var membership in filteredMemberships) { - RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent; - @switch (content.Membership) { - case RoomMemberEventContent.MembershipTypes.Invite: { - if (_showInvites) { - <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Ban: { - if (_showBans) { - <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Leave: { - if (membership.Sender == membership.StateKey) { - if (_showLeaves) { - <p style="color: #C66;">@membership.Sender left the room</p> - } - } - else { - if (_showKicks) { - <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Knock: { - if (_showKnocks) { - <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Join: { - if (previousMemberships.TryGetValue(membership.StateKey, out var previous) - && (previous.TypedContent as RoomMemberEventContent).Membership == RoomMemberEventContent.MembershipTypes.Join) { - if (_showUpdates) { - <p style="color: #777;">@membership.Sender changed their profile</p> - } - } - else { - if (_showJoins) { - <p style="color: #6C6;">@membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } + var filteredMemberships = GetFilteredMemberships(); + } + <table> + @foreach (var membershipEntry in filteredMemberships) { + var (transition, membership, previousMembership) = membershipEntry; + RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!"); + RoomMemberEventContent? previousContent = previousMembership?.TypedContent as RoomMemberEventContent; + + <tr> + <td>@DateTimeOffset.FromUnixTimeMilliseconds(membership.OriginServerTs ?? 0).ToString("g")</td> + <td> + @switch (transition) { + case MembershipTransition.None: + <b>Unknown membership! Got None</b> + break; + case MembershipTransition.Join: + <p style="color: #6C6;"> + @membership.StateKey joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")<br/> + Display name: @content.DisplayName<br/> + Avatar URL: @content.AvatarUrl + </p> + break; + case MembershipTransition.Leave: + <p style="color: #C66;"> + @membership.StateKey left the room + </p> + break; + case MembershipTransition.Knock: + <p style="color: #426"> + @membership.StateKey knocked @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + case MembershipTransition.Invite: + <p style="color: #262;"> + @membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + case MembershipTransition.Ban: + <p style="color: red;"> + @membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + @* disambiguated *@ + case MembershipTransition.Kick: + <p style="color: darkorange;"> + @membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + case MembershipTransition.ProfileUpdate: + <p style="color: #777;"> + @membership.Sender changed their profile<br/> + Display name: @previousContent!.DisplayName -> @content.DisplayName<br/> + Avatar URL: @previousContent.AvatarUrl -> @content.AvatarUrl + </p> + break; + case MembershipTransition.InviteAccepted: + <p style="color: #084;"> + @membership.StateKey accepted the invite + from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})") + </p> + break; + case MembershipTransition.KnockAccepted: + <p style="color: #288;"> + @membership.StateKey's knock was accepted + by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})") + </p> + break; + case MembershipTransition.KnockRejected: + <p style="color: #828;"> + @membership.StateKey's knock was rejected + by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})") + </p> + break; + case MembershipTransition.Unban: + <p style="color: #0C0;"> + @membership.Sender unbanned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + case MembershipTransition.InviteRejected: + <p style="color: #733;"> + @membership.StateKey rejected the invite + from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})") + </p> + break; + case MembershipTransition.InviteRetracted: + <p style="color: #844;"> + @membership.Sender retracted the invite + for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + case MembershipTransition.KnockRetracted: + <p style="color: #b55;"> + @membership.Sender retracted the knock + for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})") + </p> + break; + default: + throw new ArgumentOutOfRangeException(); } - - break; - } - default: { - <b>Unknown membership @content.Membership!</b> - break; - } - } - - previousMemberships[membership.StateKey] = membership; + </td> + </tr> } - } + </table> </details> <br/> <details open> <summary>Log</summary> - @foreach (var line in log.Reverse()) { + @foreach (var line in Log.Reverse()) { <pre>@line</pre> } </details> @@ -138,139 +298,289 @@ #region Filter bindings - private bool _chronologicalOrder = false; - - private bool ChronologicalOrder { - get => _chronologicalOrder; - set { - _chronologicalOrder = value; - StateHasChanged(); - } - } + private bool ChronologicalOrder { get; set; } + private bool ShowJoins { get; set; } = true; + private bool ShowLeaves { get; set; } = true; + private bool ShowKnocks { get; set; } = true; + private bool ShowInvites { get; set; } = true; + private bool ShowBans { get; set; } = true; + + private bool DoDisambiguate { get; set; } = true; + private bool DisambiguateProfileUpdates { get => field && DoDisambiguate; set; } = true; + private bool DisambiguateKicks { get => field && DoDisambiguate; set; } = true; + private bool DisambiguateUnbans { get => field && DoDisambiguate; set; } = true; + private bool DisambiguateInviteAccepted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; + private bool DisambiguateInviteRejected { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; + private bool DisambiguateInviteRetracted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; + private bool DisambiguateKnockAccepted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; + private bool DisambiguateKnockRejected { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; + private bool DisambiguateKnockRetracted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; + + private bool DisambiguateKnockActions { get => field && DoDisambiguate; set; } = true; + private bool DisambiguateInviteActions { get => field && DoDisambiguate; set; } = true; - private bool _showJoins = true; + private bool ShowProfileUpdates { + get => field && DisambiguateProfileUpdates; + set; + } = true; - private bool ShowJoins { - get => _showJoins; + private bool ShowKicks { + get => field && DisambiguateKicks; + set; + } = true; + + private bool ShowUnbans { + get => field && DisambiguateUnbans; + set; + } = true; + + private bool ShowInviteAccepted { + get => field && DisambiguateInviteAccepted; + set; + } = true; + + private bool ShowInviteRejected { + get => field && DisambiguateInviteRejected; + set; + } = true; + + private bool ShowInviteRetracted { + get => field && DisambiguateInviteRetracted; + set; + } = true; + + private bool ShowKnockAccepted { + get => field && DisambiguateKnockAccepted; + set; + } = true; + + private bool ShowKnockRejected { + get => field && DisambiguateKnockRejected; + set; + } = true; + + private bool ShowKnockRetracted { + get => field && DisambiguateKnockRetracted; + set; + } = true; + + private bool ShowKnockActions { + get => field && DisambiguateKnockActions; + set; + } = true; + + private bool ShowInviteActions { + get => field && DisambiguateInviteActions; + set; + } = true; + + [Parameter, SupplyParameterFromQuery(Name = "sender")] + public string Sender { get; set; } = ""; + + [Parameter, SupplyParameterFromQuery(Name = "user")] + public string User { get; set; } = ""; + + [Parameter, SupplyParameterFromQuery(Name = "filter")] + public string Filter { + get; set { - _showJoins = value; + field = value; + if (string.IsNullOrWhiteSpace(value)) return; + var parts = value.Split(','); + ShowJoins = parts.Contains("join"); + ShowLeaves = parts.Contains("leave"); + ShowKnocks = parts.Contains("knock"); + ShowInvites = parts.Contains("invite"); + ShowBans = parts.Contains("ban"); StateHasChanged(); } - } - - private bool _showLeaves = true; + } = ""; - private bool ShowLeaves { - get => _showLeaves; - set { - _showLeaves = value; - StateHasChanged(); - } - } +#endregion - private bool _showUpdates = true; + private ObservableCollection<string> Log { get; set; } = new(); + private List<StateEventResponse> Memberships { get; set; } = []; + private AuthenticatedHomeserverGeneric Homeserver { get; set; } - private bool ShowUpdates { - get => _showUpdates; - set { - _showUpdates = value; - StateHasChanged(); - } - } + [Parameter, SupplyParameterFromQuery(Name = "room")] + public string RoomId { get; set; } = ""; - private bool _showKnocks = true; + protected override async Task OnInitializedAsync() { + Log.CollectionChanged += (sender, args) => StateHasChanged(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; - private bool ShowKnocks { - get => _showKnocks; - set { - _showKnocks = value; - StateHasChanged(); - } + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + if (!string.IsNullOrWhiteSpace(RoomId)) + await Execute(); } - private bool _showInvites = true; + private async Task Execute() { + Memberships.Clear(); + var room = Homeserver.GetRoom(RoomId); + var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] }; + var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false)); + await foreach (var resp in events) { + var all = resp.State.Concat(resp.Chunk) + // ugly hack, because some users fuck around too much + .Select(x => { + if (x.RawContent?["displayname"]?.GetValueKind() != JsonValueKind.String) + x.RawContent?.Remove("displayname"); + if (x.RawContent?["avatar_url"]?.GetValueKind() is not JsonValueKind.String) + x.RawContent?.Remove("avatar_url"); + return x; + }); + Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId)); - private bool ShowInvites { - get => _showInvites; - set { - _showInvites = value; - StateHasChanged(); + Log.Add($"Got {resp.State.Count} state and {resp.Chunk.Count} timeline events."); } - } - private bool _showKicks = true; + Log.Add("Reached end of timeline!"); - private bool ShowKicks { - get => _showKicks; - set { - _showKicks = value; - StateHasChanged(); - } + StateHasChanged(); } - private bool _showBans = true; + private readonly struct MembershipEntry { + public required MembershipTransition State { get; init; } + public required StateEventResponse Event { get; init; } + public required StateEventResponse? Previous { get; init; } - private bool ShowBans { - get => _showBans; - set { - _showBans = value; - StateHasChanged(); + public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + transition = State; + evt = Event; + prev = Previous; } } - - private string sender = ""; - - private string Sender { - get => sender; - set { - sender = value; - StateHasChanged(); - } + + private enum MembershipTransition : byte { + None, + Join, + Leave, + Knock, + Invite, + Ban, + + // disambiguated + ProfileUpdate, + Kick, + Unban, + InviteAccepted, + InviteRejected, + InviteRetracted, + KnockAccepted, + KnockRejected, + KnockRetracted } - - private string user = ""; - - private string User { - get => user; - set { - user = value; - StateHasChanged(); + + private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> evts) { + Dictionary<string, MembershipEntry> transitions = new(); + foreach (var evt in evts.OrderBy(x => x.OriginServerTs)) { + var content = evt.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!"); + var prev = transitions.GetValueOrDefault(evt.StateKey!) as MembershipEntry?; + transitions[evt.StateKey ?? throw new Exception("Member event has no state key??")] = new MembershipEntry { + Event = evt, + Previous = prev?.Event, + State = content.Membership switch { + RoomMemberEventContent.MembershipTypes.Join => + prev?.State switch { + MembershipTransition.Join or MembershipTransition.InviteAccepted => MembershipTransition.ProfileUpdate, + MembershipTransition.Invite => MembershipTransition.InviteAccepted, + _ => MembershipTransition.Join + }, + RoomMemberEventContent.MembershipTypes.Leave => + evt.Sender == evt.StateKey + ? prev?.State switch { + MembershipTransition.Knock => MembershipTransition.KnockRetracted, + MembershipTransition.Invite => MembershipTransition.InviteRejected, + _ => MembershipTransition.Leave + } + : prev?.State switch { + // not self + MembershipTransition.Knock => MembershipTransition.KnockRejected, + MembershipTransition.Invite => MembershipTransition.InviteRetracted, + _ => MembershipTransition.Kick, + }, + RoomMemberEventContent.MembershipTypes.Invite => + prev?.State switch { + MembershipTransition.Knock => MembershipTransition.KnockAccepted, + _ => MembershipTransition.Invite + }, + RoomMemberEventContent.MembershipTypes.Knock => MembershipTransition.Knock, + RoomMemberEventContent.MembershipTypes.Ban => MembershipTransition.Ban, + _ => MembershipTransition.None + } + }; + yield return transitions[evt.StateKey]; } } -#endregion - - private ObservableCollection<string> log { get; set; } = new(); - private List<StateEventResponse> Memberships { get; set; } = []; - private AuthenticatedHomeserverGeneric hs { get; set; } - - [Parameter, SupplyParameterFromQuery(Name = "room")] - public string roomId { get; set; } - - protected override async Task OnInitializedAsync() { - log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); - if (hs is null) return; + private IEnumerable<MembershipEntry> Disambiguated(IEnumerable<MembershipEntry> entries) { + FrozenDictionary<MembershipTransition, MembershipTransition> disambiguated = new Dictionary<MembershipTransition, MembershipTransition>() { + { MembershipTransition.ProfileUpdate, MembershipTransition.Join }, + { MembershipTransition.Kick, MembershipTransition.Leave }, + { MembershipTransition.Unban, MembershipTransition.Leave }, + { MembershipTransition.InviteAccepted, MembershipTransition.Join }, + { MembershipTransition.InviteRejected, MembershipTransition.Leave }, + { MembershipTransition.InviteRetracted, MembershipTransition.Leave }, + { MembershipTransition.KnockAccepted, MembershipTransition.Invite }, + { MembershipTransition.KnockRejected, MembershipTransition.Leave }, + { MembershipTransition.KnockRetracted, MembershipTransition.Leave } + }.ToFrozenDictionary(); + + foreach (var entry in entries) { + if (!DoDisambiguate) { + yield return entry; + continue; + } - StateHasChanged(); - Console.WriteLine("Rerendered!"); - await base.OnInitializedAsync(); - if (!string.IsNullOrWhiteSpace(roomId)) - await Execute(); + var newState = entry.State switch { + MembershipTransition.ProfileUpdate when !DoDisambiguate || !DisambiguateProfileUpdates => MembershipTransition.Join, + MembershipTransition.Kick when !DoDisambiguate || !DisambiguateKicks => MembershipTransition.Leave, + MembershipTransition.Unban when !DoDisambiguate || !DisambiguateUnbans => MembershipTransition.Leave, + MembershipTransition.InviteAccepted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted => MembershipTransition.Join, + MembershipTransition.InviteRejected when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected => MembershipTransition.Leave, + MembershipTransition.InviteRetracted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted => MembershipTransition.Leave, + MembershipTransition.KnockAccepted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted => MembershipTransition.Invite, + MembershipTransition.KnockRejected when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected => MembershipTransition.Leave, + MembershipTransition.KnockRetracted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted => MembershipTransition.Leave, + _ => entry.State + }; + if (newState != entry.State) { + yield return entry with { State = newState }; + } + else yield return entry; + } } - private async Task Execute() { - Memberships.Clear(); - var room = hs.GetRoom(roomId); - var events = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000); - await foreach (var resp in events) { - var all = resp.State.Concat(resp.Chunk); - Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId)); - - log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline"); + private IEnumerable<MembershipEntry> GetFilteredMemberships() { + var filteredMemberships = GetTransitions(Memberships); + if (!string.IsNullOrWhiteSpace(Sender)) filteredMemberships = filteredMemberships.Where(x => x.Event.Sender == Sender); + if (!string.IsNullOrWhiteSpace(User)) filteredMemberships = filteredMemberships.Where(x => x.Event.StateKey == User); + filteredMemberships = Disambiguated(filteredMemberships); + + if (!ShowJoins) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Join); + if (!ShowLeaves) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Leave); + if (!ShowKnocks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Knock); + if (!ShowInvites) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Invite); + if (!ShowBans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Ban); + // extended filters + if (DoDisambiguate) { + if (!DisambiguateProfileUpdates || !ShowProfileUpdates) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.ProfileUpdate); + if (!DisambiguateKicks || !ShowKicks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Kick); + if (!DisambiguateUnbans || !ShowUnbans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Unban); + if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteAccepted || !ShowInviteAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteAccepted); + if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRejected || !ShowInviteRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRejected); + if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRetracted || !ShowInviteRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRetracted); + if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockAccepted || !ShowKnockAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockAccepted); + if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRejected || !ShowKnockRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRejected); + if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRetracted || !ShowKnockRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRetracted); } - StateHasChanged(); + if (!ChronologicalOrder) filteredMemberships = filteredMemberships.Reverse(); + + return filteredMemberships; } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index b8baeb8..ee77532 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -2,19 +2,19 @@ @using LibMatrix.RoomTypes @using System.Collections.ObjectModel @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>Room intersections</h3> <hr/> <p>Set A: </p> <InputText @bind-Value="@ImportSetASpaceId"></InputText> -<LinkButton OnClick="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton> +<LinkButton OnClickAsync="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton> <p>Set B: </p> <InputText @bind-Value="@ImportSetBSpaceId"></InputText> -<LinkButton OnClick="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton> +<LinkButton OnClickAsync="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton> <br/> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> <details> @@ -113,7 +113,7 @@ protected override async Task OnInitializedAsync() { Log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; StateHasChanged(); @@ -144,7 +144,7 @@ public async Task GetMembers(List<GenericRoom> rooms, Dictionary<string, List<Match>> users) { foreach (var room in rooms) { Log.Add($"Getting members for {room.RoomId}"); - var members = await room.GetMembersListAsync(false); + var members = await room.GetMembersListAsync(); foreach (var member in members) { if (member.RawContent?["membership"]?.ToString() == "ban") continue; if (member.RawContent?["membership"]?.ToString() == "invite") continue; @@ -158,7 +158,7 @@ } public async Task AppendSet(string spaceId, List<GenericRoom> rooms) { - var space = hs.GetRoom(spaceId).AsSpace; + var space = hs.GetRoom(spaceId).AsSpace(); Log.Add($"Found space {spaceId}"); var roomIdsEnum = space.GetChildrenAsync(true); List<Task> tasks = new(); diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index 915f8dc..2261cb8 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -3,13 +3,15 @@ @using LibMatrix.RoomTypes @using System.Collections.ObjectModel @using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>User Trace</h3> <hr/> <p>Users: </p> <InputTextArea @bind-Value="@UserIdString"></InputTextArea> <br/> -<InputText @bind-Value="@ImportFromRoomId"></InputText><LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton> +<InputText @bind-Value="@ImportFromRoomId"></InputText> +<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton> <details> <summary>Rooms to be searched (@rooms.Count)</summary> @@ -19,23 +21,26 @@ } </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <details> <summary>Results</summary> - @foreach (var (userId, events) in matches) { + @foreach (var (userId, events) in matches.OrderBy(x => x.Key)) { <h4>@userId</h4> - <ul> - @foreach (var match in events) { - <li> - <ul> - <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li> - <li>Membership: @(match.Event.RawContent.ToJson(indent: false))</li> - </ul> - </li> + <table> + @foreach (var match in events.OrderBy(x => x.RoomName)) { + <tr> + <td>@match.RoomName (<span>@match.Room.RoomId</span>)</td> + <td> + <details> + <summary>@SummarizeMembership(match.Event)</summary> + <pre>@match.Event.RawContent.ToJson(indent: true)</pre> + </details> + </td> + </tr> } - </ul> + </table> } </details> @@ -61,56 +66,34 @@ protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - // var sessions = await RMUStorage.GetAllTokens(); - // var baseRooms = new List<GenericRoom>(); - // foreach (var userAuth in sessions) { - // var session = await RMUStorage.GetSession(userAuth); - // if (session is not null) { - // baseRooms.AddRange(await session.GetJoinedRooms()); - // var sessionRooms = (await session.GetJoinedRooms()).Where(x => !rooms.Any(y => y.Room.RoomId == x.RoomId)).ToList(); - // StateHasChanged(); - // log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}"); - // } - // } - // - // log.Add("Done fetching rooms!"); - // - // baseRooms = baseRooms.DistinctBy(x => x.RoomId).ToList(); - // - // // rooms.CollectionChanged += (sender, args) => StateHasChanged(); - // var tasks = baseRooms.Select(async newRoom => { - // bool success = false; - // while (!success) - // try { - // var state = await newRoom.GetFullStateAsListAsync(); - // var newRoomInfo = new RoomInfo(newRoom, state); - // rooms.Add(newRoomInfo); - // log.Add($"Got {newRoomInfo.StateEvents.Count} events for {newRoomInfo.RoomName}"); - // success = true; - // } - // catch (MatrixException e) { - // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}"); - // throw; - // } - // catch (HttpRequestException e) { - // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}"); - // } - // }); - // await Task.WhenAll(tasks); - // - // log.Add($"Done fetching members!"); - // - // UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x)); - - foreach (var session in await RMUStorage.GetAllTokens()) { - var _hs = await RMUStorage.GetSession(session); - if (_hs is not null) { - rooms.AddRange(await _hs.GetJoinedRooms()); - log.Add($"Got {rooms.Count} rooms after adding {_hs.UserId}"); + + var sessions = await sessionStore.GetAllSessions(); + var tasks = sessions.Select(async session => { + try { + var _hs = await sessionStore.GetHomeserver(session.Key); + if (_hs is not null) { + try { + var _rooms = await _hs.GetJoinedRooms(); + if (!_rooms.Any()) return; + // Check if homeserver supports `?format=event`: + await _rooms.First().GetStateEventAsync(RoomMemberEventContent.EventId, session.Value.Auth.UserId); + rooms.AddRange(_rooms); + log.Add($"Got {_rooms.Count} rooms for {_hs.UserId}, total {rooms.Count}"); + } + catch (Exception e) { + if (e is LibMatrixException { ErrorCode: LibMatrixException.ErrorCodes.M_UNSUPPORTED }) + log.Add($"Homeserver {_hs.UserId} does not support `?format=event`! Skipping..."); + else log.Add($"Failed to fetch rooms for {_hs.UserId}! {e}"); + } + } } - } + catch (Exception e) { + log.Add($"Failed to fetch rooms for {session.Value.Auth.UserId}! {e}"); + } + }); + await Task.WhenAll(tasks); //get distinct rooms evenly distributed per session, accounting for count per session rooms = rooms.OrderBy(x => rooms.Count(y => y.Homeserver == x.Homeserver)).DistinctBy(x => x.RoomId).ToList(); @@ -125,17 +108,6 @@ foreach (var userId in UserIDs) { matches.Add(userId, new List<Match>()); - // foreach (var room in rooms) { - // var state = room.StateEvents.Where(x => x!.Type == RoomMemberEventContent.EventId).ToList(); - // if (state!.Any(x => x.StateKey == userId)) { - // matches[userId].Add(new() { - // Event = state.First(x => x.StateKey == userId), - // Room = room.Room, - // RoomName = room.RoomName ?? "No name" - // }); - // } - // } - log.Add($"Searching for {userId}..."); await foreach (var match in GetMatches(userId)) { matches[userId].Add(match); @@ -173,17 +145,23 @@ private async IAsyncEnumerable<Match> GetMatches(string userId) { var results = rooms.Select(async room => { - var state = await room.GetStateEventOrNullAsync(room.RoomId, userId); - if (state is not null) { - return new Match { - Room = room, - Event = state, - RoomName = await room.GetNameOrFallbackAsync() - }; + try { + var state = await room.GetStateEventOrNullAsync(RoomMemberEventContent.EventId, userId); + if (state is not null) { + log.Add($"Found {userId} in {room.RoomId} with membership {state.RawContent.ToJson(indent: false)}"); + return new Match { + Room = room, + Event = state, + RoomName = await room.GetNameOrFallbackAsync() + }; + } + } + catch (Exception e) { + log.Add($"Failed to fetch state for {userId} in {room.RoomId}! {e}"); } return null; - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var result in results) { if (result is not null) { yield return result; @@ -191,4 +169,26 @@ } } + public string SummarizeMembership(StateEventResponse state) { + var membership = state.ContentAs<RoomMemberEventContent>(); + var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value); + return membership switch { + { Membership: "invite", Reason: null } => $"Invited by {state.Sender} at {time}", + { Membership: "invite", Reason: not null } => $"Invited by {state.Sender} at {time} for {membership.Reason}", + { Membership: "join", Reason: null } => $"Joined at {time}", + { Membership: "join", Reason: not null } => $"Joined at {time} for {membership.Reason}", + { Membership: "leave", Reason: null } => state.Sender == state.StateKey ? $"Left at {time}" : $"Kicked by {state.Sender} at {time}", + { Membership: "leave", Reason: not null } => state.Sender == state.StateKey ? $"Left at {time} with reason {membership.Reason}" : $"Kicked by {state.Sender} at {time} for {membership.Reason}", + { Membership: "ban", Reason: null } => $"Banned by {state.Sender} at {time}", + { Membership: "ban", Reason: not null } => $"Banned by {state.Sender} at {time} for {membership.Reason}", + { Membership: "knock", Reason: null } => $"Knocked at {time}", + { Membership: "knock", Reason: not null } => $"Knocked at {time} for {membership.Reason}", + _ => $"Unknown membership {membership.Membership}, sent at {time} by {state.Sender} for {membership.Reason}" + }; + } + + private async Task ExportJson() { + var json = matches.ToJson(); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor new file mode 100644
index 0000000..208cd19 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor
@@ -0,0 +1,51 @@ +@page "/Tools/Room/DropPowerlevel" +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +<h3>DropPowerlevel</h3> +<hr/> + +<span>User ID: </span><FancyTextBox @bind-Value="@UserId"/><br/> +<span>Room ID: </span><FancyTextBox @bind-Value="@RoomId"/><br/> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> + +<pre>@Result</pre> + +@code { + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } = null!; + + [Parameter, SupplyParameterFromQuery(Name = "RoomId")] + public string RoomId { get; set; } = ""; + + [Parameter, SupplyParameterFromQuery(Name = "UserId")] + public string UserId { get; set; } = ""; + + private string Result { get; set; } = ""; + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(); + Result = "I am: " + Homeserver.WhoAmI.ToJson() + "\n"; + StateHasChanged(); + } + + private async Task Execute() { + try { + if (Homeserver is not AuthenticatedHomeserverGeneric hs) { + Result = "Not authenticated"; + return; + } + + var room = hs.GetRoom(RoomId); + + var powerlevels = await room.GetPowerLevelsAsync(); + powerlevels.Users.Remove(UserId); + Result = (await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, powerlevels)).ToJson(); + } + catch (Exception e) { + Result = e.Message; + } + finally { + StateHasChanged(); + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor new file mode 100644
index 0000000..a47d7f5 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
@@ -0,0 +1,204 @@ +@page "/Tools/Room/SpacePermissions" +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Pages.Rooms +<h3>Space Permissions</h3> +<hr/> +<span>Space ID: </span> +<FancyTextBox @bind-Value="@SpaceId"/> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> +<br/> +<InputCheckbox @bind-Value="@AutoRecurseSpaces"/> +<span> Auto-recurse into child spaces</span> +<br/> + +@if (RoomPowerLevels.Count == 0) { + <p>No data loaded.</p> +} +else { + <span>Loaded @LoadedSpaceRooms.Count spaces.</span> + <br/> + @if (SpaceRooms.Count > 0) { + <h3>Load more spaces:</h3> + @foreach (var room in SpaceRooms) { + <LinkButton OnClickAsync="@(() => LoadSpaceAsync(room.Key))">@room.Value</LinkButton> + } + } + + <h3>By event type:</h3> + <table class="table-striped table-hover table-bordered align-middle"> + <thead> + <td>Room</td> + @foreach (var key in OrderedEventTypes) { + <td>@key.Key + <br/> + ~ @Math.Round(key.Value, 2) + </td> + } + </thead> + <tbody> + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Events!.Values.Average())) { + <tr> + <td>@roomName</td> + @foreach (var eventType in OrderedEventTypes) { + if (!powerLevels.Events!.ContainsKey(eventType.Key)) { + <td style="background-color: #ff000044;">-</td> + continue; + } + + <td>@(powerLevels.Events![eventType.Key])</td> + } + </tr> + } + </tbody> + </table> + <br/> + <h3>By user:</h3> + <table class="table-striped table-hover table-bordered align-middle"> + <thead> + <td>Room</td> + @foreach (var key in OrderedUsers) { + <td>@key.Key + <br/> + ~ @Math.Round(key.Value, 2) + </td> + } + </thead> + <tbody> + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Users!.Values.Average())) { + <tr> + <td>@roomName</td> + @foreach (var eventType in OrderedUsers) { + if (!powerLevels.Users!.ContainsKey(eventType.Key)) { + <td style="background-color: #ff000044;">-</td> + continue; + } + + <td>@(powerLevels.Users![eventType.Key])</td> + } + </tr> + } + </tbody> + </table> +} + +@code { + + [Parameter, SupplyParameterFromQuery] + public string? SpaceId { get; set; } + + [Parameter, SupplyParameterFromQuery] + public bool AutoRecurseSpaces { get; set; } + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private List<AuthenticatedHomeserverGeneric> AllHomeservers { get; set; } = []; + private Dictionary<string, List<GenericRoom>> JoinedHomeserversByRoom { get; set; } = []; + + private Dictionary<string, RoomPowerLevelEventContent> RoomPowerLevels { get; set; } = []; + private Dictionary<string, string> SpaceRooms { get; set; } = []; + private List<string> LoadedSpaceRooms { get; set; } = []; + + private Dictionary<string, double> OrderedEventTypes { get; set; } = new(); + private Dictionary<string, double> OrderedUsers { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverGeneric hs) return; + Homeserver = hs; + await foreach (var server in sessionStore.TryGetAllHomeservers()) { + AllHomeservers.Add(server); + var joinedRooms = await server.GetJoinedRooms(); + foreach (var room in joinedRooms) { + if (!JoinedHomeserversByRoom.ContainsKey(room.RoomId)) { + JoinedHomeserversByRoom[room.RoomId] = []; + } + + JoinedHomeserversByRoom[room.RoomId].Add(room); + } + } + + if (!string.IsNullOrWhiteSpace(SpaceId)) { + await Execute(); + } + } + + private async Task Execute() { + RoomPowerLevels = []; + SpaceRooms = []; + await LoadSpaceAsync(SpaceId); + } + + private async Task<GenericRoom> GetJoinedRoomAsync(string roomId) { + var room = Homeserver.GetRoom(roomId); + if (await room.IsJoinedAsync()) return room; + + if (JoinedHomeserversByRoom.TryGetValue(roomId, out var rooms)) { + foreach (var r in rooms) { + if (await r.IsJoinedAsync()) return r; + } + } + + foreach (var hs in AllHomeservers) { + if (hs == Homeserver) continue; + room = hs.GetRoom(roomId); + if (await room.IsJoinedAsync()) return room; + } + + Console.WriteLine($"Not joined to room {roomId} on any known homeserver."); + return room; // not null, in case we can preview the room + } + + private async Task LoadSpaceAsync(string spaceId) { + LoadedSpaceRooms.Add(spaceId); + SpaceRooms.Remove(spaceId); + + var space = (await GetJoinedRoomAsync(spaceId)).AsSpace(); + RoomPowerLevels[await space.GetNameOrFallbackAsync()] = AddFakeEvents(await space.GetPowerLevelsAsync()); + var children = space.GetChildrenAsync(); + await foreach (var childRoom in children) { + var child = await GetJoinedRoomAsync(childRoom.RoomId); + try { + var powerlevels = await child.GetPowerLevelsAsync(); + RoomPowerLevels[await child.GetNameOrFallbackAsync()] = AddFakeEvents(powerlevels!); + if (await child.GetRoomType() == SpaceRoom.TypeName) { + if (AutoRecurseSpaces) + await LoadSpaceAsync(child.RoomId); + else + SpaceRooms.Add(child.RoomId, await child.GetNameOrFallbackAsync()); + } + + OrderedEventTypes = RoomPowerLevels + .SelectMany(x => x.Value.Events!) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Average(y => y.Value)) + .OrderByDescending(x => x.Value) + .ToDictionary(x => x.Key, x => x.Value); + + OrderedUsers = RoomPowerLevels + .SelectMany(x => x.Value.Users!) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Average(y => y.Value)) + .OrderByDescending(x => x.Value) + .ToDictionary(x => x.Key, x => x.Value); + StateHasChanged(); + } + catch (Exception ex) { + Console.WriteLine($"Failed to get power levels for room {child.RoomId}: {ex}"); + } + } + } + + private RoomPowerLevelEventContent AddFakeEvents(RoomPowerLevelEventContent powerlevels) { + powerlevels.Events ??= []; + powerlevels.Events["[user_default]"] = powerlevels.UsersDefault ?? 0; + powerlevels.Events["[event_default]"] = powerlevels.EventsDefault ?? 0; + powerlevels.Events["[state_default]"] = powerlevels.StateDefault ?? 100; + powerlevels.Events["[ban]"] = powerlevels.Ban ?? 100; + powerlevels.Events["[invite]"] = powerlevels.Invite ?? 100; + powerlevels.Events["[kick]"] = powerlevels.Kick ?? 100; + powerlevels.Events["[ping_room]"] = powerlevels.NotificationsPl?.Room ?? 100; + powerlevels.Events["[redact]"] = powerlevels.Redact ?? 100; + return powerlevels; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
index 80a03f2..d6ae945 100644 --- a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor +++ b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
@@ -1,6 +1,6 @@ @page "/Tools/Room/SpaceRestrictedJoins" @using System.Collections.ObjectModel -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>Allow space to restricted join children</h3> <hr/> @@ -10,7 +10,7 @@ <p><InputCheckbox @bind-Value="@ChangeKnocking"/> Change knock access: <InputCheckbox @bind-Value="@Knocking"/></p> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <br/> @@ -31,7 +31,7 @@ protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); - hs = await RMUStorage.GetCurrentSessionOrNavigate(); + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; StateHasChanged(); @@ -40,7 +40,7 @@ } private async Task Execute() { - var space = hs.GetRoom(RoomId).AsSpace; + var space = hs.GetRoom(RoomId).AsSpace(); await foreach (var room in space.GetChildrenAsync()) { log.Add($"Got room {room.RoomId}"); if (ChangeGuestAccess) { diff --git a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index 667b518..acc86a2 100644 --- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor +++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -1,7 +1,7 @@ @page "/Tools/CopyPowerlevel" @using ArcaneLibs.Extensions @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.RoomTypes <h3>Copy powerlevel</h3> <hr/> @@ -12,7 +12,7 @@ } <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -23,11 +23,11 @@ List<AuthenticatedHomeserverGeneric> hss { get; set; } = new(); protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - var sessions = await RMUStorage.GetAllTokens(); - foreach (var userAuth in sessions) { - var session = await RMUStorage.GetSession(userAuth); + var sessions = await sessionStore.GetAllSessions(); + foreach (var userAuth in sessions.Keys) { + var session = await sessionStore.GetHomeserver(userAuth); if (session is not null) { hss.Add(session); StateHasChanged(); @@ -42,7 +42,7 @@ private async Task Execute() { foreach (var hs in hss) { var rooms = await hs.GetJoinedRooms(); - var tasks = rooms.Select(x=>Execute(hs, x)).ToAsyncEnumerable(); + var tasks = rooms.Select(x => ApplyPowerlevelsInRoom(hs, x)).ToAsyncResultEnumerable(); await foreach (var a in tasks) { if (!string.IsNullOrWhiteSpace(a)) { log.Add(a); @@ -52,7 +52,7 @@ } } - private async Task<string> Execute(AuthenticatedHomeserverGeneric hs, GenericRoom room) { + private async Task<string> ApplyPowerlevelsInRoom(AuthenticatedHomeserverGeneric hs, GenericRoom room) { try { var pls = await room.GetPowerLevelsAsync(); // if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) == pls.UsersDefault) return "I am default PL in " + room.RoomId; @@ -62,12 +62,11 @@ log.Add("I am same PL in " + room.RoomId); continue; } - + pls.SetUserPowerLevel(ahs.WhoAmI.UserId, pls.GetUserPowerLevel(hs.WhoAmI.UserId)); await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls); log.Add($"Updated powerlevel of {room.RoomId} to {pls.GetUserPowerLevel(ahs.WhoAmI.UserId)}"); } - } catch (MatrixException e) { return $"Failed to update PLs in {room.RoomId}: {e.Message}"; @@ -75,6 +74,7 @@ catch (Exception e) { return $"Failed to update PLs in {room.RoomId}: {e.Message}"; } + StateHasChanged(); return ""; } diff --git a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
index a2ad388..ee17f1d 100644 --- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -1,7 +1,7 @@ @page "/Tools/MassRoomJoin" @using ArcaneLibs.Extensions @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>Mass join room</h3> <hr/> <p>Room: </p> @@ -13,7 +13,7 @@ } <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -25,11 +25,11 @@ string roomId { get; set; } protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; - var sessions = await RMUStorage.GetAllTokens(); - foreach (var userAuth in sessions) { - var session = await RMUStorage.GetSession(userAuth); + var sessions = await sessionStore.GetAllSessions(); + foreach (var userAuth in sessions.Keys) { + var session = await sessionStore.GetHomeserver(userAuth); if (session is not null) { hss.Add(session); StateHasChanged(); @@ -42,23 +42,24 @@ } private async Task Execute() { - // foreach (var hs in hss) { - // var rooms = await hs.GetJoinedRooms(); - var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable(); + // foreach (var hs in hss) { + // var rooms = await hs.GetJoinedRooms(); + var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable(); await foreach (var a in tasks) { if (!string.IsNullOrWhiteSpace(a)) { log.Add(a); StateHasChanged(); } } - tasks = hss.Select(ExecuteJoin).ToAsyncEnumerable(); + + tasks = hss.Select(ExecuteJoin).ToAsyncResultEnumerable(); await foreach (var a in tasks) { if (!string.IsNullOrWhiteSpace(a)) { log.Add(a); StateHasChanged(); } } - // } + // } } private async Task<string> ExecuteInvite(AuthenticatedHomeserverGeneric hs) { @@ -69,6 +70,7 @@ if (joinRule.JoinRule == RoomJoinRulesEventContent.JoinRules.Public) return "Room is public, no invite needed"; } catch { } + var pls = await room.GetPowerLevelsAsync(); if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) < pls.Invite) return "I do not have permission to send invite in " + room.RoomId; await room.InviteUsersAsync(hss.Select(x => x.WhoAmI.UserId).ToList()); @@ -80,6 +82,7 @@ catch (Exception e) { return $"Failed to invite in {room.RoomId}: {e.Message}"; } + StateHasChanged(); return ""; } @@ -92,6 +95,7 @@ if (mse?.Membership == "join") return $"User {hs.WhoAmI.UserId} already in room"; } catch { } + await room.JoinAsync(); } catch (MatrixException e) { @@ -100,6 +104,7 @@ catch (Exception e) { return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}"; } + StateHasChanged(); return ""; } diff --git a/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor new file mode 100644
index 0000000..0e838c7 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
@@ -0,0 +1,80 @@ +@page "/Tools/User/StickerManager" +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Common +@using LibMatrix.EventTypes.Spec +@inject ILogger<StickerManager> Logger +<h3>Sticker/emoji manager</h3> + +@if (TotalStepsProgress is not null) { + <SimpleProgressIndicator ObservableProgress="@TotalStepsProgress"/> + <br/> +} +@if (_observableProgressState is not null) { + <SimpleProgressIndicator ObservableProgress="@_observableProgressState"/> + <br/> +} + +@code { + + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private Msc2545EmoteRoomsAccountDataEventContent? EnabledEmoteRooms { get; set; } + private Dictionary<string, StickerRoom> StickerRooms { get; set; } = []; + + private SimpleProgressIndicator.ObservableProgressState? _observableProgressState; + + private SimpleProgressIndicator.ObservableProgressState? TotalStepsProgress { get; set; } = new() { + Label = "Authenticating with Matrix...", + Max = 2, + Value = 0 + }; + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not { } hs) + return; + Homeserver = hs; + TotalStepsProgress?.Next("Fetching enabled emote packs..."); + _ = hs.GetAccountDataOrNullAsync<Msc2545EmoteRoomsAccountDataEventContent>(Msc2545EmoteRoomsAccountDataEventContent.EventId) + .ContinueWith(r => { + EnabledEmoteRooms = r.Result; + StateHasChanged(); + }); + + TotalStepsProgress?.Next("Getting joined rooms..."); + _observableProgressState = new() { + Label = "Loading rooms...", + Max = 1, + Value = 0 + }; + var rooms = await hs.GetJoinedRooms(); + _observableProgressState.Max.Value = rooms.Count; + StateHasChanged(); + + var ss = new SemaphoreSlim(32, 32); + var ss1 = new SemaphoreSlim(1, 1); + var roomScanTasks = rooms.Select(async room => { + // await Task.Delay(Random.Shared.Next(100, 1000 + (rooms.Count * 100))); + // await ss.WaitAsync(); + var state = await room.GetFullStateAsListAsync(); + StickerRoom sr = new(); + foreach (var evt in state) { + if (evt.Type == RoomEmotesEventContent.EventId) { } + } + + // ss.Release(); + // await ss1.WaitAsync(); + Console.WriteLine("Got state for room " + room.RoomId); + // _observableProgressState.Next($"Got state for room {room.RoomId}"); + // await Task.Delay(1); + // ss1.Release(); + return room.RoomId; + }) + .ToList(); + await foreach (var roomScanResult in roomScanTasks.ToAsyncResultEnumerable()) { + _observableProgressState.Label.Value = roomScanResult; + } + } + + private class StickerRoom { } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
index d8b02bb..a393d2e 100644 --- a/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor +++ b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
@@ -1,4 +1,4 @@ -@page "/Tools/ViewAccountData" +@page "/Tools/User/ViewAccountData" @using ArcaneLibs.Extensions @using LibMatrix <h3>View account data</h3> @@ -16,7 +16,7 @@ Dictionary<string, EventList?> perRoomAccountData = new(); protected override async Task OnInitializedAsync() { - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is null) return; perRoomAccountData = await hs.EnumerateAccountDataPerRoom(); globalAccountData = await hs.EnumerateAccountData(); diff --git a/MatrixUtils.Web/Pages/User/DMManager.razor b/MatrixUtils.Web/Pages/User/DMManager.razor
index 80bf3b2..4b8b7c2 100644 --- a/MatrixUtils.Web/Pages/User/DMManager.razor +++ b/MatrixUtils.Web/Pages/User/DMManager.razor
@@ -1,8 +1,8 @@ @page "/User/DirectMessages" -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.Responses @using MatrixUtils.Abstractions @using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo <h3>Direct Messages</h3> <hr/> @@ -29,7 +29,7 @@ } protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; Status = "Loading global profile..."; if (Homeserver.WhoAmI?.UserId is null) return; diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index 49af22f..2b7b6cf 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -1,10 +1,8 @@ @page "/User/Profile" -@using LibMatrix.EventTypes.Spec.State -@using ArcaneLibs.Extensions @using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses @using MatrixUtils.Abstractions -@using Microsoft.AspNetCore.Components.Forms <h3>Manage Profile - @Homeserver?.WhoAmI?.UserId</h3> <hr/> @@ -12,13 +10,17 @@ <h4>Profile</h4> <hr/> <div> - <img src="@Homeserver.ResolveMediaUri(NewProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> + <MxcAvatar Homeserver="@Homeserver" MxcUri="@NewProfile.AvatarUrl" Circular="true" Size="96"/> <div style="display: inline-block; vertical-align: middle;"> - <span>Display name: </span><FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox><br/> - <span>Avatar URL: </span><FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox> - <InputFile OnChange="@AvatarChanged"></InputFile><br/> - <LinkButton OnClick="@(() => UpdateProfile())">Update profile</LinkButton> - <LinkButton OnClick="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton> + <span>Display name: </span> + <FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox> + <br/> + <span>Avatar URL: </span> + <FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox> + <InputFile OnChange="@AvatarChanged"></InputFile> + <br/> + <LinkButton OnClickAsync="@(() => UpdateProfile())">Update profile</LinkButton> + <LinkButton OnClickAsync="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton> </div> </div> @if (!string.IsNullOrWhiteSpace(Status)) { @@ -28,24 +30,33 @@ <br/> @* <details> *@ - <h4>Room profiles<hr></h4> + <h4>Room profiles + <hr> + </h4> @foreach (var room in Rooms) { <details class="details-compact"> <summary style="@(room.OwnMembership?.DisplayName == OldProfile.DisplayName && room.OwnMembership?.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")"> <div style="display: inline-block; width: calc(100% - 50px); vertical-align: middle; margin-top: -8px; margin-bottom: -8px;"> <CascadingValue Value="OldProfile"> - <RoomListItem ShowOwnProfile="true" RoomInfo="@room" OwnMemberState="@room.OwnMembership"></RoomListItem> + <RoomListItem Homeserver="Homeserver" ShowOwnProfile="true" RoomInfo="@room" OwnMemberState="@room.OwnMembership"></RoomListItem> </CascadingValue> </div> </summary> @if (room.OwnMembership is not null) { - <img src="@Homeserver.ResolveMediaUri(room.OwnMembership.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> + @* <img src="@Homeserver.ResolveMediaUri(room.OwnMembership.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> *@ + <MxcAvatar Homeserver="@Homeserver" MxcUri="@room.OwnMembership.AvatarUrl" Circular="true" Size="96"/> <div style="display: inline-block; vertical-align: middle;"> - <span>Display name: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.DisplayName"></FancyTextBox><br/> - <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.AvatarUrl"></FancyTextBox> - <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile><br/> - <LinkButton OnClick="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton> + <span>Display name: </span> + <FancyTextBox BackgroundColor="@(room.OwnMembership.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" + @bind-Value="@room.OwnMembership.DisplayName"></FancyTextBox> + <br/> + <span>Avatar URL: </span> + <FancyTextBox BackgroundColor="@(room.OwnMembership.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" + @bind-Value="@room.OwnMembership.AvatarUrl"></FancyTextBox> + <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile> + <br/> + <LinkButton OnClickAsync="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton> </div> <br/> @if (!string.IsNullOrWhiteSpace(Status)) { @@ -58,29 +69,11 @@ </details> <br/> } - - @foreach (var (roomId, roomProfile) in RoomProfiles.OrderBy(x => RoomNames.TryGetValue(x.Key, out var _name) ? _name : x.Key)) { - <details class="details-compact"> - <summary style="@(roomProfile.DisplayName == OldProfile.DisplayName && roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")">@(RoomNames.TryGetValue(roomId, out var name) ? name : roomId)</summary> - <img src="@Homeserver.ResolveMediaUri(roomProfile.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> - <div style="display: inline-block; vertical-align: middle;"> - <span>Display name: </span><FancyTextBox BackgroundColor="@(roomProfile.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@roomProfile.DisplayName"></FancyTextBox><br/> - <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(roomProfile.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@roomProfile.AvatarUrl"></FancyTextBox> - <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, roomId))"></InputFile><br/> - <LinkButton OnClick="@(() => UpdateRoomProfile(roomId))">Update profile</LinkButton> - </div> - <br/> - @if (!string.IsNullOrWhiteSpace(Status)) { - <p>@Status</p> - } - </details> - <br/> - } // </details> } @code { - private string? _status = null; + private string? _status; private AuthenticatedHomeserverGeneric? Homeserver { get; set; } private UserProfileResponse? NewProfile { get; set; } @@ -99,7 +92,7 @@ private Dictionary<string, string> RoomNames { get; set; } = new(); protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; Status = "Loading global profile..."; if (Homeserver.WhoAmI?.UserId is null) return; @@ -107,44 +100,50 @@ OldProfile = (await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId)); //.DeepClone(); Status = "Loading room profiles..."; var roomProfiles = Homeserver.GetRoomProfilesAsync(); + List<Task> roomInfoTasks = []; await foreach (var (roomId, roomProfile) in roomProfiles) { - var room = Homeserver.GetRoom(roomId); - var roomNameTask = room.GetNameOrFallbackAsync(); - var roomIconTask = room.GetAvatarUrlAsync(); - var roomInfo = new RoomInfo(room) { - OwnMembership = roomProfile - }; - try { - roomInfo.RoomIcon = (await roomIconTask).Url; - } - catch (MatrixException e) { - if (e is not { ErrorCode: "M_NOT_FOUND" }) throw; - } + var task = Task.Run(async () => { + var room = Homeserver.GetRoom(roomId); + var roomNameTask = room.GetNameOrFallbackAsync(); + var roomIconTask = room.GetAvatarUrlAsync(); + var roomInfo = new RoomInfo(room) { + OwnMembership = roomProfile + }; + try { + roomInfo.RoomIcon = (await roomIconTask).Url; + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_NOT_FOUND" }) throw; + } - try { - roomInfo.RoomName = await roomNameTask; - } - catch (MatrixException e) { - if (e is not { ErrorCode: "M_NOT_FOUND" }) throw; - } + try { + RoomNames[roomId] = roomInfo.RoomName = await roomNameTask; + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_NOT_FOUND" }) throw; + } - Rooms.Add(roomInfo); - // Status = $"Got profile for {roomId}..."; - RoomProfiles[roomId] = roomProfile; //.DeepClone(); + Rooms.Add(roomInfo); + // Status = $"Got profile for {roomId}..."; + RoomProfiles[roomId] = roomProfile; //.DeepClone(); + }); + roomInfoTasks.Add(task); } + await Task.WhenAll(roomInfoTasks); + StateHasChanged(); Status = "Room profiles loaded, loading room names..."; - var roomNameTasks = RoomProfiles.Keys.Select(x => Homeserver.GetRoom(x)).Select(async x => { - var name = await x.GetNameOrFallbackAsync(); - return new KeyValuePair<string, string?>(x.RoomId, name); - }).ToAsyncEnumerable(); + // var roomNameTasks = RoomProfiles.Keys.Select(x => Homeserver.GetRoom(x)).Select(async x => { + // var name = await x.GetNameOrFallbackAsync(); + // return new KeyValuePair<string, string?>(x.RoomId, name); + // }).ToAsyncResultEnumerable(); - await foreach (var (roomId, roomName) in roomNameTasks) { - // Status = $"Got room name for {roomId}: {roomName}"; - RoomNames[roomId] = roomName; - } + // await foreach (var (roomId, roomName) in roomNameTasks) { + // Status = $"Got room name for {roomId}: {roomName}"; + // RoomNames[roomId] = roomName; + // } StateHasChanged(); Status = null; diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 1b8960c..e48782f 100644 --- a/MatrixUtils.Web/Program.cs +++ b/MatrixUtils.Web/Program.cs
@@ -1,13 +1,17 @@ using System.Net; using System.Text.Json; using System.Text.Json.Serialization; +using ArcaneLibs.Blazor.Components.Services; using Blazored.LocalStorage; using Blazored.SessionStorage; +using LibMatrix.Extensions; using LibMatrix.Services; using MatrixUtils.Web; using MatrixUtils.Web.Classes; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using SpawnDev.BlazorJS; +using SpawnDev.BlazorJS.WebWorkers; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); @@ -16,6 +20,17 @@ builder.RootComponents.Add<HeadOutlet>("head::after"); // builder.Logging.SetMinimumLevel(LogLevel.Trace); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddBlazorJSRuntime(out var jsRuntime); +builder.Services.AddWebWorkerService(webWorkerService => { + // Optionally configure the WebWorkerService service before it is used + // Default WebWorkerService.TaskPool settings: PoolSize = 0, MaxPoolSize = 1, AutoGrow = true + // Below sets TaskPool max size to 2. By default the TaskPool size will grow as needed up to the max pool size. + // Setting max pool size to -1 will set it to the value of navigator.hardwareConcurrency + webWorkerService.TaskPool.MaxPoolSize = -1; + // Below is telling the WebWorkerService TaskPool to set the initial size to 2 if running in a Window scope and 0 otherwise + // This starts up 2 WebWorkers to handle TaskPool tasks as needed + webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0; +}); try { builder.Configuration.AddJsonStream(await new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.GetStreamAsync("/appsettings.json")); @@ -33,8 +48,7 @@ catch (Exception e) { Console.WriteLine("Could not load appsettings: " + e); } -builder.Logging.AddConfiguration( - builder.Configuration.GetSection("Logging")); +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); builder.Services.AddBlazoredLocalStorage(config => { config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; @@ -63,6 +77,13 @@ builder.Services.AddScoped<TieredStorageService>(x => ) ); +MatrixHttpClient.LogRequests = false; + builder.Services.AddRoryLibMatrixServices(); -builder.Services.AddScoped<RMUStorageWrapper>(); -await builder.Build().RunAsync(); \ No newline at end of file +builder.Services.AddScoped<RmuSessionStore>(); +builder.Services.AddSingleton<BlazorSaveFileService>(); +builder.Services.AddSingleton<JsConsoleService>(); + +// await builder.Build().RunAsync(); +var host = App.Host = builder.Build(); +await host.BlazorJSRunAsync(); \ No newline at end of file diff --git a/MatrixUtils.Web/Properties/launchSettings.json b/MatrixUtils.Web/Properties/launchSettings.json
index aa41dc8..660211d 100644 --- a/MatrixUtils.Web/Properties/launchSettings.json +++ b/MatrixUtils.Web/Properties/launchSettings.json
@@ -13,7 +13,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5117", + "applicationUrl": "http://*:5117", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor b/MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor new file mode 100644
index 0000000..0730701 --- /dev/null +++ b/MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor
@@ -0,0 +1,17 @@ +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +<span> + <InputCheckbox @bind-Value="@Filter.Enabled"/> @Label: + @if (Filter.Enabled) { + <InputCheckbox @bind-Value="@Filter.Value"/> + } +</span> + +@code { + + [Parameter] + public required BoolFilter Filter { get; set; } + + [Parameter] + public required string Label { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor b/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor new file mode 100644
index 0000000..c5a6e15 --- /dev/null +++ b/MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor
@@ -0,0 +1,31 @@ +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +<span style="vertical-align: top;"> + <InputCheckbox @bind-Value="@Filter.Enabled"/> @Label: +</span> +@if (Filter.Enabled) { + <div style="display: inline-block;"> + <InputCheckbox @bind-Value="@Filter.CheckValueContains"/> + Contains + <FancyTextBox @bind-Value="@Filter.ValueContains"></FancyTextBox> + <br/> + <InputCheckbox @bind-Value="@Filter.CheckValueEquals"/> + Equals + <FancyTextBox @bind-Value="@Filter.ValueEquals"></FancyTextBox> + <LinkButton OnClick="@SetEqualsNull" InlineText="true"> [Set null]</LinkButton> + </div> +} + +@code { + + [Parameter] + public required StringFilter Filter { get; set; } + + [Parameter] + public required string Label { get; set; } + + private void SetEqualsNull() { + Filter.ValueEquals = null; + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/InlineUserItem.razor b/MatrixUtils.Web/Shared/InlineUserItem.razor
index 9c6608a..eaf7a92 100644 --- a/MatrixUtils.Web/Shared/InlineUserItem.razor +++ b/MatrixUtils.Web/Shared/InlineUserItem.razor
@@ -1,4 +1,4 @@ -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses <div style="background-color: #ffffff11; border-radius: 0.5em; height: 1em; display: inline-block; vertical-align: middle;" alt="@UserId"> <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "vertical-align: top;") width: 1em; height: 1em; border-radius: 50%;" src="@ProfileAvatar"/> @@ -39,7 +39,6 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - Homeserver ??= await RMUStorage.GetCurrentSessionOrNavigate(); if(Homeserver is null) return; await _semaphoreSlim.WaitAsync(); @@ -59,7 +58,7 @@ } - ProfileAvatar ??= Homeserver.ResolveMediaUri(User.AvatarUrl); + // ProfileAvatar ??= Homeserver.ResolveMediaUri(User.AvatarUrl); ProfileName ??= User.DisplayName; _semaphoreSlim.Release(); diff --git a/MatrixUtils.Web/Shared/InputLocalPart.razor b/MatrixUtils.Web/Shared/InputLocalPart.razor new file mode 100644
index 0000000..8f34377 --- /dev/null +++ b/MatrixUtils.Web/Shared/InputLocalPart.razor
@@ -0,0 +1,50 @@ +<div style="display: inline-flex;"> + @if (!string.IsNullOrWhiteSpace(Label)) { + <label>@Label</label> + } + <span>@Sigil</span> + <FancyTextBox @bind-Value="@LocalPart"></FancyTextBox> + <span>:</span> + @if (ServerNameChanged is not null) { + <FancyTextBox @bind-Value="@ServerName"></FancyTextBox> + } + else { + <span>@ServerName</span> + } +</div> + +@code { + + [Parameter] + public string? Label { get; set; } + + [Parameter] + public required string Sigil { get; set; } + + [Parameter] + public string? LocalPart { + get; + set { + if (field == value) return; + field = value; + LocalPartChanged.InvokeAsync(value); + } + } + + [Parameter] + public EventCallback<string> LocalPartChanged { get; set; } + + [Parameter] + public string? ServerName { + get; + set { + if (field == value) return; + field = value; + ServerNameChanged?.InvokeAsync(value); + } + } + + [Parameter] + public EventCallback<string>? ServerNameChanged { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MainLayout.razor b/MatrixUtils.Web/Shared/MainLayout.razor
index c67f73c..b32735f 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor +++ b/MatrixUtils.Web/Shared/MainLayout.razor
@@ -1,5 +1,4 @@ -@using ArcaneLibs -@inherits LayoutComponentBase +@inherits LayoutComponentBase <div class="page"> <div class="sidebar"> @@ -10,16 +9,15 @@ <div class="top-row px-4"> @* <PortableDevTools/> *@ @* <ResourceUsage/> *@ - <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/MatrixRoomUtils.git/" target="_blank">Git</a> - <a style="color: #ccc; text-decoration: underline" href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" target="_blank">Matrix</a> + <a style="color: #ccc; text-decoration: underline" href="https://cgit.rory.gay/matrix/tools/MatrixUtils.git/" target="_blank">Git</a> + <a style="color: #ccc; text-decoration: underline" href="https://matrix.to/#/%23mru%3Arory.gay?via=rory.gay&via=matrix.org&via=feline.support" + target="_blank">Matrix</a> </div> <article class="Content px-4"> @Body </article> - - </main> </div> -<UpdateAvailableDetector/> \ No newline at end of file +<UpdateAvailableDetector/> diff --git a/MatrixUtils.Web/Shared/MainLayout.razor.css b/MatrixUtils.Web/Shared/MainLayout.razor.css
index 01a5066..924393b 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor.css +++ b/MatrixUtils.Web/Shared/MainLayout.razor.css
@@ -57,6 +57,7 @@ main { .sidebar { width: 250px; + min-width: 250px; height: 100vh; position: sticky; top: 0; diff --git a/MatrixUtils.Web/Shared/MxcAvatar.razor b/MatrixUtils.Web/Shared/MxcAvatar.razor new file mode 100644
index 0000000..822894a --- /dev/null +++ b/MatrixUtils.Web/Shared/MxcAvatar.razor
@@ -0,0 +1,49 @@ +<MxcImage Homeserver="@Homeserver" Uri="@MxcUri" style="@StyleString"/> + +@code { + private string _style; + + [Parameter] + public string? MxcUri { + get; + set { + if(field == value) return; + field = value; + // UriHasChanged(value); + StateHasChanged(); + } + } + + [Parameter] + public bool Circular { get; set; } + + [Parameter] + public int Size { get; set; } = 48; + + [Parameter] + public string SizeUnit { get; set; } = "px"; + + [Parameter] + public required AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private string StyleString => $"{(Circular ? "border-radius: 50%;" : "")} width: {Size}{SizeUnit}; height: {Size}{SizeUnit}; object-fit: cover;"; + + private static readonly string Prefix = "mxc://"; + private static readonly int PrefixLength = Prefix.Length; + + // private async Task UriHasChanged(string? value) { + // if (string.IsNullOrWhiteSpace(value) || !value.StartsWith(Prefix)) { + // Console.WriteLine($"[MxcAvatar] UriHasChanged: {value} does not start with {Prefix}!"); + // return; + // } + // + // if (Homeserver is null) { + // Console.WriteLine($"[MxcAvatar] Homeserver is required for MxcAvatar! URI: {MxcUri}, Homeserver: {Homeserver?.ToString() ?? "null"}"); + // return; + // } + // + // Console.WriteLine($"[MxcAvatar] Homeserver: {Homeserver}"); + // StateHasChanged(); + // } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/MxcImage.razor b/MatrixUtils.Web/Shared/MxcImage.razor
index e651c3f..26609ee 100644 --- a/MatrixUtils.Web/Shared/MxcImage.razor +++ b/MatrixUtils.Web/Shared/MxcImage.razor
@@ -1,69 +1,69 @@ -<img src="@ResolvedUri" style="@StyleString"/> -@code { - private string _mxcUri; - private string _style; - private string _resolvedUri; +<AuthorizedImage src="@ResolvedUrl" AccessToken="@Homeserver?.AccessToken" style="@StyleString"/> +@code { [Parameter] - public string MxcUri { - get => _mxcUri ?? ""; + public string? Uri { + get; set { - Console.WriteLine($"New MXC uri: {value}"); - _mxcUri = value; + // Console.WriteLine($"New MXC uri: {value}"); + if (field == value) return; + field = value; UriHasChanged(value); } } + [Parameter] public bool Circular { get; set; } - + [Parameter] public int? Width { get; set; } - + [Parameter] public int? Height { get; set; } - + [Parameter] - public string Style { - get => _style; + public string? Style { + get; set { - _style = value; + field = value; StateHasChanged(); } } - + [Parameter] - public RemoteHomeserver? Homeserver { get; set; } + public required AuthenticatedHomeserverGeneric Homeserver { get; set; } - private string ResolvedUri { - get => _resolvedUri; + private string? ResolvedUrl { + get; set { - _resolvedUri = value; + field = value; StateHasChanged(); } } private string StyleString => $"{Style} {(Circular ? "border-radius: 50%;" : "")} {(Width.HasValue ? $"width: {Width}px;" : "")} {(Height.HasValue ? $"height: {Height}px;" : "")} object-fit: cover;"; - - private static readonly string Prefix = "mxc://"; - private static readonly int PrefixLength = Prefix.Length; - private async Task UriHasChanged(string value) { - if (!value.StartsWith(Prefix)) { - Console.WriteLine($"UriHasChanged: {value} does not start with {Prefix}, passing as resolved URI!!!"); - ResolvedUri = value; - return; - } - var uri = value[PrefixLength..].Split('/'); - Console.WriteLine($"UriHasChanged: {value} {uri[0]}"); - if (Homeserver is null) { - Console.WriteLine($"Homeserver is null, creating new remotehomeserver for {uri[0]}"); - Homeserver = await hsProvider.GetRemoteHomeserver(uri[0]); + // private static readonly string Prefix = "mxc://"; + // private static readonly int PrefixLength = Prefix.Length; + + private async Task UriHasChanged(string? value) { + try { + if (string.IsNullOrWhiteSpace(value)) { + ResolvedUrl = null; + return; + } + + if (Homeserver is null) { + Console.WriteLine($"Homeserver is required for MxcImage! Uri: {value}, Homeserver: {Homeserver?.ToString() ?? "null"}"); + return; + } + + ResolvedUrl = await Homeserver.GetMediaUrlAsync(value); + // Console.WriteLine($"[MxcImage] Resolved URL: {ResolvedUrl}"); + StateHasChanged(); + } catch (Exception e) { + await Console.Error.WriteLineAsync($"Error resolving media URL: {e}"); } - ResolvedUri = Homeserver.ResolveMediaUri(value); - Console.WriteLine($"ResolvedUri: {ResolvedUri}"); } - // [Parameter] - // public string Class { get; set; } - } \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/NavMenu.razor b/MatrixUtils.Web/Shared/NavMenu.razor
index 770a246..7371e66 100644 --- a/MatrixUtils.Web/Shared/NavMenu.razor +++ b/MatrixUtils.Web/Shared/NavMenu.razor
@@ -37,6 +37,12 @@ </div> <div class="nav-item px-3"> + <NavLink class="nav-link" href="PolicyLists"> + <span class="oi oi-ban" aria-hidden="true"></span> Manage policy lists + </NavLink> + </div> + + <div class="nav-item px-3"> <NavLink class="nav-link" href="User/Profile"> <span class="oi oi-person" aria-hidden="true"></span> Manage profile </NavLink> diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor new file mode 100644
index 0000000..bb4b672 --- /dev/null +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -0,0 +1,221 @@ +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix +@using System.Collections.Frozen +@using LibMatrix.EventTypes +@using LibMatrix.RoomTypes +<ModalWindow + Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType ?? "") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))" + OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600"> + <span>Policy type:</span> + <select @bind="@MappedType"> + <option>Select a value</option> + @foreach (var (type, mappedType) in PolicyTypes) { + <option value="@type">@mappedType.GetFriendlyName().ToLower()</option> + } + </select><br/> + + <span>Reason:</span> + <FancyTextBox @bind-Value="@Reason"></FancyTextBox> + <br/> + + <span>Recommendation:</span> + <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox> + <br/> + + <span>Entities:</span><br/> + <FancyTextBox Multiline="true" @bind-Value="@Entities"></FancyTextBox> + <br/> + + + @* <details> *@ + @* <summary>JSON data</summary> *@ + @* <pre> *@ + @* $1$ @PolicyEvent.ToJson(true, true) #1# *@ + @* </pre> *@ + @* </details> *@ + @if (!VerifyIntent) { + <LinkButton OnClickAsync="@(() => { + OnClose.Invoke(); + return Task.CompletedTask; + })"> Cancel + </LinkButton> + <LinkButton OnClickAsync="@(() => { + _ = Save(); + return Task.CompletedTask; + })"> Save + </LinkButton> + @if (!string.IsNullOrWhiteSpace(Response)) { + <pre style="color: red;">@Response</pre> + } + } + else { + <b class="blink">WARNING!!!</b> + <br/> + + @if (!string.IsNullOrWhiteSpace(Response)) { + <pre style="color: red;">@Response</pre> + } + + <span>Are you sure you want to do this?</span> + <LinkButton Color="#00FF00" OnClick="@(() => { + VerifyIntent = false; + Response = null; + StateHasChanged(); + })">No + </LinkButton> + <LinkButton Color="#FF0000" OnClick="@(() => { _ = Save(force: true); })">Yes</LinkButton> + } + +</ModalWindow> + +@code { + + [Parameter] + public required Action OnClose { get; set; } + + [Parameter] + public required Action OnSaved { get; set; } + + [Parameter] + public required GenericRoom Room { get; set; } + + private string Recommendation { get; set; } = "m.ban"; + private string Reason { get; set; } = "spam"; + + private string Entities { get; set; } = ""; + + private string? Response { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool VerifyIntent { get; set; } + + private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes + .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + + private static FrozenSet<string> AllKnownPolicyTypes = KnownPolicyTypes + .SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName)) + .ToFrozenSet(); + + private string? MappedType { get; set; } + + private async Task Save(bool force = false) { + if (string.IsNullOrWhiteSpace(MappedType)) { + Response = "No type selected"; + return; + } + + if (string.IsNullOrWhiteSpace(Entities)) { + Response = "No users selected"; + return; + } + + Console.WriteLine("Saving ---"); + + var entities = Entities.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(x => x.Trim()) + .Distinct() + .ToList(); + + if (!force && !Validate(entities, PolicyTypes[MappedType])) { + List<string> distinctTypes = entities + .Select(GuessType) + .Where(x => x != null) + .Distinct() + .Select(x => x!.Name) + .ToList(); + + VerifyIntent = true; + Response = $"Invalid entities. Expected {PolicyTypes[MappedType].Name}, got:\n - " + + string.Join("\n - ", distinctTypes); + return; + } + + try { + await SaveAll(entities); + } + catch (Exception e) { + Response = $"Failed to save: {e}"; + } + } + + private bool Validate(List<string> entities, Type expectedType) { + return entities.All(x => GuessType(x) == expectedType); + } + + private Type? GuessType(string entity) { + var sigil = entity[0]; + return TypesBySigil.GetValueOrDefault(sigil.ToString(), typeof(ServerPolicyRuleEventContent)); + } + + private Dictionary<string, Type> TypesBySigil = new() { + { "@", typeof(UserPolicyRuleEventContent) }, + { "!", typeof(RoomPolicyRuleEventContent) }, + { "#", typeof(RoomPolicyRuleEventContent) } + }; + + private async Task SaveAll(List<string> entities) { + await foreach (var evt in Room.GetFullStateAsync()) { + if (evt is null + || !AllKnownPolicyTypes.Contains(evt.Type) + || !evt.TypedContent!.GetType().IsAssignableTo(PolicyTypes[MappedType!]) + ) continue; + + if (evt.TypedContent is PolicyRuleEventContent content && content.Recommendation == Recommendation && content.Reason == Reason) { + if (content.Entity != null && entities.Contains(content.Entity)) + entities.Remove(content.Entity); + } + } + + // var tasks = entities.Select(x => ExecuteBan(Room, x)).ToList(); + // await Task.WhenAll(tasks); + + var events = entities.Select(entity => { + var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content"); + content.Recommendation = Recommendation; + content.Reason = Reason; + content.Entity = entity; + return new StateEvent() { + Type = MappedType, + TypedContent = content, + StateKey = content.GetDraupnir2StateKey() + }; + }); + + foreach(var chunk in events.Chunk(50)) + await Room.BulkSendEventsAsync(chunk); + + OnSaved.Invoke(); + } + + private async Task ExecuteBan(GenericRoom room, string entity) { + bool success = false; + while (!success) { + try { + var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content"); + content.Recommendation = Recommendation; + content.Reason = Reason; + content.Entity = entity; + await room.SendStateEventAsync(MappedType!, content.GetDraupnir2StateKey(), content); + success = true; + } + catch (MatrixException e) { + if (e is not { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw; + Console.WriteLine(e); + } + catch (Exception e) { + //ignored + Console.WriteLine(e); + } + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css new file mode 100644
index 0000000..49ab31b --- /dev/null +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css
@@ -0,0 +1,15 @@ +.blink { + animation: blinker 2s linear infinite; +} + +@keyframes blinker { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 1bd00d1..0205e16 100644 --- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -6,7 +6,7 @@ @using System.Collections.Frozen @using LibMatrix.EventTypes <ModalWindow Title="@((string.IsNullOrWhiteSpace(PolicyEvent.EventId) ? "Creating new " : "Editing ") + (PolicyEvent.MappedType.GetFriendlyNameOrNull()?.ToLower() ?? "event"))" - OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="300"> + OnCloseClickedAsync="@InvokeOnClose" X="60" Y="60" MinWidth="300"> @if (string.IsNullOrWhiteSpace(PolicyEvent.EventId)) { <span>Policy type:</span> <select @bind="@MappedType"> @@ -35,39 +35,75 @@ </thead> <tbody> @foreach (var prop in props) { + var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null; <tr> <td style="padding-right: 8px;"> <span>@prop.GetFriendlyName()</span> - @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) { + @if (Nullable.GetUnderlyingType(prop.PropertyType) is null) { <span style="color: red;">*</span> } </td> @{ var getter = prop.GetGetMethod(); var setter = prop.GetSetMethod(); - } - @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) { - case Type t when t == typeof(string): - <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox> - break; - default: - <p style="color: red;">Unsupported type: @prop.PropertyType</p> - break; + if (getter is null) { + <p style="color: red;">Missing property getter: @prop.Name</p> + } + else { + switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) { + case Type t when t == typeof(string): + <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@((string e) => { + Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); + setter?.Invoke(PolicyData, [e]); + PolicyEvent.TypedContent = PolicyData; + StateHasChanged(); + })"></FancyTextBox> + break; + case Type t when t == typeof(DateTime): + if (!isNullable) { + @* <InputDate TValue="DateTime" Value="@(getter?.Invoke(PolicyData, null) as DateTime? ?? new DateTime())" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate> *@ + } + else { + var value = getter?.Invoke(PolicyData, null) as DateTime?; + if (value is null) { + <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }"> + Add value + </button> + } + else { + var notNullValue = Nullable.GetValueRefOrDefaultRef(ref value); + Console.WriteLine($"Value: {value?.ToString() ?? "null"}"); + <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => { + Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); + setter?.Invoke(PolicyData, [e]); + PolicyEvent.TypedContent = PolicyData; + StateHasChanged(); + })"></InputDate> + <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove + value + </button> + } + } + + break; + default: + <p style="color: red;">Unsupported type: @prop.PropertyType</p> + break; + } + } } </tr> } </tbody> </table> - <br/> - <pre> - @PolicyEvent.ToJson(true, false) - </pre> - <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton> - <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton> - @* <span>Target entity: </span> *@ - @* <FancyTextBox @bind-Value="@policyData.Entity"></FancyTextBox><br/> *@ - @* <span>Reason: </span> *@ - @* <FancyTextBox @bind-Value="@policyData.Reason"></FancyTextBox> *@ + <details> + <summary>JSON data</summary> + <pre> + @PolicyEvent.ToJson(true, true) + </pre> + </details> + <LinkButton OnClickAsync="@InvokeOnClose">Cancel</LinkButton> + <LinkButton OnClickAsync="@InvokeOnSave">Save</LinkButton> } else { <p>Policy data is null</p> @@ -89,10 +125,32 @@ } [Parameter] - public required Action OnClose { get; set; } + public Action? OnClose { get; set; } [Parameter] - public required Action<StateEventResponse> OnSave { get; set; } + public Func<Task>? OnCloseAsync { get; set; } + + private async Task InvokeOnClose() { + if (OnClose is not null) + OnClose.Invoke(); + + if (OnCloseAsync is not null) + await OnCloseAsync.Invoke(); + } + + [Parameter] + public Action<StateEventResponse>? OnSave { get; set; } + + [Parameter] + public Func<StateEventResponse, Task>? OnSaveAsync { get; set; } + + private async Task InvokeOnSave() { + if (OnSave is not null) + OnSave.Invoke(PolicyEvent); + + if (OnSaveAsync is not null) + await OnSaveAsync.Invoke(PolicyEvent); + } public PolicyRuleEventContent? PolicyData { get; set; } @@ -102,7 +160,7 @@ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); private StateEventResponse? _policyEvent; - + private string? MappedType { get => _policyEvent?.Type; set { @@ -110,9 +168,9 @@ PolicyEvent.Type = value; PolicyEvent.TypedContent ??= Activator.CreateInstance(PolicyTypes[value]) as PolicyRuleEventContent; PolicyData = PolicyEvent.TypedContent as PolicyRuleEventContent; + PolicyData.Recommendation ??= "m.ban"; } } } - } \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/RoomList.razor b/MatrixUtils.Web/Shared/RoomList.razor
index 42c5a9f..ba9cd69 100644 --- a/MatrixUtils.Web/Shared/RoomList.razor +++ b/MatrixUtils.Web/Shared/RoomList.razor
@@ -10,7 +10,7 @@ } else { @foreach (var category in RoomsWithTypes.OrderBy(x => x.Value.Count)) { - <RoomListCategory Category="@category" GlobalProfile="@GlobalProfile"></RoomListCategory> + <RoomListCategory Category="@category" GlobalProfile="@GlobalProfile" Homeserver="@Homeserver"></RoomListCategory> } } @@ -35,6 +35,9 @@ else { } [Parameter] + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + [Parameter] public UserProfileResponse? GlobalProfile { get; set; } [Parameter] diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
index 1f5ce89..1ab0a1a 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListCategory.razor
@@ -1,12 +1,12 @@ +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using MatrixUtils.Web.Classes.Constants -@using LibMatrix.EventTypes.Spec.State @using LibMatrix.Responses @using MatrixUtils.Abstractions <details open> <summary>@RoomType (@Rooms.Count)</summary> @foreach (var room in Rooms) { <div class="room-list-item"> - <RoomListItem RoomInfo="@room" ShowOwnProfile="@(RoomType == "Room")"></RoomListItem> + <RoomListItem RoomInfo="@room" ShowOwnProfile="@(RoomType == "Room")" Homeserver="@Homeserver"/> @* @if (RoomVersionDangerLevel(room) != 0 && *@ @* (room.StateEvents.FirstOrDefault(x=>x.Type == "m.room.power_levels")?.TypedContent is RoomPowerLevelEventContent powerLevels && powerLevels.UserHasPermission(Homeserver.UserId, "m.room.tombstone"))) { *@ @* <MatrixUtils.Web.Shared.SimpleComponents.LinkButton Color="@(RoomVersionDangerLevel(room) == 2 ? "#ff0000" : "#ff8800")" href="@($"/Rooms/Create?Import={room.Room.RoomId}")">Upgrade room</MatrixUtils.Web.Shared.SimpleComponents.LinkButton> *@ @@ -14,10 +14,11 @@ <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Timeline")">View timeline</LinkButton> <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/View")">View state</LinkButton> <LinkButton href="@($"/Rooms/{room.Room.RoomId}/State/Edit")">Edit state</LinkButton> + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Upgrade")" Color="#888800">Upgrade/replace room</LinkButton> <LinkButton href="@($"/Tools/LeaveRoom?roomId={room.Room.RoomId}")" Color="#FF0000">Leave room</LinkButton> @if (room.CreationEventContent?.Type == "m.space") { - <RoomListSpace Space="@room"></RoomListSpace> + <RoomListSpace Space="@room" Homeserver="@Homeserver"/> } else if (room.CreationEventContent?.Type == "support.feline.policy.lists.msc.v1" || RoomType == "org.matrix.mjolnir.policy") { <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")">Manage policies</LinkButton> @@ -35,9 +36,9 @@ [Parameter] public UserProfileResponse? GlobalProfile { get; set; } - [CascadingParameter] - public AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; - + [Parameter] + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private string RoomType => Category.Key; private List<RoomInfo> Rooms => Category.Value; diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index 6954990..471f586 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -35,15 +35,18 @@ set => _breadcrumbs = value; } + [Parameter] + public required AuthenticatedHomeserverGeneric Homeserver { get; set; } + private ObservableCollection<RoomInfo> Children { get; set; } = new(); private Collection<RoomInfo> Unjoined { get; set; } = new(); protected override async Task OnInitializedAsync() { if (Breadcrumbs == null) throw new ArgumentNullException(nameof(Breadcrumbs)); + if (Homeserver is null) throw new ArgumentNullException(nameof(Homeserver)); await Task.Delay(Random.Shared.Next(1000, 10000)); - var rooms = Space.Room.AsSpace.GetChildrenAsync(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); - var joinedRooms = await hs.GetJoinedRooms(); + var rooms = Space.Room.AsSpace().GetChildrenAsync(); + var joinedRooms = await Homeserver.GetJoinedRooms(); await foreach (var room in rooms) { if (Breadcrumbs.Contains(room.RoomId)) continue; var roomInfo = KnownRooms.FirstOrDefault(x => x.Room.RoomId == room.RoomId); @@ -51,10 +54,12 @@ roomInfo = new RoomInfo(room); KnownRooms.Add(roomInfo); } - if(joinedRooms.Any(x=>x.RoomId == room.RoomId)) + + if (joinedRooms.Any(x => x.RoomId == room.RoomId)) Children.Add(roomInfo); else Unjoined.Add(roomInfo); } + await base.OnInitializedAsync(); } diff --git a/MatrixUtils.Web/Shared/RoomListItem.razor b/MatrixUtils.Web/Shared/RoomListItem.razor
index bfaa900..2d85f64 100644 --- a/MatrixUtils.Web/Shared/RoomListItem.razor +++ b/MatrixUtils.Web/Shared/RoomListItem.razor
@@ -1,19 +1,26 @@ +@using ArcaneLibs @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses @using MatrixUtils.Abstractions @using MatrixUtils.Web.Classes.Constants @if (RoomInfo is not null) { <div class="roomListItem @(HasDangerousRoomVersion ? "dangerousRoomVersion" : HasOldRoomVersion ? "oldRoomVersion" : "")" id="@RoomInfo.Room.RoomId"> @if (OwnMemberState != null) { - @* Class="@("avatar32" + (OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? " highlightChange" : "") + (ChildContent is not null ? " vcenter" : ""))" *@ - <MxcImage Homeserver="hs" Circular="true" Height="32" Width="32" MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/> + <MxcAvatar Homeserver="@Homeserver" Circular="true" Size="32" MxcUri="@(OwnMemberState.AvatarUrl ?? GlobalProfile.AvatarUrl)"/> <span class="centerVertical border75 @(OwnMemberState?.AvatarUrl != GlobalProfile?.AvatarUrl ? "highlightChange" : "")"> @(OwnMemberState?.DisplayName ?? GlobalProfile?.DisplayName ?? "Loading...") </span> <span class="centerVertical noLeftPadding">-></span> } - <MxcImage Circular="true" Height="32" Width="32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> + @* <MxcImage Circular="true" Height="32" Width="32" MxcUri="@RoomInfo.RoomIcon" Style="@(ChildContent is not null ? "vertical-align: middle;" : "")"/> *@ + + @if (!string.IsNullOrWhiteSpace(RoomInfo.RoomIcon)) { + <MxcAvatar Homeserver="@Homeserver" Circular="true" Size="32" MxcUri="@RoomInfo.RoomIcon"/> + } + else { + <img src="@Identicon" width="32" height="32" style="border-radius: 50%;"/> + } <div class="inlineBlock"> <span class="centerVertical">@RoomInfo.RoomName</span> @if (ChildContent is not null) { @@ -42,8 +49,6 @@ else { } } - - [Parameter] public bool ShowOwnProfile { get; set; } = false; @@ -61,27 +66,36 @@ else { OnParametersSetAsync(); } } + + [Parameter] + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } private bool HasOldRoomVersion { get; set; } = false; private bool HasDangerousRoomVersion { get; set; } = false; + private string Identicon { get; set; } + + private static SvgIdenticonGenerator _identiconGenerator = new SvgIdenticonGenerator(); + private static SemaphoreSlim _semaphoreSlim = new(8); private RoomInfo? _roomInfo; private bool _loadData = false; - private static AuthenticatedHomeserverGeneric? hs { get; set; } private bool _hooked; - + private async Task RoomInfoChanged() { + if (RoomInfo is null) return; + Identicon = _identiconGenerator.GenerateAsDataUri(RoomInfo.Room.RoomId); + RoomInfo.PropertyChanged += async (_, a) => { if (a.PropertyName == nameof(RoomInfo.CreationEventContent)) { await CheckRoomVersion(); } - + StateHasChanged(); }; } - + // protected override async Task OnParametersSetAsync() { // if (RoomInfo != null) { // if (!_hooked) { @@ -127,21 +141,24 @@ else { protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - hs ??= await RMUStorage.GetCurrentSessionOrNavigate(); - if (hs is null) return; + // hs ??= await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + // if (hs is null) return; + if (Homeserver is null) { + Console.WriteLine($"RoomListItem called without homeserver"); + } await CheckRoomVersion(); } private async Task LoadOwnProfile() { if (!ShowOwnProfile) return; try { - // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent; - GlobalProfile ??= await hs.GetProfileAsync(hs.UserId); + // OwnMemberState ??= (await RoomInfo.GetStateEvent("m.room.member", hs.UserId)).TypedContent as RoomMemberEventContent; + GlobalProfile ??= await Homeserver.GetProfileAsync(Homeserver.UserId); } catch (MatrixException e) { if (e is { ErrorCode: "M_FORBIDDEN" }) { - Console.WriteLine($"Failed to get profile for {hs.UserId}: {e.Message}"); + Console.WriteLine($"Failed to get profile for {Homeserver.UserId}: {e.Message}"); ShowOwnProfile = false; } else { @@ -151,8 +168,8 @@ else { } private async Task CheckRoomVersion() { - if (RoomInfo?.CreationEventContent is null) return; - + if (RoomInfo?.CreationEventContent is null) return; + var ce = RoomInfo.CreationEventContent; if (int.TryParse(ce.RoomVersion, out var rv)) { if (rv < 10) @@ -163,7 +180,7 @@ else { if (RoomConstants.DangerousRoomVersions.Contains(ce.RoomVersion)) { HasDangerousRoomVersion = true; - // RoomName = "Dangerous room: " + RoomName; + // RoomName = "Dangerous room: " + RoomName; } } diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index 08aeffe..f107eb3 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -1,5 +1,5 @@ @using LibMatrix -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses <h3>BaseTimelineItem</h3> diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
index 0488e36..d1984dd 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @inherits BaseTimelineItem @if (currentEventContent is not null) { diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
index bdd6104..5d09603 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @inherits BaseTimelineItem @if (currentEventContent is not null) { diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
index 3b18b95..e5a5650 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Responses @inherits BaseTimelineItem diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
index 81956b0..98b5a6d 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -15,7 +15,7 @@ } case "m.image": { <i>@currentEventContent.Body</i><br/> - <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)"> + @* <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)"> *@ break; } default: { diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
index f3e6c7e..aeb987a 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @inherits BaseTimelineItem <i> diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
index 63594a9..c342c83 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @inherits BaseTimelineItem @if (currentEventContent is not null) { diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
index f70d563..467c644 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
@@ -1,5 +1,5 @@ @using ArcaneLibs.Extensions -@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.RoomInfo @inherits BaseTimelineItem @if (currentEventContent is not null) { diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor
index d4652b2..fd2fdec 100644 --- a/MatrixUtils.Web/Shared/UserListItem.razor +++ b/MatrixUtils.Web/Shared/UserListItem.razor
@@ -1,7 +1,12 @@ @using LibMatrix.Responses @using ArcaneLibs <div style="background-color: #ffffff11; border-radius: 25px; margin: 8px; width: fit-Content;"> - <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@(string.IsNullOrWhiteSpace(User?.AvatarUrl) ? _identiconGenerator.GenerateAsDataUri(UserId) : User.AvatarUrl)"/> + @if (!string.IsNullOrWhiteSpace(User?.AvatarUrl)) { + <MxcAvatar Homeserver="@_homeserver" Size="32" Circular="true" MxcUri="@User.AvatarUrl"/> + } + else { + <img style="@(ChildContent is not null ? "vertical-align: baseline;" : "") width: 32px; height: 32px; border-radius: 50%;" src="@_identiconGenerator.GenerateAsDataUri(UserId)"/> + } <span style="vertical-align: middle; margin-right: 8px; border-radius: 75px;">@User?.DisplayName</span> <div style="display: inline-block;"> @@ -23,20 +28,28 @@ [Parameter] public string UserId { get; set; } - private AuthenticatedHomeserverGeneric _homeserver = null!; + [Parameter] + public AuthenticatedHomeserverGeneric _homeserver { get; set; } - private SvgIdenticonGenerator _identiconGenerator = new(); + private static SvgIdenticonGenerator _identiconGenerator = new(); protected override async Task OnInitializedAsync() { - _homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); - if (_homeserver is null) return; + // _homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + // if (_homeserver is null) return; if (User == null) { if (UserId == null) { throw new ArgumentNullException(nameof(UserId)); } - User = await _homeserver.GetProfileAsync(UserId); + try { + User = await _homeserver.GetProfileAsync(UserId); + } + catch (Exception) { + User = new() { + DisplayName = UserId + }; + } } await base.OnInitializedAsync(); diff --git a/MatrixUtils.Web/_Imports.razor b/MatrixUtils.Web/_Imports.razor
index 81c7874..b5a1316 100644 --- a/MatrixUtils.Web/_Imports.razor +++ b/MatrixUtils.Web/_Imports.razor
@@ -1,13 +1,10 @@ @using System.Net.Http @using System.Net.Http.Json -@* @using Blazored.LocalStorage *@ @using LibMatrix.Services @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 MatrixUtils.Web @using MatrixUtils.Web.Classes @using MatrixUtils.Web.Shared @@ -16,8 +13,8 @@ @using Microsoft.JSInterop @inject NavigationManager NavigationManager -@inject RMUStorageWrapper RMUStorage -@inject HomeserverProviderService hsProvider +@inject RmuSessionStore sessionStore +@inject HomeserverProviderService HsProvider @inject TieredStorageService TieredStorage -@inject HomeserverResolverService hsResolver -@inject IJSRuntime JSRuntime +@inject HomeserverResolverService HsResolver +@inject IJSRuntime JsRuntime diff --git a/MatrixUtils.Web/appsettings.Development.json b/MatrixUtils.Web/appsettings.Development.json
index 1ca99ed..1555d4e 100644 --- a/MatrixUtils.Web/appsettings.Development.json +++ b/MatrixUtils.Web/appsettings.Development.json
@@ -3,7 +3,10 @@ "LogLevel": { "Default": "Trace", "System": "Information", - "Microsoft": "Information" + "Microsoft": "Information", + "Microsoft.AspNetCore.StaticAssets": "Warning", + "Microsoft.AspNetCore.EndpointMiddleware": "Warning", + "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information" } } } diff --git a/MatrixUtils.Web/appsettings.json b/MatrixUtils.Web/appsettings.json
index 29d3614..f33cc65 100644 --- a/MatrixUtils.Web/appsettings.json +++ b/MatrixUtils.Web/appsettings.json
@@ -3,7 +3,8 @@ "LogLevel": { "Default": "Trace", //debug "System": "Information", - "Microsoft": "Information" + "Microsoft": "Information", + "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information" } } } diff --git a/MatrixUtils.Web/wwwroot/appsettings.json b/MatrixUtils.Web/wwwroot/appsettings.json
index 1ca99ed..826edbf 100644 --- a/MatrixUtils.Web/wwwroot/appsettings.json +++ b/MatrixUtils.Web/wwwroot/appsettings.json
@@ -3,7 +3,8 @@ "LogLevel": { "Default": "Trace", "System": "Information", - "Microsoft": "Information" + "Microsoft": "Information", + "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information" } } } diff --git a/MatrixUtils.Web/wwwroot/css/app.css b/MatrixUtils.Web/wwwroot/css/app.css
index 3fac9ca..4511b3a 100644 --- a/MatrixUtils.Web/wwwroot/css/app.css +++ b/MatrixUtils.Web/wwwroot/css/app.css
@@ -1,6 +1,11 @@ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); @import url('jetbrains-mono/jetbrains-mono.css'); +:root { + /*--bs-table-hover-bg: rgba(0, 0, 0, 0.75);*/ + --bs-table-hover-bg: #FF00FF; +} + .avatar48 { width: 48px; height: 48px; diff --git a/MatrixUtils.Web/wwwroot/index.html b/MatrixUtils.Web/wwwroot/index.html
index 5182193..0a80cff 100644 --- a/MatrixUtils.Web/wwwroot/index.html +++ b/MatrixUtils.Web/wwwroot/index.html
@@ -12,6 +12,7 @@ <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png"/> <link href="favicon.png" rel="icon" type="image/png"/> <link href="MatrixUtils.Web.styles.css" rel="stylesheet"/> + <link rel="preload" id="webassembly"/> </head> <body> @@ -29,16 +30,6 @@ <a class="dismiss">🗙</a> </div> <script> - function BlazorFocusElement(element) { - if (element == null) return; - if (element instanceof HTMLElement) { - console.log(element); - element.focus(); - } else if (element.hasOwnProperty("__internalId")) { - console.log("Element is not an HTMLElement", element); - } - } - function getWidth(element) { console.log("getWidth", element); if (element == null) return 0; @@ -57,9 +48,25 @@ height: window.innerHeight }; } + + setImageStream = async (element, imageStream) => { + if (!(element instanceof HTMLElement)) { + console.error("Element is not an HTMLElement", element); + return; + } + + const arrayBuffer = await imageStream.arrayBuffer(); + const blob = new Blob([arrayBuffer]); + const url = URL.createObjectURL(blob); + const image = document.getElementById(imageElementId); + image.onload = () => { + URL.revokeObjectURL(url); + } + image.src = url; + } </script> <script src="_framework/blazor.webassembly.js"></script> -<!-- <script>navigator.serviceWorker.register('service-worker.js');</script>--> + <!-- <script>navigator.serviceWorker.register('service-worker.js');</script>--> <script src="sw-registrator.js"></script> </body> diff --git a/MatrixUtils.Web/wwwroot/sw-registrator.js b/MatrixUtils.Web/wwwroot/sw-registrator.js
index 94b96b2..67aa5cb 100644 --- a/MatrixUtils.Web/wwwroot/sw-registrator.js +++ b/MatrixUtils.Web/wwwroot/sw-registrator.js
@@ -8,7 +8,7 @@ window.updateAvailable = new Promise((resolve, reject) => { return; } - navigator.serviceWorker.register('/service-worker.js') + navigator.serviceWorker.register('/service-worker.js', {updateViaCache: 'none'}) .then(registration => { console.info(`Service worker registration successful (scope: ${registration.scope})`); diff --git a/MatrixUtils.sln b/MatrixUtils.sln new file mode 100644
index 0000000..032c49b --- /dev/null +++ b/MatrixUtils.sln
@@ -0,0 +1,489 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web", "MatrixUtils.Web\MatrixUtils.Web.csproj", "{D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Web.Server", "MatrixUtils.Web.Server\MatrixUtils.Web.Server.csproj", "{F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Desktop", "MatrixUtils.Desktop\MatrixUtils.Desktop.csproj", "{27C08A4F-5AF0-4C2C-AFCB-050E3388C116}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.LibDMSpace", "MatrixUtils.LibDMSpace\MatrixUtils.LibDMSpace.csproj", "{EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.Abstractions", "MatrixUtils.Abstractions\MatrixUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LibMatrix", "LibMatrix", "{933DC8A6-8B1F-46BF-9046-4B636AA46469}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{84BE90C4-2FDE-4A48-B154-58926EF24846}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "LibMatrix\ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{EC5536AB-0613-4CB5-B22B-822A3DBB112A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "LibMatrix\ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{0C542A8E-54B6-4A20-B7A9-8C7190A0C232}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "LibMatrix\ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{48AF8AC7-5F59-4401-B173-523D37FDD7A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "LibMatrix\ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "LibMatrix\ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "LibMatrix\ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{D6315791-949B-4501-AA95-50516DE899C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "LibMatrix\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{03466515-77CC-49E4-90E5-9A21EDD0A644}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "LibMatrix\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj", "{0336306C-285A-4810-9253-5C5F0373992E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix\LibMatrix.csproj", "{D7E5B226-114C-4747-9277-A4D6341A16FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B37F87A8-B5E2-4724-800C-F5D9A91F35C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "LibMatrix\Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{D293AFEC-8322-4FEC-8425-143B5FE10D0F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{80828C75-9C5B-442F-86A4-8CE9D85E811C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "LibMatrix\Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{FA6A9923-419A-40E1-8A32-30DD906E5025}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "LibMatrix\Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "LibMatrix\Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{CC87DFFB-EE19-4147-9212-4FAF16D79AD5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "LibMatrix\Utilities\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{DBCE6260-052E-46F9-ACCD-059AA51B8A48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.JsonSerializerContextGenerator", "LibMatrix\Utilities\LibMatrix.JsonSerializerContextGenerator\LibMatrix.JsonSerializerContextGenerator.csproj", "{7AA3CDF9-D1F6-4A12-BA47-EB721F353701}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator", "LibMatrix\Utilities\LibMatrix.TestDataGenerator\LibMatrix.TestDataGenerator.csproj", "{D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{72D44C6C-1BC7-4310-B1A9-1169C0812E33}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.DmSpaced", "MatrixUtils.DmSpaced\MatrixUtils.DmSpaced.csproj", "{CDBE012E-B48B-4F9D-8CA4-99F6328E9630}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MxApiExtensions", "MxApiExtensions", "{0641F1C8-8518-4C67-B385-832745C063FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes.LibMatrix", "MxApiExtensions\MxApiExtensions.Classes.LibMatrix\MxApiExtensions.Classes.LibMatrix.csproj", "{3BD05B05-86DE-4680-A7A0-5A326E41E776}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions.Classes", "MxApiExtensions\MxApiExtensions.Classes\MxApiExtensions.Classes.csproj", "{98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExtensions\MxApiExtensions\MxApiExtensions.csproj", "{44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{CEECE820-1BA9-4E29-8668-25967B3E712B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Federation", "LibMatrix\LibMatrix.Federation\LibMatrix.Federation.csproj", "{8F154875-96EE-4BE5-8456-F5EBB2516C1C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.FederationTest", "LibMatrix\Utilities\LibMatrix.FederationTest\LibMatrix.FederationTest.csproj", "{960CC2DF-BB1A-4164-A895-834F81B3A113}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixUtils.RoomUpgradeCLI", "MatrixUtils.RoomUpgradeCLI\MatrixUtils.RoomUpgradeCLI.csproj", "{F0F10F51-4883-4C70-80D2-24D3AA8C0096}" +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 + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x64.Build.0 = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Debug|x86.Build.0 = Debug|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|Any CPU.Build.0 = Release|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x64.ActiveCfg = Release|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x64.Build.0 = Release|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x86.ActiveCfg = Release|Any CPU + {D38DA95D-DD83-4340-96A4-6F59FC6AE3D9}.Release|x86.Build.0 = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x64.Build.0 = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Debug|x86.Build.0 = Debug|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|Any CPU.Build.0 = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x64.ActiveCfg = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x64.Build.0 = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x86.ActiveCfg = Release|Any CPU + {F997F26F-2EC1-4D18-B3DD-C46FB2AD65C0}.Release|x86.Build.0 = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x64.ActiveCfg = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x64.Build.0 = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x86.ActiveCfg = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Debug|x86.Build.0 = Debug|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|Any CPU.Build.0 = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x64.ActiveCfg = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x64.Build.0 = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x86.ActiveCfg = Release|Any CPU + {27C08A4F-5AF0-4C2C-AFCB-050E3388C116}.Release|x86.Build.0 = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x64.ActiveCfg = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x64.Build.0 = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x86.ActiveCfg = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Debug|x86.Build.0 = Debug|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|Any CPU.Build.0 = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x64.ActiveCfg = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x64.Build.0 = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x86.ActiveCfg = Release|Any CPU + {EDD2FBAB-2DEC-4527-AE9C-20E21D0D6B14}.Release|x86.Build.0 = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x64.Build.0 = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|x86.Build.0 = Debug|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x64.ActiveCfg = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x64.Build.0 = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x86.ActiveCfg = Release|Any CPU + {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|x86.Build.0 = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x64.Build.0 = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Debug|x86.Build.0 = Debug|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|Any CPU.Build.0 = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x64.ActiveCfg = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x64.Build.0 = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x86.ActiveCfg = Release|Any CPU + {EC5536AB-0613-4CB5-B22B-822A3DBB112A}.Release|x86.Build.0 = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x64.Build.0 = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Debug|x86.Build.0 = Debug|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|Any CPU.Build.0 = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x64.ActiveCfg = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x64.Build.0 = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x86.ActiveCfg = Release|Any CPU + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE}.Release|x86.Build.0 = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x64.Build.0 = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Debug|x86.Build.0 = Debug|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|Any CPU.Build.0 = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x64.ActiveCfg = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x64.Build.0 = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x86.ActiveCfg = Release|Any CPU + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232}.Release|x86.Build.0 = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x64.Build.0 = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Debug|x86.Build.0 = Debug|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|Any CPU.Build.0 = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x64.ActiveCfg = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x64.Build.0 = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x86.ActiveCfg = Release|Any CPU + {48AF8AC7-5F59-4401-B173-523D37FDD7A8}.Release|x86.Build.0 = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x64.ActiveCfg = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x64.Build.0 = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Debug|x86.Build.0 = Debug|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|Any CPU.Build.0 = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x64.ActiveCfg = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x64.Build.0 = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x86.ActiveCfg = Release|Any CPU + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D}.Release|x86.Build.0 = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x64.ActiveCfg = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x64.Build.0 = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x86.ActiveCfg = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Debug|x86.Build.0 = Debug|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|Any CPU.Build.0 = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x64.ActiveCfg = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x64.Build.0 = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x86.ActiveCfg = Release|Any CPU + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72}.Release|x86.Build.0 = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x64.Build.0 = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Debug|x86.Build.0 = Debug|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|Any CPU.Build.0 = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|x64.ActiveCfg = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|x64.Build.0 = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|x86.ActiveCfg = Release|Any CPU + {D6315791-949B-4501-AA95-50516DE899C1}.Release|x86.Build.0 = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x64.ActiveCfg = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x64.Build.0 = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x86.ActiveCfg = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Debug|x86.Build.0 = Debug|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|Any CPU.Build.0 = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x64.ActiveCfg = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x64.Build.0 = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x86.ActiveCfg = Release|Any CPU + {03466515-77CC-49E4-90E5-9A21EDD0A644}.Release|x86.Build.0 = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x64.ActiveCfg = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x64.Build.0 = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x86.ActiveCfg = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Debug|x86.Build.0 = Debug|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|Any CPU.Build.0 = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|x64.ActiveCfg = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|x64.Build.0 = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|x86.ActiveCfg = Release|Any CPU + {0336306C-285A-4810-9253-5C5F0373992E}.Release|x86.Build.0 = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x64.Build.0 = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Debug|x86.Build.0 = Debug|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|Any CPU.Build.0 = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x64.ActiveCfg = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x64.Build.0 = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x86.ActiveCfg = Release|Any CPU + {D7E5B226-114C-4747-9277-A4D6341A16FE}.Release|x86.Build.0 = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x64.Build.0 = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Debug|x86.Build.0 = Debug|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|Any CPU.Build.0 = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x64.ActiveCfg = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x64.Build.0 = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x86.ActiveCfg = Release|Any CPU + {D293AFEC-8322-4FEC-8425-143B5FE10D0F}.Release|x86.Build.0 = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x64.Build.0 = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Debug|x86.Build.0 = Debug|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|Any CPU.Build.0 = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x64.ActiveCfg = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x64.Build.0 = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x86.ActiveCfg = Release|Any CPU + {FA6A9923-419A-40E1-8A32-30DD906E5025}.Release|x86.Build.0 = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x64.ActiveCfg = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x64.Build.0 = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x86.ActiveCfg = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Debug|x86.Build.0 = Debug|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|Any CPU.Build.0 = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x64.ActiveCfg = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x64.Build.0 = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x86.ActiveCfg = Release|Any CPU + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.Release|x86.Build.0 = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x64.Build.0 = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Debug|x86.Build.0 = Debug|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|Any CPU.Build.0 = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x64.ActiveCfg = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x64.Build.0 = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x86.ActiveCfg = Release|Any CPU + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5}.Release|x86.Build.0 = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x64.ActiveCfg = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x64.Build.0 = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x86.ActiveCfg = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Debug|x86.Build.0 = Debug|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|Any CPU.Build.0 = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x64.ActiveCfg = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x64.Build.0 = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x86.ActiveCfg = Release|Any CPU + {DBCE6260-052E-46F9-ACCD-059AA51B8A48}.Release|x86.Build.0 = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x64.Build.0 = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Debug|x86.Build.0 = Debug|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|Any CPU.Build.0 = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x64.ActiveCfg = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x64.Build.0 = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x86.ActiveCfg = Release|Any CPU + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701}.Release|x86.Build.0 = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x64.Build.0 = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Debug|x86.Build.0 = Debug|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|Any CPU.Build.0 = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x64.ActiveCfg = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x64.Build.0 = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x86.ActiveCfg = Release|Any CPU + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9}.Release|x86.Build.0 = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x64.ActiveCfg = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x64.Build.0 = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x86.ActiveCfg = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Debug|x86.Build.0 = Debug|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|Any CPU.Build.0 = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x64.ActiveCfg = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x64.Build.0 = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x86.ActiveCfg = Release|Any CPU + {72D44C6C-1BC7-4310-B1A9-1169C0812E33}.Release|x86.Build.0 = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x64.Build.0 = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Debug|x86.Build.0 = Debug|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|Any CPU.Build.0 = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x64.ActiveCfg = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x64.Build.0 = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x86.ActiveCfg = Release|Any CPU + {CDBE012E-B48B-4F9D-8CA4-99F6328E9630}.Release|x86.Build.0 = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x64.ActiveCfg = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x64.Build.0 = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x86.ActiveCfg = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Debug|x86.Build.0 = Debug|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|Any CPU.Build.0 = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x64.ActiveCfg = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x64.Build.0 = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x86.ActiveCfg = Release|Any CPU + {3BD05B05-86DE-4680-A7A0-5A326E41E776}.Release|x86.Build.0 = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x64.ActiveCfg = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x64.Build.0 = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x86.ActiveCfg = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Debug|x86.Build.0 = Debug|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|Any CPU.Build.0 = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x64.ActiveCfg = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x64.Build.0 = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x86.ActiveCfg = Release|Any CPU + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53}.Release|x86.Build.0 = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x64.ActiveCfg = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x64.Build.0 = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x86.ActiveCfg = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Debug|x86.Build.0 = Debug|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|Any CPU.Build.0 = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x64.ActiveCfg = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x64.Build.0 = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x86.ActiveCfg = Release|Any CPU + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684}.Release|x86.Build.0 = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x64.Build.0 = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Debug|x86.Build.0 = Debug|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|Any CPU.Build.0 = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x64.ActiveCfg = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x64.Build.0 = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x86.ActiveCfg = Release|Any CPU + {CEECE820-1BA9-4E29-8668-25967B3E712B}.Release|x86.Build.0 = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x64.Build.0 = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Debug|x86.Build.0 = Debug|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|Any CPU.Build.0 = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x64.ActiveCfg = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x64.Build.0 = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x86.ActiveCfg = Release|Any CPU + {8F154875-96EE-4BE5-8456-F5EBB2516C1C}.Release|x86.Build.0 = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|Any CPU.Build.0 = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x64.ActiveCfg = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x64.Build.0 = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x86.ActiveCfg = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Debug|x86.Build.0 = Debug|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|Any CPU.ActiveCfg = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|Any CPU.Build.0 = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x64.ActiveCfg = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x64.Build.0 = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x86.ActiveCfg = Release|Any CPU + {960CC2DF-BB1A-4164-A895-834F81B3A113}.Release|x86.Build.0 = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x64.Build.0 = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Debug|x86.Build.0 = Debug|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|Any CPU.Build.0 = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x64.ActiveCfg = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x64.Build.0 = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x86.ActiveCfg = Release|Any CPU + {F0F10F51-4883-4C70-80D2-24D3AA8C0096}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {84BE90C4-2FDE-4A48-B154-58926EF24846} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {EC5536AB-0613-4CB5-B22B-822A3DBB112A} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {CF252EDF-C5A1-4030-8666-C78AA0A3B7DE} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {0C542A8E-54B6-4A20-B7A9-8C7190A0C232} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {48AF8AC7-5F59-4401-B173-523D37FDD7A8} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {CFAFFBF1-8C85-4FB2-AB32-B5C17AC7BB5D} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {ADFBDF2D-0CEC-43C1-8896-75DCE439CF72} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {D6315791-949B-4501-AA95-50516DE899C1} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {03466515-77CC-49E4-90E5-9A21EDD0A644} = {84BE90C4-2FDE-4A48-B154-58926EF24846} + {0336306C-285A-4810-9253-5C5F0373992E} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {D7E5B226-114C-4747-9277-A4D6341A16FE} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {B37F87A8-B5E2-4724-800C-F5D9A91F35C7} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {D293AFEC-8322-4FEC-8425-143B5FE10D0F} = {B37F87A8-B5E2-4724-800C-F5D9A91F35C7} + {80828C75-9C5B-442F-86A4-8CE9D85E811C} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {FA6A9923-419A-40E1-8A32-30DD906E5025} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {CC87DFFB-EE19-4147-9212-4FAF16D79AD5} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {DBCE6260-052E-46F9-ACCD-059AA51B8A48} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {7AA3CDF9-D1F6-4A12-BA47-EB721F353701} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {D7F9BDF7-35B7-4C84-A34E-B940C1763CC9} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {72D44C6C-1BC7-4310-B1A9-1169C0812E33} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + {3BD05B05-86DE-4680-A7A0-5A326E41E776} = {0641F1C8-8518-4C67-B385-832745C063FD} + {98BB2D9F-BFB9-4E70-93D5-7C4C1205BD53} = {0641F1C8-8518-4C67-B385-832745C063FD} + {44BFB1AD-62FB-4B5B-A5A8-E7D04D731684} = {0641F1C8-8518-4C67-B385-832745C063FD} + {8F154875-96EE-4BE5-8456-F5EBB2516C1C} = {933DC8A6-8B1F-46BF-9046-4B636AA46469} + {960CC2DF-BB1A-4164-A895-834F81B3A113} = {80828C75-9C5B-442F-86A4-8CE9D85E811C} + EndGlobalSection +EndGlobal diff --git a/MxApiExtensions b/MxApiExtensions -Subproject 86e41aa749d961c5731ea52e570cf0f9e8f8d3a +Subproject b4ef05afcfac87ae197ae69bdbae93c3ca4d46b diff --git a/global.json b/global.json
index ecc6db8..6d77f62 100644 --- a/global.json +++ b/global.json
@@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "9.0.0", "rollForward": "latestMajor", "allowPrerelease": true } diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 1abe9e7..7c5086f 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh
@@ -11,4 +11,5 @@ BASE_DIR=`pwd` rm -rf **/bin/Release cd MatrixUtils.Web dotnet publish -c Release -rsync --delete -raP bin/Release/net8.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ +dotnet restore # restore debug deps +rsync --delete -raP bin/Release/net9.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/