about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.idea/.idea.MatrixUtils/.idea/indexLayout.xml1
-rw-r--r--.idea/.idea.MatrixUtils/.idea/vcs.xml3
-rw-r--r--Benchmarks/Benchmarks.csproj6
m---------LibMatrix0
-rw-r--r--MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj4
-rw-r--r--MatrixUtils.Abstractions/RoomInfo.cs24
-rw-r--r--MatrixUtils.Desktop/App.axaml.cs7
-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/ModerationBot.cs21
-rw-r--r--MatrixUtils.LibDMSpace/DMSpaceRoom.cs13
-rw-r--r--MatrixUtils.LibDMSpace/MatrixUtils.LibDMSpace.csproj4
-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.cs72
-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.cs274
-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.razor9
-rw-r--r--MatrixUtils.Web/Classes/LocalStorageProviderService.cs18
-rw-r--r--MatrixUtils.Web/Classes/RmuSessionStore.cs173
-rw-r--r--MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs10
-rw-r--r--MatrixUtils.Web/Classes/UserAuth.cs14
-rw-r--r--MatrixUtils.Web/MatrixUtils.Web.csproj56
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevOptions.razor16
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevUtilities.razor31
-rw-r--r--MatrixUtils.Web/Pages/Dev/WellKnownRes.razor3
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor2
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor11
-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/SynapseRoomShutdownWindowContent.razor235
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor714
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor211
-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/Index.razor22
-rw-r--r--MatrixUtils.Web/Pages/InvalidSession.razor6
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs10
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor14
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor22
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor8
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor1
-rw-r--r--MatrixUtils.Web/Pages/LoginPage.razor8
-rw-r--r--MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor6
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create.razor39
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create2.razor147
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index.razor43
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor877
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs142
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css9
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor48
-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.razor160
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css6
-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.razor82
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateEditor.razor16
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateViewer.razor53
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Timeline.razor11
-rw-r--r--MatrixUtils.Web/Pages/ServerInfo.razor1
-rw-r--r--MatrixUtils.Web/Pages/StreamTest.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor70
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor6
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor12
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor13
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor15
-rw-r--r--MatrixUtils.Web/Pages/Tools/InviteCounter.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/MassCMEBan.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor195
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor18
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor17
-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.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor8
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor17
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/StickerManager.razor80
-rw-r--r--MatrixUtils.Web/Pages/User/Profile.razor36
-rw-r--r--MatrixUtils.Web/Program.cs22
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor17
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor31
-rw-r--r--MatrixUtils.Web/Shared/InputLocalPart.razor50
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor171
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css15
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor60
-rw-r--r--MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor2
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor12
-rw-r--r--MatrixUtils.Web/Shared/UserListItem.razor18
-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.html39
-rw-r--r--MatrixUtils.Web/wwwroot/service-worker.published.js11
-rw-r--r--MatrixUtils.Web/wwwroot/sw-registrator.js4
-rw-r--r--MatrixUtils.sln289
-rw-r--r--global.json7
-rwxr-xr-xscripts/deploy.sh2
137 files changed, 5856 insertions, 1363 deletions
diff --git a/.idea/.idea.MatrixUtils/.idea/indexLayout.xml b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml

index d166ec4..4520708 100644 --- a/.idea/.idea.MatrixUtils/.idea/indexLayout.xml +++ b/.idea/.idea.MatrixUtils/.idea/indexLayout.xml
@@ -2,7 +2,6 @@ <project version="4"> <component name="UserContentModel"> <attachedFolders> - <Path>LibMatrix/LibMatrix/Homeservers</Path> <Path>MatrixRoomUtils.Bot/bot_data</Path> <Path>MatrixRoomUtils.Desktop/bin/Debug/net7.0/mru-desktop</Path> </attachedFolders> diff --git a/.idea/.idea.MatrixUtils/.idea/vcs.xml b/.idea/.idea.MatrixUtils/.idea/vcs.xml
index 94a25f7..df05d42 100644 --- a/.idea/.idea.MatrixUtils/.idea/vcs.xml +++ b/.idea/.idea.MatrixUtils/.idea/vcs.xml
@@ -2,5 +2,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/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj
index 5d584c9..06164d3 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj
@@ -2,16 +2,16 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> -<!-- <PublishAot>true</PublishAot>--> + <!-- <PublishAot>true</PublishAot>--> <InvariantGlobalization>true</InvariantGlobalization> </PropertyGroup> <ItemGroup> - <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> + <PackageReference Include="BenchmarkDotNet" Version="0.15.8"/> </ItemGroup> </Project> diff --git a/LibMatrix b/LibMatrix -Subproject cacabe2b1a15bb7492e23d477ec653513e84d26 +Subproject 0640eba992f95cc45873330b76fadf123202d1c diff --git a/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj b/MatrixUtils.Abstractions/MatrixUtils.Abstractions.csproj
index 96f9fcb..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>net9.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 4b2a53c..1efbd6f 100644 --- a/MatrixUtils.Abstractions/RoomInfo.cs +++ b/MatrixUtils.Abstractions/RoomInfo.cs
@@ -16,22 +16,22 @@ public class RoomInfo : NotifyPropertyChanged { RegisterEventListener(); } - public RoomInfo(GenericRoom room, List<StateEventResponse>? stateEvents) { + public RoomInfo(GenericRoom room, List<MatrixEventResponse>? stateEvents) { Room = room; // _fallbackIcon = identiconGenerator.GenerateAsDataUri(room.RoomId); if (stateEvents is { Count: > 0 }) StateEvents = new(stateEvents!); RegisterEventListener(); ProcessNewItems(stateEvents!); } - + public readonly GenericRoom Room; - public ObservableCollection<StateEventResponse?> StateEvents { get; private set; } = new(); - public ObservableCollection<StateEventResponse?> Timeline { get; private set; } = new(); + public ObservableCollection<MatrixEventResponse?> StateEvents { get; private set; } = new(); + public ObservableCollection<MatrixEventResponse?> Timeline { get; private set; } = new(); private static ConcurrentBag<AuthenticatedHomeserverGeneric> homeserversWithoutEventFormatSupport = new(); // private static SvgIdenticonGenerator identiconGenerator = new(); - public async Task<StateEventResponse?> GetStateEvent(string type, string stateKey = "") { + public async Task<MatrixEventResponse?> GetStateEvent(string type, string stateKey = "") { if (homeserversWithoutEventFormatSupport.Contains(Room.Homeserver)) return await GetStateEventForged(type, stateKey); var @event = StateEvents.FirstOrDefault(x => x?.Type == type && x.StateKey == stateKey); if (@event is not null) return @event; @@ -54,8 +54,8 @@ public class RoomInfo : NotifyPropertyChanged { return @event; } - private async Task<StateEventResponse?> GetStateEventForged(string type, string stateKey = "") { - var @event = new StateEventResponse { + private async Task<MatrixEventResponse?> GetStateEventForged(string type, string stateKey = "") { + var @event = new MatrixEventResponse { RoomId = Room.RoomId, Type = type, StateKey = stateKey, @@ -148,16 +148,16 @@ public class RoomInfo : NotifyPropertyChanged { private void RegisterEventListener() { StateEvents.CollectionChanged += (_, args) => { if (args.NewItems is { Count: > 0 }) - ProcessNewItems(args.NewItems.OfType<StateEventResponse>()); + ProcessNewItems(args.NewItems.OfType<MatrixEventResponse>()); }; } - private void ProcessNewItems(IEnumerable<StateEventResponse?> newItems) { - foreach (StateEventResponse? newState in newItems) { + private void ProcessNewItems(IEnumerable<MatrixEventResponse?> newItems) { + foreach (MatrixEventResponse? newState in newItems) { if (newState is null) continue; // TODO: Benchmark switch statement - - if(newState.StateKey != "") continue; + + if (newState.StateKey != "") continue; if (newState.Type == RoomNameEventContent.EventId && newState.TypedContent is RoomNameEventContent roomNameContent) RoomName = roomNameContent.Name; else if (newState is { Type: RoomAvatarEventContent.EventId, TypedContent: RoomAvatarEventContent roomAvatarContent }) 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/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 a7ff4b9..70b64a8 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>net9.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.2.1" /> - <PackageReference Include="Avalonia.Desktop" Version="11.2.1" /> - <PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" /> - <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" /> + <PackageReference Include="Avalonia" Version="11.3.9" /> + <PackageReference Include="Avalonia.Desktop" Version="11.3.9" /> + <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" /> + <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" /> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> - <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1" /> - <PackageReference Include="Sentry" Version="4.13.0" /> + <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.1"/> </ItemGroup> - - <ItemGroup> - <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.2.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" /> + <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.3.0.6"/> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" /> </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 f8dd598..b02650d 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>net9.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" /> + </ItemGroup> + <ItemGroup> + <Content Include="appsettings*.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + </ItemGroup> </Project> diff --git a/MatrixUtils.DmSpaced/ModerationBot.cs b/MatrixUtils.DmSpaced/ModerationBot.cs
index 6e534fc..17a017b 100644 --- a/MatrixUtils.DmSpaced/ModerationBot.cs +++ b/MatrixUtils.DmSpaced/ModerationBot.cs
@@ -57,9 +57,11 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation await _logRoom?.SendMessageEventAsync(MessageFormatter.FormatWarning($"Control room has no m.room.power_levels?")); continue; } + pls.SetUserPowerLevel(configurationAdmin, pls.GetUserPowerLevel(hs.UserId)); await _controlRoom.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls); } + var syncHelper = new SyncHelper(hs); List<string> admins = new(); @@ -85,7 +87,8 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation x.Type == "m.room.member" && x.StateKey == hs.UserId); logger.LogInformation("Got invite to {RoomId} by {Sender} with reason: {Reason}", args.Key, inviteEvent!.Sender, (inviteEvent.TypedContent as RoomMemberEventContent)!.Reason); - await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}")); + await _logRoom.SendMessageEventAsync( + MessageFormatter.FormatSuccess($"Bot invited to {MessageFormatter.HtmlFormatMention(args.Key)} by {MessageFormatter.HtmlFormatMention(inviteEvent.Sender)}")); if (admins.Contains(inviteEvent.Sender)) { try { await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining {MessageFormatter.HtmlFormatMention(args.Key)}...")); @@ -117,7 +120,8 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation var rules = await engine.GetMatchingPolicies(@event); foreach (var matchedRule in rules) { await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson( - $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}", @matchedRule.OriginalEvent.RawContent)); + $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.OriginalEvent.EventId, roomId: matchedRule.PolicyList.Room.RoomId, displayName: "rule")}", + @matchedRule.OriginalEvent.RawContent)); } if (configuration.DemoMode) { @@ -263,9 +267,10 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation await syncHelper.RunSyncLoopAsync(); } - private async Task LogPolicyChange(StateEventResponse changeEvent) { + private async Task LogPolicyChange(MatrixEventResponse changeEvent) { var room = hs.GetRoom(changeEvent.RoomId!); - var message = MessageFormatter.FormatWarning($"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!"); + var message = MessageFormatter.FormatWarning( + $"Policy change detected in {MessageFormatter.HtmlFormatMessageLink(changeEvent.RoomId, changeEvent.EventId, [hs.ServerName], await room.GetNameOrFallbackAsync())}!"); message = message.ConcatLine(new RoomMessageEventContent(body: $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}") { FormattedBody = $"Policy type: {changeEvent.Type} -> {changeEvent.MappedType.Name}" }); @@ -281,11 +286,12 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation // else { // message = message.ConcatLine(MessageFormatter.FormatSuccess("New rule added!")); // } - message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}", changeEvent.RawContent!)); + message = message.ConcatLine(MessageFormatter.FormatSuccessJson($"{(isUpdated ? "Updated" : isRemoved ? "Removed" : "New")} rule: {changeEvent.StateKey}", + changeEvent.RawContent!)); if (isRemoved || isUpdated) { message = message.ConcatLine(MessageFormatter.FormatSuccessJson("Old content: ", changeEvent.Unsigned.PrevContent!)); } - + await _logRoom.SendMessageEventAsync(message); } @@ -294,5 +300,4 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation public async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("Shutting down bot!"); } - -} +} \ No newline at end of file diff --git a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
index 646a3f3..dc88c75 100644 --- a/MatrixUtils.LibDMSpace/DMSpaceRoom.cs +++ b/MatrixUtils.LibDMSpace/DMSpaceRoom.cs
@@ -32,7 +32,7 @@ public class DMSpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomI else await ImportNativeDMsWithoutLayers(); } - public async Task<List<StateEventResponse>> GetAllActiveLayersAsync() { + public async Task<List<MatrixEventResponse>> GetAllActiveLayersAsync() { var state = await GetFullStateAsListAsync(); return state.Where(x => x.Type == DMSpaceChildLayer.EventId && x.RawContent.ContainsKey("space_id")).ToList(); } @@ -58,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 @@ -117,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() { @@ -140,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 e39440e..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>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LinkIncremental>true</LinkIncremental> @@ -10,6 +10,6 @@ </PropertyGroup> <ItemGroup> - <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/> </ItemGroup> </Project> 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..a095f2e --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Commands/ExecuteCommand.cs
@@ -0,0 +1,72 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +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}..."); + if (rb is RoomUpgradeBuilder { CanUpgrade: false } rub && !(rub.UpgradeOptions.ForceUpgrade || rub.UpgradeOptions.NoopUpgrade)) { + Console.WriteLine("Warning: Import state has determined that you cannot upgrade this room."); + Console.WriteLine(rub.ToJson()); + return; + } + + 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..f6e5199 --- /dev/null +++ b/MatrixUtils.RoomUpgradeCLI/Extensions/RoomBuilderExtensions.cs
@@ -0,0 +1,274 @@ +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 "--restrict-old-room": + if (rb is not RoomUpgradeBuilder upgradeBuilderRestrict) { + throw new InvalidOperationException("Restrict old room can only be used with room upgrades"); + } + + var oldRoom = hs.GetRoom(upgradeBuilderRestrict.OldRoomId); + var createEvt = await oldRoom.GetCreateEventAsync(); + if (createEvt == null) { + throw new InvalidOperationException("Could not get create event for old room " + upgradeBuilderRestrict.OldRoomId); + } + + if (!int.TryParse(createEvt.RoomVersion ?? "1", out int numericVersion)) { + Console.WriteLine("Warning: Could not parse old room version '" + createEvt.RoomVersion + "' as a number! Setting restricted join rule may not work."); + } + else if (numericVersion < 8) { + throw new InvalidOperationException( + "Cannot set restrict old room on rooms with version lower than 8!\nhttps://spec.matrix.org/v1.17/rooms/#feature-matrix" + ); + } + + upgradeBuilderRestrict.UpgradeOptions.RestrictOldRoom = GetBoolArg(args, ref i, true); + break; + 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..edca2f5 --- /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" /> + </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..3a4c822 --- /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.AsEnumerable().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 24401ab..925266b 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>net9.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="9.0.1" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" /> </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 a8cf817..7e8e1c3 100644 --- a/MatrixUtils.Web/App.razor +++ b/MatrixUtils.Web/App.razor
@@ -1,4 +1,5 @@ -<Router AppAssembly="@typeof(App).Assembly"> +@using Microsoft.AspNetCore.Components.WebAssembly.Hosting +<Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> <FocusOnNavigate RouteData="@routeData" Selector="h1"/> @@ -10,3 +11,9 @@ </LayoutView> </NotFound> </Router> + +@code { + + public static WebAssemblyHost Host { get; set; } = null!; + +} diff --git a/MatrixUtils.Web/Classes/LocalStorageProviderService.cs b/MatrixUtils.Web/Classes/LocalStorageProviderService.cs
index 0e99cd4..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<IEnumerable<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/RmuSessionStore.cs b/MatrixUtils.Web/Classes/RmuSessionStore.cs
index 7e5b155..1611b83 100644 --- a/MatrixUtils.Web/Classes/RmuSessionStore.cs +++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -1,5 +1,3 @@ -using System.Text.Json; -using System.Text.Json.Nodes; using LibMatrix; using LibMatrix.Homeservers; using LibMatrix.Services; @@ -28,6 +26,11 @@ public class RmuSessionStore( 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; @@ -41,6 +44,11 @@ public class RmuSessionStore( 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); } @@ -54,25 +62,31 @@ public class RmuSessionStore( SessionId = sessionId }; + await SaveStorage(); if (CurrentSession == null) await SetCurrentSession(sessionId); - else await SaveStorage(); return sessionId; } public async Task RemoveSession(string sessionId) { await LoadStorage(); - logger.LogTrace("Removing session {sessionId}.", sessionId); - var tokens = await GetAllSessions(); - if (tokens == null) { + if (SessionCache.Count == 0) { + logger.LogWarning("No sessions found."); return; } + logger.LogTrace("Removing session {sessionId}.", sessionId); + if ((await GetCurrentSession())?.SessionId == sessionId) - await SetCurrentSession(tokens.First(x => x.Key != sessionId).Key); + await SetCurrentSession(SessionCache.FirstOrDefault(x => x.Key != sessionId).Key); - if (tokens.Remove(sessionId)) - await SaveStorage(); + 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) { @@ -136,16 +150,66 @@ public class RmuSessionStore( } } + 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 MigrateFromMRU(); + if (!hasMigrated) { + await RunMigrations(); + await LoadStorage(true); + } else logger.LogWarning("No sessions found in storage."); + return; } @@ -169,7 +233,8 @@ public class RmuSessionStore( CurrentSession = currentSession; } - private async Task SaveStorage() { + 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, @@ -177,6 +242,7 @@ public class RmuSessionStore( ) ); await storageService.DataStorageProvider.SaveObjectAsync("rmu.session", CurrentSession?.SessionId); + if (log) logger.LogWarning("{count} sessions saved to storage.", SessionCache.Count); } #endregion @@ -184,67 +250,70 @@ public class RmuSessionStore( #region Migrations public async Task RunMigrations() { - await LoadStorage(); - await MigrateFromMRU(); + await MigrateFromMru(); await MigrateAccountsToKeyedStorage(); } - private async Task MigrateFromMRU() { - await LoadStorage(); - logger.LogInformation("Migrating from MRU token namespace!"); + private async Task MigrateFromMru() { 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("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("rmu.tokens", oldTokens); - await dsp.DeleteObjectAsync("tokens"); + 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.tokens")) { - var oldTokens = await dsp.LoadObjectAsync<List<UserAuth>>("mru.tokens"); - if (oldTokens != null) { - await dsp.SaveObjectAsync("rmu.tokens", oldTokens); - await dsp.DeleteObjectAsync("mru.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() { - await LoadStorage(); - logger.LogInformation("Migrating accounts to keyed storage!"); var dsp = storageService.DataStorageProvider!; - if (await dsp.ObjectExistsAsync("rmu.tokens")) { - var tokens = await dsp.LoadObjectAsync<JsonNode>("rmu.tokens") ?? throw new Exception("Failed to load tokens"); - if (tokens is JsonArray array) { - var keyedTokens = array - .Deserialize<UserAuth[]>()! - .ToDictionary(x => x.GetHashCode().ToString(), x => x); - await dsp.SaveObjectAsync("rmu.sessions", keyedTokens); - await dsp.DeleteObjectAsync("rmu.tokens"); - } - } + 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 tokens"); - var sessionId = (await GetAllSessions()) - .FirstOrDefault(x => x.Value.Equals(token)).Key; + 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 not null) { - await dsp.SaveObjectAsync("rmu.session", sessionId); - } - else AddSession(token); + 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 diff --git a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index 7078308..5c0fdab 100644 --- a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs +++ b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
@@ -12,7 +12,7 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate { new() { Name = "My new room", RoomAliasName = "myroom", - InitialState = new List<StateEvent> { + InitialState = new List<MatrixEvent> { new() { Type = "m.room.history_visibility", TypedContent = new RoomHistoryVisibilityEventContent() { @@ -82,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/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 aa9f37f..9100c87 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>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LinkIncremental>true</LinkIncremental> @@ -10,40 +10,44 @@ <UseBlazorWebAssembly>true</UseBlazorWebAssembly> <BlazorEnableCompression>false</BlazorEnableCompression> + <CompressionEnabled>false</CompressionEnabled> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> <BlazorCacheBootResources>false</BlazorCacheBootResources> <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> + <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> + <WasmEnableHotReload>false</WasmEnableHotReload> </PropertyGroup> <!-- 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>--> + <!-- <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> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/> <PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" /> - <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" /> - <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.1" /> - <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.5.39" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0"/> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all"/> + <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="10.0.0"/> + <PackageReference Include="SpawnDev.BlazorJS" Version="2.47.0"/> + <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.26.0"/> </ItemGroup> <ItemGroup> @@ -52,8 +56,8 @@ </ItemGroup> <ItemGroup> -<!-- <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'"/>--> + <!-- <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> diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
index 33e577f..281cf07 100644 --- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor +++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -21,7 +21,7 @@ </p> <details> <summary>Manage local sessions</summary> - + </details> @if (userSettings is not null) { @@ -40,10 +40,14 @@ @code { private RmuSessionStore.Settings? userSettings { get; set; } + protected override async Task OnInitializedAsync() { - // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RmuSessionStore.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() { @@ -58,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"); } @@ -69,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 3b2d533..f6392a4 100644 --- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor +++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,5 +1,8 @@ @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> @@ -14,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> @@ -61,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 @@ -69,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/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index 1906dd8..722f9b3 100644 --- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor +++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -8,7 +8,7 @@ <h3>Known Homeserver List</h3> <hr/> -<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span> +<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClickAsync="@Execute">Execute</LinkButton></span> <span>Stats:</span><br/> <span>Server count: @entries.Count</span><br/> @@ -72,6 +72,7 @@ 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 } }; + public bool HasPolicyServerWellKnown => WellKnownResolutionResult?.PolicyServerWellKnown?.Content is not null and not { PublicKey: null or "" }; } private async Task Execute() { diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index e1b46e2..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/> @@ -11,7 +12,9 @@ else { <h4>Synapse tools</h4> <hr/> <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/> - <a href="/HSAdmin/Synapse/BlockMedia">Block media</a> + <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> diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
index 87600c6..ec2ec54 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -3,7 +3,7 @@ @using LibMatrix.Responses <h3>Manage external profiles</h3> -<LinkButton OnClick="AddAllLocalProfiles">Add local sessions</LinkButton> +<LinkButton OnClickAsync="AddAllLocalProfiles">Add local sessions</LinkButton> @foreach(var p in ExternalProfiles) { diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
index d07ff08..5ccaca9 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -3,6 +3,7 @@ @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes.Spec +@using LibMatrix.StructuredData <h3>Homeserver Administration - Block media</h3> @if (Homeserver is not null) { @@ -24,13 +25,13 @@ <pre>@MxcUri?.ToJson(ignoreNull: true)</pre> @if (Event is not null) { - <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton> + <LinkButton OnClickAsync="@RedactAllEvents">Redact all messages</LinkButton> } @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) { <p>User is a local user!</p> - <LinkButton OnClick="@DeactivateUser">Deactivate User</LinkButton> - <LinkButton OnClick="@QuarantineMediaByUser">Quarantine all media</LinkButton> + <LinkButton OnClickAsync="@DeactivateUser">Deactivate User</LinkButton> + <LinkButton OnClickAsync="@QuarantineMediaByUser">Quarantine all media</LinkButton> } } @@ -95,7 +96,7 @@ } } - private StateEventResponse? Event { get; set; } + private MatrixEventResponse? Event { get; set; } private string? EventJson { get; @@ -139,7 +140,7 @@ private async Task ExpandEventJson() { Console.WriteLine("Expanding event JSON..."); if (!string.IsNullOrWhiteSpace(EventJson)) { - Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson); + Event = JsonSerializer.Deserialize<MatrixEventResponse>(EventJson); MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url; Console.WriteLine($"MXC URL: {MxcUrl}"); 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/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
index d5daf75..b0e6a89 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -1,36 +1,12 @@ +@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)) { - <b>Media options</b> - <br/> - <hr/> - <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"/> - <br/> - - <b>User options</b> - <br/> - <hr/> - <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/> - - <b>Room deletion options</b> - <br/> - <hr/> +@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) { <span>Block room: </span> <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/> <br/> @@ -40,19 +16,63 @@ <span>Force purge room (unsafe): </span> <InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox> <br/> - <span>Warning room User ID (optional): </span> - <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/> - <br/> - @if (!string.IsNullOrWhiteSpace(Context.DeleteRequest.NewRoomUserId)) { + <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> - <LinkButton OnClick="@DeleteRoom">Execute</LinkButton> + @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 { @@ -63,14 +83,52 @@ [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(); @@ -80,11 +138,14 @@ ForcePurge = false }; + public SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom? RoomDetails { get; set; } + public class ExtraDeleteOptions { - // room options + public bool FollowTombstone { get; set; } + + // media options public bool QuarantineLocalMedia { get; set; } public bool QuarantineRemoteMedia { get; set; } - public bool DeleteRemoteMedia { get; set; } // user options @@ -95,9 +156,14 @@ } 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.DeleteId!); + await TaskMap.RemoveValueAsync(Context.RoomId!); + } + catch (Exception e) { + Console.WriteLine("Failed to remove completed room shutdown task from map: " + e); } finally { OnCompleteLock.Release(); @@ -105,9 +171,96 @@ } 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<MatrixEventResponse?> 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
index 79e7357..05899c8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,10 +1,18 @@ @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.Requests @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> @@ -16,121 +24,154 @@ <option value="@item.Key">@item.Value</option> } </select><br/> -<label>Ascending: </label> -<InputCheckbox @bind-Value="Ascending"/><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> - <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> + <summary>Local filtering (slow)</summary> + <SynapseRoomQueryFilter Filter="@Filter"/> </details> -<button class="btn btn-primary" @onclick="Search">Search</button> +<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> - <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;"> +@foreach (var room in Results) { + <div class="room-list-item"> @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ <p> - @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) { - <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span> - <br/> + @if (EnableMultiPurge) { + <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/> + <span> </span> } - else { - <span>@res.RoomId (@res.Name)</span> - <br/> + @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { + <span>@room.CanonicalAlias - </span> } - @if (!string.IsNullOrWhiteSpace(res.Creator)) { - @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@ - <span>Created by @res.Creator</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 OnClick="@(() => { - DeleteRequests.Add(res.RoomId, new() { - RoomId = res.RoomId, - DeleteRequest = new() { - Block = true, - Purge = true, - ForcePurge = false - } - }); - - return Task.CompletedTask; - })">Delete room - </LinkButton> + <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> - <span>@res.StateEvents state events</span><br/> - @if (res.LocalMembers is null) { - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> + + @{ + 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> } - else { - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</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> } - <details> - <summary>Full result data</summary> - <pre>@res.ToJson(ignoreNull: true)</pre> - </details> </div> } @* *@ @@ -146,48 +187,15 @@ @* </ModalWindow> *@ @* } *@ -@foreach(var (roomId, deleteRequest) in DeleteRequests) { - <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> +@foreach (var (roomId, deleteRequest) in DeleteRequests) { + <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => { + DeleteRequests.Remove(roomId); + StateHasChanged(); + })"> + <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> + </ModalWindow> } -<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] @@ -196,27 +204,65 @@ [Parameter] [SupplyParameterFromQuery(Name = "name_search")] - public string SearchTerm { get; set; } + public string? SearchTerm { get; set; } [Parameter] [SupplyParameterFromQuery(Name = "ascending")] - public bool Ascending { get; set; } + 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 string Status { get; set; } - - public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); + 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() { - if (Ascending == null) - Ascending = true; OrderBy ??= "name"; var execute = false; @@ -224,67 +270,95 @@ foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) { switch (key) { case "RoomIdContains": - Filter.RoomIdContains = value[0]!; + Filter.RoomId.Enabled = Filter.RoomId.CheckValueContains = true; + Filter.RoomId.ValueContains = value[0]!; break; case "NameContains": - Filter.NameContains = value[0]!; + Filter.Name.Enabled = Filter.Name.CheckValueContains = true; + Filter.Name.ValueContains = value[0]!; break; case "CanonicalAliasContains": - Filter.CanonicalAliasContains = value[0]!; + Filter.CanonicalAlias.Enabled = Filter.CanonicalAlias.CheckValueContains = true; + Filter.CanonicalAlias.ValueContains = value[0]!; break; case "VersionContains": - Filter.VersionContains = value[0]!; + Filter.Version.Enabled = Filter.Version.CheckValueContains = true; + Filter.Version.ValueContains = value[0]!; break; case "CreatorContains": - Filter.CreatorContains = value[0]!; + Filter.Creator.Enabled = Filter.Creator.CheckValueContains = true; + Filter.Creator.ValueContains = value[0]!; break; case "EncryptionContains": - Filter.EncryptionContains = value[0]!; + Filter.Encryption.Enabled = Filter.Encryption.CheckValueContains = true; + Filter.Encryption.ValueContains = value[0]!; break; case "JoinRulesContains": - Filter.JoinRulesContains = value[0]!; + Filter.JoinRules.Enabled = Filter.JoinRules.CheckValueContains = true; + Filter.JoinRules.ValueContains = value[0]!; break; case "GuestAccessContains": - Filter.GuestAccessContains = value[0]!; + Filter.GuestAccess.Enabled = Filter.GuestAccess.CheckValueContains = true; + Filter.GuestAccess.ValueContains = value[0]!; break; case "HistoryVisibilityContains": - Filter.HistoryVisibilityContains = value[0]!; + Filter.HistoryVisibility.Enabled = Filter.HistoryVisibility.CheckValueContains = true; + Filter.HistoryVisibility.ValueContains = value[0]!; break; case "Federatable": - Filter.Federatable = bool.Parse(value[0]!); - Filter.CheckFederation = true; + Filter.Federation = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; break; case "Public": - Filter.Public = value[0] == "true"; - Filter.CheckPublic = true; + Filter.Public = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; break; case "JoinedMembersGreaterThan": - Filter.JoinedMembersGreaterThan = int.Parse(value[0]!); + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedMembers.GreaterThan = int.Parse(value[0]!); break; case "JoinedMembersLessThan": - Filter.JoinedMembersLessThan = int.Parse(value[0]!); + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedMembers.LessThan = int.Parse(value[0]!); break; case "JoinedLocalMembersGreaterThan": - Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!); + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedLocalMembers.GreaterThan = int.Parse(value[0]!); break; case "JoinedLocalMembersLessThan": - Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!); + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedLocalMembers.LessThan = int.Parse(value[0]!); break; case "StateEventsGreaterThan": - Filter.StateEventsGreaterThan = int.Parse(value[0]!); + Filter.StateEvents.Enabled = Filter.StateEvents.CheckGreaterThan = true; + Filter.StateEvents.GreaterThan = int.Parse(value[0]!); break; case "StateEventsLessThan": - Filter.StateEventsLessThan = int.Parse(value[0]!); + 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(); @@ -293,109 +367,96 @@ private async Task Search() { Results.Clear(); - var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); - if (hs is AuthenticatedHomeserverSynapse synapse) { - Homeserver = 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("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; - if (Results.Count < 100) - Console.WriteLine("Hit: " + room.ToJson(false)); + 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 + }; - 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 - }; - - Results.Add(roomInfo); + 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)}"); + } + } - if (room.JoinedLocalMembers is > 0 and < 100) - roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList(); + Results.Add(roomInfo); - if (Results.Count < 200 || Results.Count % 1000 == 0) { - StateHasChanged(); - await Task.Yield(); - } + 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 async Task DeleteRoom() { - // if (DeleteRequest is { } deleteRequest) { - // var media = await Homeserver.Admin.GetRoomMediaAsync(deleteRequest.RoomId); - // if (deleteRequest.DeleteRequest.QuarantineRemoteMedia) { - // foreach (var remoteMedia in media.Remote) { - // await Homeserver.Admin.QuarantineMediaById(remoteMedia); - // } - // } - // - // if (deleteRequest.DeleteRequest.DeleteRemoteMedia) { - // foreach (var remoteMedia in media.Remote) { - // await Homeserver.Admin.DeleteMediaById(remoteMedia); - // } - // } - // else if (deleteRequest.DeleteRequest.QuarantineLocalMedia) { - // foreach (var localMedia in media.Local) { - // await Homeserver.Admin.QuarantineMediaById(localMedia); - // } - // } - // - // var deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false); - // DeleteRequest = null; - // List<string> alreadyCleanedUsers = []; - // while (true) { - // var status = await Homeserver.Admin.GetRoomDeleteStatus(deleteId.DeleteId); - // DeleteStatuses[deleteRequest.RoomId] = status; - // StateHasChanged(); - // await Task.Delay(5000); - // if (status.Status == "complete") { - // DeleteStatuses.Remove(deleteRequest.RoomId); - // StateHasChanged(); - // break; - // } - // - // if (status.Status == "failed") { - // deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false); - // } - // - // var newCleanedUsers = status.ShutdownRoom?.KickedUsers?.Except(alreadyCleanedUsers).ToList(); - // if (newCleanedUsers is not null) { - // alreadyCleanedUsers.AddRange(newCleanedUsers); - // foreach (var user in newCleanedUsers) { - // if (deleteRequest.DeleteRequest.SuspendLocalUsers) { - // // await Homeserver.Admin.(user); - // } - // - // if (deleteRequest.DeleteRequest.QuarantineLocalUserMedia) { - // await Homeserver.Admin.QuarantineMediaByUserId(user); - // } - // - // if (deleteRequest.DeleteRequest.DeleteLocalUserMedia) { - // var userMedia = Homeserver.Admin.GetUserMediaEnumerableAsync(user); - // await foreach (var mediaEntry in userMedia) { - // await Homeserver.Admin.DeleteMediaById(mediaEntry.MediaId); - // } - // } - // } - // } - // } - // } - // } private readonly Dictionary<string, string> validOrderBy = new() { { "name", "Room name" }, @@ -415,6 +476,143 @@ 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
index e69de29..62941e5 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css +++ 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..3cc5a6a --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -0,0 +1,211 @@ +@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<MatrixEventResponse>? 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/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index 7de4613..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,7 +20,7 @@ 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.Auth; @@ -58,9 +59,9 @@ Small collection of tools to do not-so-everyday things. </td> <td> <p> - <LinkButton OnClick="@(() => ManageUser(session.SessionId))">Manage</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId, 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> @@ -89,7 +90,7 @@ Small collection of tools to do not-so-everyday things. </p> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -118,10 +119,10 @@ Small collection of tools to do not-so-everyday things. </p> </td> <td> - <LinkButton OnClick="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton> + <LinkButton OnClickAsync="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -137,6 +138,8 @@ Small collection of tools to do not-so-everyday things. private const bool _debug = false; #endif + private bool Busy { get; set; } = true; + private class HomepageSessionInfo : RmuSessionStore.SessionInfo { public UserInfo? UserInfo { get; set; } public ServerVersionResponse? ServerVersion { get; set; } @@ -153,7 +156,6 @@ Small collection of tools to do not-so-everyday things. protected override async Task OnInitializedAsync() { Console.WriteLine("Index.OnInitializedAsync"); logger.LogDebug("Initialising index page"); - await sessionStore.RunMigrations(); _currentSession = await sessionStore.GetCurrentSession(); _sessions.Clear(); @@ -242,7 +244,9 @@ 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 { diff --git a/MatrixUtils.Web/Pages/InvalidSession.razor b/MatrixUtils.Web/Pages/InvalidSession.razor
index 1ec99f6..f86d112 100644 --- a/MatrixUtils.Web/Pages/InvalidSession.razor +++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -8,14 +8,14 @@ @if (_auth is not null) { <p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p> - <LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton> - <LinkButton OnClick="@(RemoveUser)">Remove</LinkButton> + <LinkButton OnClickAsync="@(OpenRefreshDialog)">Refresh token</LinkButton> + <LinkButton OnClickAsync="@(RemoveUser)">Remove</LinkButton> @if (_showRefreshDialog) { <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")"> <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox> <br/> - <LinkButton OnClick="TryLogin">Log in</LinkButton> + <LinkButton OnClickAsync="TryLogin">Log in</LinkButton> @if (_loginException is not null) { <pre style="color: red;">@_loginException.RawContent</pre> } diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
index 8831dd1..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/> diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
index 16051b8..c58114e 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
@@ -13,9 +13,10 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti MinimumDelay = TimeSpan.FromMilliseconds(2000), IsInitialSync = false }; + private string _status = "Loading..."; - public ObservableCollection<StateEvent> AccountData { get; set; } = new(); + public ObservableCollection<MatrixEvent> AccountData { get; set; } = new(); public ObservableCollection<RoomInfo> Rooms { get; set; } = new(); public string Status { @@ -29,13 +30,12 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti Status = $"[{DateTime.Now:s}] Syncing..."; await foreach (var response in resp) { Task.Yield(); - Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates"; + Status = + $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates"; await HandleSyncResponse(response); await Task.Yield(); } } - private async Task HandleSyncResponse(SyncResponse resp) { - - } + private async Task HandleSyncResponse(SyncResponse resp) { } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index a974a8f..7199934 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -26,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> @@ -78,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); @@ -109,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; @@ -118,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 b8eb257..ed65e94 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -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 dac9c49..686894c 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -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/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index 79f931b..dd217e9 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -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/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 88577a2..38ede74 100644 --- a/MatrixUtils.Web/Pages/LoginPage.razor +++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -22,8 +22,8 @@ <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/> @@ -44,7 +44,7 @@ <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox> </span> <br/> -<LinkButton OnClick="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton> +<LinkButton OnClickAsync="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton> <br/> <br/> @@ -101,7 +101,7 @@ } </table> <br/> -<LinkButton OnClick="@LoginAll">Log in</LinkButton> +<LinkButton OnClickAsync="@LoginAll">Log in</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
index e30adf6..17dd554 100644 --- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor +++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -19,6 +19,7 @@ else { else if (checkedRooms.Count > 1) { <p>Done!</p> } + @foreach (var (state, rooms) in matchingStates) { <u>@state</u> <br/> @@ -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 021ad18..051d5af 100644 --- a/MatrixUtils.Web/Pages/Rooms/Create.razor +++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -12,10 +12,10 @@ @* <pre Contenteditable="true" @onkeypress="@JsonChanged" content="JsonString">@JsonString</pre> *@ <style> - table.table-top-first-tr tr td:first-child { - vertical-align: top; - } - </style> + table.table-top-first-tr tr td:first-child { + vertical-align: top; + } +</style> <table class="table-top-first-tr"> <tr style="padding-bottom: 16px;"> <td>Preset:</td> @@ -41,7 +41,10 @@ } else { <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox> - <p>(#<FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>:@Homeserver.WhoAmI.UserId.Split(':').Last())</p> + <p>(# + <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox> + :@Homeserver.WhoAmI.UserId.Split(':').Last()) + </p> } </td> </tr> @@ -89,7 +92,8 @@ <td> @* <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/> + <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox> + <br/> <InputFile OnChange="RoomIconFilePicked"></InputFile> </div> </td> @@ -105,19 +109,27 @@ <FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"> - </FancyTextBox>: + </FancyTextBox> + : </td> <td> - <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" + @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" + @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> </td> </tr> } @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) { var _user = user; <tr> - <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td> <td> - <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> + <FancyTextBox Value="@_user" + ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox> + : + </td> + <td> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" + @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> </td> </tr> } @@ -266,6 +278,7 @@ 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")) { @@ -299,7 +312,7 @@ private void InviteMember(string mxid) { if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && Homeserver.UserId != mxid) - creationEvent.InitialState.Add(new StateEvent { + creationEvent.InitialState.Add(new MatrixEvent { Type = "m.room.member", StateKey = mxid, TypedContent = new RoomMemberEventContent { @@ -316,7 +329,7 @@ "m.room.server_acl" => "Server ACL", "m.room.avatar" => "Avatar", _ => key - }; + }; private string GetPermissionFriendlyName(string key) => key switch { "m.reaction" => "Send reaction", @@ -331,6 +344,6 @@ "m.room.pinned_events" => "Pin events", "m.room.server_acl" => "Change server ACLs", _ => key - }; + }; } 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 0373a46..115c903 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -73,7 +73,7 @@ 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 ?? []) { @@ -97,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, @@ -106,9 +107,9 @@ // }; // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); - RunSyncLoop(syncHelper); + _ = RunSyncLoop(syncHelper); // RunSyncLoop(profileSyncHelper); - RunQueueProcessor(); + _ = RunQueueProcessor(); await base.OnInitializedAsync(); } @@ -138,7 +139,7 @@ } else { // Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); - room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events); + room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events); Rooms.Add(room); } @@ -147,11 +148,10 @@ 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}"); @@ -181,7 +181,8 @@ get => _status; set { _status = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -191,7 +192,8 @@ get => _status2; set { _status2 = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -203,26 +205,24 @@ 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)); @@ -231,6 +231,7 @@ $"{sync.Rooms?.Join?.Count ?? 0} new updates!"; 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 9c35673..f2ab186 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -5,159 +5,148 @@ @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.Common @using LibMatrix.EventTypes.Interop.Draupnir @using LibMatrix.EventTypes.Spec.State.RoomInfo - -@using MatrixUtils.Web.Shared.PolicyEditorComponents @using SpawnDev.BlazorJS.WebWorkers +@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents +@using SpawnDev.BlazorJS @inject WebWorkerService WebWorkerService +@inject ILogger<PolicyList> logger +@inject BlazorJSRuntime JsRuntime -<h3>Policy list editor - Editing @(RoomName ?? 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 OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> -<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</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 { - 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> + <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> + } - Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}"); + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans + </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"> - @{ - 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(); + @if (RedundantBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @RedundantBans.Value.ActivePolicies.Count redundant bans + </p> + } - 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()}"); - } - <thead> - <tr> - @foreach (var name in propNames) { - <th>@name</th> - } - <th>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!; - } - @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> - } + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); - @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) { - <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent</LinkButton> - @if (CurrentUserIsDraupnir) { - <LinkButton Color="@(ActiveKicks.ContainsKey(policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(policy))">Kick users @(ActiveKicks.ContainsKey(policy) ? $"({ActiveKicks[policy]})" : null)</LinkButton> - } - } - } - else { - <p>No permission to modify</p> - } - </div> - </td> - </tr> - } - </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> - } + // 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()}"); - Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); - Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); -} + @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> + } -@if (ServerPolicyToMakePermanent is not null) { - <ModalWindow Title="Make policy permanent"> + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent> + } - </ModalWindow> -} + // 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> *@ + // } -@if (MassCreatePolicies) { - <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal> + // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed); + } } @code { @@ -168,50 +157,50 @@ else { private const bool Debug = false; #endif + private bool IsInitialised { get; set; } = false; private bool Loading { get; set; } = true; [Parameter] - public string RoomId { get; set; } + public required string RoomId { get; set; } - private bool _enableAvatars; - private StateEventResponse? _currentlyEditingEvent; - private bool _massCreatePolicies; - private StateEventResponse? _serverPolicyToMakePermanent; - - private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); - - private StateEventResponse? CurrentlyEditingEvent { - get => _currentlyEditingEvent; + [Parameter, SupplyParameterFromQuery] + public bool RenderEventInfo { + get; set { - _currentlyEditingEvent = value; + field = value; StateHasChanged(); } } - public StateEventResponse? ServerPolicyToMakePermanent { - get => _serverPolicyToMakePermanent; + private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new(); + + public MatrixEventResponse? ServerPolicyToMakePermanent { + get; set { - _serverPolicyToMakePermanent = value; + field = value; StateHasChanged(); } } - private AuthenticatedHomeserverGeneric Homeserver { get; set; } - private GenericRoom Room { get; set; } - private RoomPowerLevelEventContent PowerLevels { get; set; } + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private GenericRoom Room { get; set; } = null!; + private RoomPowerLevelEventContent PowerLevels { get; set; } = null!; public bool CurrentUserIsDraupnir { get; set; } - public string? RoomName { get; set; } - public string? RoomAlias { get; set; } - public string? DraupnirShortcode { get; set; } - public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = []; - public bool MassCreatePolicies { - get => _massCreatePolicies; - set { - _massCreatePolicies = value; - StateHasChanged(); - } - } + public Dictionary<MatrixEventResponse, int> ActiveKicks { get; set; } = []; + + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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(); @@ -219,202 +208,456 @@ else { Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; if (Homeserver is null) return; Room = Homeserver.GetRoom(RoomId!); - await Task.WhenAll([ + IsInitialised = true; + StateHasChanged(); + await Task.WhenAll( Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }), - 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(); }), - Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; }), - ]); - await LoadStatesAsync(); - Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); + 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() { - 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); + 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 (MatrixEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue; + collection.RemovedPolicies[key] = policyInfo; + } + } + else { + collection.RemovedPolicies.Remove(key); + if (!collection.ActivePolicies.TryAdd(key, policyInfo)) { + if (MatrixEvent.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); - private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + // return; + logger.LogInformation("LoadStatesAsync: Scanning for redundant policies..."); - private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + 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; - private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // foreach (var (policyInfo, policyContent) in allPolicies) { + // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) { + // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event + // if (MatrixEvent.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); + // } + // } - private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() - ?? type.GetCustomAttributes<MatrixEventAttribute>() - .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + int scanningPolicyCount = 0; + var aggregatedPolicies = PolicyCollections.Values + .Aggregate(new List<MatrixEventResponse>(), (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()))); - private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + 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 => MatrixEvent.Equals(x, y))).ToList(); + original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => MatrixEvent.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); + } - private async Task RemovePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { }); - PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); - await LoadStatesAsync(); + 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(); } - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); - CurrentlyEditingEvent = null; - await LoadStatesAsync(); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(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); } - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { - policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; - await LoadStatesAsync(); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(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); } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + [return: WorkerTransfer] + private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, int start, int end) + => CheckDuplicatePoliciesAsync(policies, start .. end); - // event types, unnamed - private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes - .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> 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>(); - private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes - .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); + foreach (var (policyEvent, policyContent) in toCheck) { + List<MatrixEventResponse> duplicatedBy = []; + List<MatrixEventResponse> madeRedundantBy = []; -#region Draupnir interop + foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) { + if (policyEvent == otherPolicyEvent) continue; // same event + if (MatrixEvent.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 + } - private SemaphoreSlim ss = new(16, 16); + // 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); + } + } - private async Task DraupnirKickMatching(StateEventResponse policy) { - try { - var content = policy.TypedContent! as PolicyRuleEventContent; - if (content is null) return; - if (string.IsNullOrWhiteSpace(content.Entity)) return; + 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 + }); + } - var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId); - var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList(); + // await Task.Delay(1); + } - ActiveKicks.Add(policy, rooms.Count); - StateHasChanged(); - await Task.Delay(500); + await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}"); - // for (int i = 0; i < 12; i++) { - // _ = WebWorkerService.TaskPool.Invoke(WasteCpu); - // } + 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(); + var states = await Room.GetFullStateAsListAsync(); + // PolicyEventsByType.Clear(); - // 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); - // } + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); - 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); + foreach (var type in KnownPolicyTypes) { + if (!PolicyEventsByType.ContainsKey(type)) + PolicyEventsByType.Add(type, new List + <MatrixEventResponse>(16000)); } - } -#region Nasty, nasty internals, please ignore! + 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; + e1 = _spsw.Elapsed; + var targetPolicies = PolicyEventsByType[state.MappedType]; + e2 = _spsw.Elapsed; + if (!firstLoad && targetPolicies.FirstOrDefault(x => MatrixEvent.TypeKeyPairMatches(x, state)) is { } evt) { + e3 = _spsw.Elapsed; + if (MatrixEvent.Equals(evt, state)) { + if (count % 100 == 0) { + await Task.Delay(10); + await Task.Yield(); + } - 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); + 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); } - catch (Exception e) { - Console.WriteLine(e); + 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(); } - 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); - } + 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 List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + + // private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<MatrixEventResponse> 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>() + .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + + private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + + public struct PolicyCollection { + public required string Name { get; init; } + public SpecialViewType ViewType { get; init; } + public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count; + + public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; } + + // public Dictionary<(string Type, string StateKey), MatrixEventResponse> InvalidPolicies { get; set; } + public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; } + public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; } + + public class PolicyInfo { + public required MatrixEventResponse Policy { get; init; } + public required List<MatrixEventResponse> MadeRedundantBy { get; set; } + public required List<MatrixEventResponse> DuplicatedBy { get; set; } } - private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { - Console.WriteLine($"Checking {roomId}..."); - Console.WriteLine(policy.EventId); + public enum SpecialViewType { + None, + Duplicates, + Redundant, } } -#endregion - -#endregion + // 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..6f45041 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -0,0 +1,142 @@ +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(MatrixEventResponse 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, MatrixEventResponse 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, MatrixEventResponse policy) { + Console.WriteLine($"Checking {roomId}..."); + Console.WriteLine(policy.EventId); + } + } + +#endregion + +#endregion +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css deleted file mode 100644
index afe9fb0..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css +++ /dev/null
@@ -1,9 +0,0 @@ -th { - border-width: 1px; -} - -table { - width: fit-content; - border-width: 1px; - vertical-align: middle; -} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
index 982fc5a..ac918a8 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -15,7 +15,11 @@ <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> +<LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy +</LinkButton> @if (Loading) { <p>Loading...</p> @@ -71,16 +75,24 @@ else { } <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> + <LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = policy; + return Task.CompletedTask; + })">Edit + </LinkButton> + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> @if (policy.IsLegacyType) { - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> } @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) { - <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton> + <LinkButton OnClickAsync="@(() => { + ServerPolicyToMakePermanent = policy; + return Task.CompletedTask; + })">Make permanent (wildcard) + </LinkButton> @if (CurrentUserIsDraupnir) { - <LinkButton OnClick="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> + <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> } } else { @@ -144,12 +156,12 @@ else { public string RoomId { get; set; } private bool _enableAvatars; - private StateEventResponse? _currentlyEditingEvent; - private StateEventResponse? _serverPolicyToMakePermanent; + private MatrixEventResponse? _currentlyEditingEvent; + private MatrixEventResponse? _serverPolicyToMakePermanent; - private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); + private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new(); - private StateEventResponse? CurrentlyEditingEvent { + private MatrixEventResponse? CurrentlyEditingEvent { get => _currentlyEditingEvent; set { _currentlyEditingEvent = value; @@ -157,7 +169,7 @@ else { } } - private StateEventResponse? ServerPolicyToMakePermanent { + private MatrixEventResponse? ServerPolicyToMakePermanent { get => _serverPolicyToMakePermanent; set { _serverPolicyToMakePermanent = value; @@ -197,12 +209,12 @@ else { StateHasChanged(); } - private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + private List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; - private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); - private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() @@ -211,24 +223,24 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; - private async Task RemovePolicyAsync(StateEventResponse policyEvent) { + private async Task RemovePolicyAsync(MatrixEventResponse policyEvent) { await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { }); PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); await LoadStatesAsync(); } - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { + private async Task UpdatePolicyAsync(MatrixEventResponse policyEvent) { await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent); CurrentlyEditingEvent = null; await LoadStatesAsync(); } - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { + private async Task UpgradePolicyAsync(MatrixEventResponse policyEvent) { policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; await LoadStatesAsync(); } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); // event types, unnamed private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes 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..b57beae --- /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 MatrixEventResponse? 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(MatrixEventResponse 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..3ded78f --- /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 MatrixEventResponse 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 => MatrixEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.DuplicatedBy.RemoveAll(x => MatrixEvent.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 => MatrixEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => MatrixEvent.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(MatrixEventResponse 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
index 2903ab8..a84ef8c 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -1,12 +1,24 @@ @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>Policy lists </h3> @* <LinkButton href="/Rooms/Create">Create new policy list</LinkButton> *@ +<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> @@ -16,22 +28,17 @@ } <hr/> -<table> +<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading"> <thead> <tr> - <th/> <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> - <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")"> - <span class="oi oi-pencil" aria-hidden="true"></span> - </LinkButton> - </td> <td style="padding-right: 24px;"> <span>@room.RoomName</span> @if (room.IsLegacy) { @@ -50,11 +57,44 @@ <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; } = []; @@ -65,44 +105,39 @@ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; + isLoading = true; Status = "Fetching rooms..."; - - var userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>(); - var serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>(); - var roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>(); - var knownPolicyTypes = (List<string>) [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - - List<GenericRoom> roomsByType = []; + List<Task> _tasks = []; await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) { - roomsByType.Add(room); + // roomsByType.Add(room); Status2 = $"Found {room.RoomId} (MSC3784)..."; + _tasks.Add(Task.Run(async () => { + Rooms.Add(await RoomInfo.FromRoom(room)); + StateHasChanged(); + })); } - List<Task<RoomInfo>> tasks = roomsByType.Select(async room => { - Status2 = $"Fetching room {room.RoomId}..."; - return await RoomInfo.FromRoom(room); - }).ToList(); + await Task.WhenAll(_tasks); - var results = tasks.ToAsyncEnumerable(); - await foreach (var result in results) { - Rooms.Add(result); - StateHasChanged(); - } + 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 => knownPolicyTypes.Contains(x.Type)) + .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); - }) - .ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var room in rooms) { if (room is not null) { @@ -111,31 +146,35 @@ } } + isLoading = false; Status = ""; Status2 = ""; - await base.OnInitializedAsync(); } - private string _status; - - public string Status { - get => _status; + private string? Status { + get; set { - _status = value; + field = value; StateHasChanged(); } } - private string _status2; - - public string Status2 { - get => _status2; + private string? Status2 { + get; set { - _status2 = value; + 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; } @@ -149,12 +188,7 @@ Server } - private static readonly List<string> userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>(); - private static readonly List<string> serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>(); - private static readonly List<string> roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>(); - private static readonly List<string> allKnownPolicyTypes = [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - - public static async Task<RoomInfo> FromRoom(GenericRoom room, List<StateEventResponse>? state = null, bool legacy = false) { + public static async Task<RoomInfo> FromRoom(GenericRoom room, List<MatrixEventResponse>? state = null, bool legacy = false) { state ??= await room.GetFullStateAsListAsync(); return new RoomInfo() { Room = room, @@ -165,12 +199,40 @@ ?? room.RoomId, Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode, PolicyCounts = new() { - { PolicyType.User, state.Count(x => userEventTypes.Contains(x.Type)) }, - { PolicyType.Server, state.Count(x => serverEventTypes.Contains(x.Type)) }, - { PolicyType.Room, state.Count(x => roomEventTypes.Contains(x.Type)) } + { 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/PolicyLists.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css deleted file mode 100644
index f9b5b3f..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css +++ /dev/null
@@ -1,6 +0,0 @@ -table, th, td { - border-width: 1px; -} -td { - padding: 8px; -} \ 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..2b1d90a --- /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<MatrixEvent>>() { + { "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<MatrixEvent>(json)); *@ + @* StateHasChanged(); *@ + @* })"></FancyTextBox> *@ + <FancyTextBoxLazyJson T="MatrixEvent" 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<MatrixEvent> events, MatrixEvent 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 46e39ed..93df5a9 100644 --- a/MatrixUtils.Web/Pages/Rooms/Space.razor +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -30,7 +30,7 @@ private GenericRoom? Room { get; set; } - private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>(); + private MatrixEventResponse[] States { get; set; } = Array.Empty<MatrixEventResponse>(); private List<RoomInfo> Rooms { get; } = new(); private List<string> ServersInSpace { get; } = new(); private string? NewRoomId { get; set; } @@ -61,6 +61,7 @@ } }); } + break; } case "m.room.member": { @@ -68,44 +69,46 @@ if (!ServersInSpace.Contains(serverName)) { ServersInSpace.Add(serverName); } + break; } } } + await base.OnInitializedAsync(); - // var state = await Room.GetStateAsync(""); - // if (state is not null) { - // // Console.WriteLine(state.Value.ToJson()); - // States = state.Value.Deserialize<StateEventResponse[]>()!; - // - // foreach (var stateEvent in States) { - // if (stateEvent.Type == "m.space.child") { - // // if (stateEvent.Content.ToJson().Length < 5) return; - // var roomId = stateEvent.StateKey; - // var room = hs.GetRoom(roomId); - // if (room is not null) { - // Rooms.Add(room); - // } - // } - // else if (stateEvent.Type == "m.room.member") { - // var serverName = stateEvent.StateKey.Split(':').Last(); - // if (!ServersInSpace.Contains(serverName)) { - // ServersInSpace.Add(serverName); - // } - // } - // } + // var state = await Room.GetStateAsync(""); + // if (state is not null) { + // // Console.WriteLine(state.Value.ToJson()); + // States = state.Value.Deserialize<MatrixEventResponse[]>()!; + // + // foreach (var stateEvent in States) { + // if (stateEvent.Type == "m.space.child") { + // // if (stateEvent.Content.ToJson().Length < 5) return; + // var roomId = stateEvent.StateKey; + // var room = hs.GetRoom(roomId); + // if (room is not null) { + // Rooms.Add(room); + // } + // } + // else if (stateEvent.Type == "m.room.member") { + // var serverName = stateEvent.StateKey.Split(':').Last(); + // if (!ServersInSpace.Contains(serverName)) { + // ServersInSpace.Add(serverName); + // } + // } + // } - // if(state.Value.TryGetProperty("Type", out var Type)) - // { - // } - // else - // { - // //this is fine, apprently... - // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); - // } + // if(state.Value.TryGetProperty("Type", out var Type)) + // { + // } + // else + // { + // //this is fine, apprently... + // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); + // } - // await base.OnInitializedAsync(); + // await base.OnInitializedAsync(); } private async Task JoinAllRooms() { @@ -120,29 +123,30 @@ var room = Room!.Homeserver.GetRoom(roomId); if (room is null) return; try { - await room.JoinAsync(ServersInSpace.ToArray()); + await room.JoinAsync(ServersInSpace.Take(10).ToArray()); var joined = false; while (!joined) { var ce = await room.GetCreateEventAsync(); - if(ce is null) continue; + 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); - } + 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); + await Room.AsSpace().AddChildByIdAsync(NewRoomId); } } diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 51cb265..47146bc 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -37,8 +37,8 @@ [Parameter] public string? RoomId { get; set; } - public List<StateEventResponse> FilteredEvents { get; set; } = new(); - public List<StateEventResponse> Events { get; set; } = new(); + public List<MatrixEventResponse> FilteredEvents { get; set; } = new(); + public List<MatrixEventResponse> Events { get; set; } = new(); public string status = ""; protected override async Task OnInitializedAsync() { @@ -58,7 +58,7 @@ var StateLoaded = 0; var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); await foreach (var _ev in response) { - // var e = new StateEventResponse { + // var e = new MatrixEventResponse { // Type = _ev.Type, // StateKey = _ev.StateKey, // OriginServerTs = _ev.OriginServerTs, @@ -68,6 +68,7 @@ if (string.IsNullOrEmpty(_ev.StateKey)) { FilteredEvents.Add(_ev); } + StateLoaded++; if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; @@ -103,11 +104,12 @@ public string content { get; set; } public long origin_server_ts { get; set; } public string state_key { get; set; } + public string type { get; set; } - // public string Sender { get; set; } - // public string EventId { get; set; } - // public string UserId { get; set; } - // public string ReplacesState { get; set; } + // public string Sender { get; set; } + // public string EventId { get; set; } + // public string UserId { get; set; } + // public string ReplacesState { get; set; } } public bool ShowMembershipEvents { diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
index c8b87d3..16b1d3d 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
@@ -12,20 +12,20 @@ <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> - <tr> - <th scope="col">Type</th> - <th scope="col">Content</th> - </tr> - </thead> - <tbody> - @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) { <tr> - <td>@stateEvent.Type</td> - <td style="max-width: fit-Content;"> - <pre>@stateEvent.RawContent.ToJson()</pre> - </td> + <th scope="col">Type</th> + <th scope="col">Content</th> </tr> - } + </thead> + <tbody> + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } </tbody> </table> @@ -34,20 +34,20 @@ <summary>@group.Key</summary> <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> - <tr> - <th scope="col">Type</th> - <th scope="col">Content</th> - </tr> - </thead> - <tbody> - @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) { <tr> - <td>@stateEvent.Type</td> - <td style="max-width: fit-Content;"> - <pre>@stateEvent.RawContent.ToJson()</pre> - </td> + <th scope="col">Type</th> + <th scope="col">Content</th> </tr> - } + </thead> + <tbody> + @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } </tbody> </table> </details> @@ -64,8 +64,8 @@ [Parameter] public string? RoomId { get; set; } - public List<StateEventResponse> FilteredEvents { get; set; } = new(); - public List<StateEventResponse> Events { get; set; } = new(); + public List<MatrixEventResponse> FilteredEvents { get; set; } = new(); + public List<MatrixEventResponse> Events { get; set; } = new(); public string status = ""; protected override async Task OnInitializedAsync() { @@ -88,6 +88,7 @@ if (string.IsNullOrEmpty(_ev.StateKey)) { FilteredEvents.Add(_ev); } + StateLoaded++; if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
index 108581c..f9137b0 100644 --- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -3,6 +3,7 @@ @using LibMatrix @using LibMatrix.EventTypes.Spec @using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Responses <h3>RoomManagerTimeline</h3> <hr/> <p>Loaded @Events.Count events...</p> @@ -21,7 +22,7 @@ public string RoomId { get; set; } private List<TimelineEventItem> Events { get; } = new(); - private List<StateEventResponse> RawEvents { get; } = new(); + private List<MatrixEventResponse> RawEvents { get; } = new(); private AuthenticatedHomeserverGeneric? Homeserver { get; set; } @@ -43,9 +44,9 @@ await base.OnInitializedAsync(); } - // private StateEventResponse GetProfileEventBefore(StateEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender); + // private MatrixEventResponse GetProfileEventBefore(MatrixEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender); - private Type ComponentType(StateEvent Event) => Event.Type switch { + private Type ComponentType(MatrixEvent Event) => Event.Type switch { RoomCanonicalAliasEventContent.EventId => typeof(TimelineCanonicalAliasItem), RoomHistoryVisibilityEventContent.EventId => typeof(TimelineHistoryVisibilityItem), RoomTopicEventContent.EventId => typeof(TimelineRoomTopicItem), @@ -56,9 +57,9 @@ // RoomMessageReactionEventContent.EventId => typeof(ComponentBase), _ => typeof(TimelineUnknownItem) }; - + private class TimelineEventItem : ComponentBase { - public StateEventResponse Event { get; set; } + public MatrixEventResponse Event { get; set; } public Type Type { get; set; } } diff --git a/MatrixUtils.Web/Pages/ServerInfo.razor b/MatrixUtils.Web/Pages/ServerInfo.razor
index 8dd7907..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) { diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 8b9735e..949bddc 100644 --- a/MatrixUtils.Web/Pages/StreamTest.razor +++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -48,9 +48,9 @@ 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; + foreach (var MatrixEventResponse in members) { + // Console.WriteLine(MatrixEventResponse.ToJson()); + var mc = MatrixEventResponse.TypedContent as RoomMemberEventContent; if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) { var uri = mc.AvatarUrl[6..].Split('/'); var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}"; @@ -89,7 +89,7 @@ // var res2 = Homeserver.ClientHttpClient.GetAsync(url); // var tasks = Enumerable.Range(1, 128) // .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url+$"?width={x*128}&height={x*128}")) - // .ToAsyncEnumerable(); + // .ToAsyncResultEnumerable(); await foreach (var result in GetStreamsDelayed(url)) { Streams.Add(result); // await Task.Delay(100); @@ -107,7 +107,7 @@ 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)}")) - .ToAsyncEnumerable(); + .ToAsyncResultEnumerable(); await foreach (var result in tasks) { yield return result; } 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 9a56fc0..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> diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 0943216..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> @@ -53,8 +53,8 @@ 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); diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index f99e932..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/> @@ -30,6 +31,7 @@ <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 acad827..8ba160a 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
@@ -41,13 +41,13 @@ 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().ToAsyncEnumerable(); + 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; - // }).ToAsyncEnumerable(); + // 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)) { diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index f8d1d31..ba8036c 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -74,7 +74,7 @@ else } //use timeline - var types = StateEventResponse.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))); + var types = MatrixEvent.KnownEventTypes.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) { @@ -83,7 +83,7 @@ else 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 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(); @@ -100,9 +100,8 @@ else 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)), @@ -120,10 +119,10 @@ else 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 required MatrixEventResponse Event { get; init; } + public required MatrixEventResponse? Previous { get; init; } - public void Deconstruct(out StateEventTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + public void Deconstruct(out StateEventTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) { transition = State; evt = Event; prev = Previous; diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index ce3513b..76ff629 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -11,7 +11,8 @@ <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> @@ -61,7 +60,7 @@ private ObservableCollection<string> log { get; set; } = new(); List<AuthenticatedHomeserverGeneric> hss { get; set; } = new(); ObservableCollection<GenericRoom> rooms { get; set; } = new(); - Dictionary<GenericRoom, FrozenSet<StateEventResponse>> roomMembers { get; set; } = new(); + Dictionary<GenericRoom, FrozenSet<MatrixEventResponse>> roomMembers { get; set; } = new(); Dictionary<string, List<Matches>> matches = new(); private string UserIdString { @@ -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); @@ -148,7 +147,7 @@ private class Matches { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; // public } diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
index 2313884..16a3853 100644 --- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -9,7 +9,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
index a252e6b..5b0f510 100644 --- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -7,7 +7,7 @@ <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
index b0d5a65..1ff97c8 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -53,7 +53,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index ea39c9a..9b0266c 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -49,7 +49,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
index 9e70687..69a9048 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -49,7 +49,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
index b62cf57..9139561 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -14,10 +14,10 @@ <p>Users (regex): </p> <InputTextArea @bind-Value="@UserIdString"></InputTextArea> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> -<LinkButton OnClick="RemoveKicks">Remove kicks</LinkButton> -<LinkButton OnClick="RemoveBans">Remove bans</LinkButton> +<LinkButton OnClickAsync="RemoveKicks">Remove kicks</LinkButton> +<LinkButton OnClickAsync="RemoveBans">Remove bans</LinkButton> <br/> @@ -145,7 +145,7 @@ private class Match { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; public string RoomName { get; set; } } @@ -161,7 +161,7 @@ } return null; - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var result in results) { if (result is not null) { yield return result; diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 5c5946f..ac68e3d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -9,7 +9,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index 8fdad84..605890d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -8,7 +8,7 @@ <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 1ec3cd0..ec1d190 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -16,7 +16,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@RoomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <p> <span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span> <span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span> @@ -30,24 +30,24 @@ <span><InputCheckbox @bind-Value="ShowBans"/> bans</span> </p> <p> - <LinkButton OnClick="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; - StateHasChanged(); - })">Hide all + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; + StateHasChanged(); + })">Hide all </LinkButton> - <LinkButton OnClick="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; - StateHasChanged(); - })">Show all + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; + StateHasChanged(); + })">Show all </LinkButton> - <LinkButton OnClick="@(async () => { - ShowJoins ^= true; - ShowLeaves ^= true; - ShowKnocks ^= true; - ShowInvites ^= true; - ShowBans ^= true; - StateHasChanged(); - })">Toggle all + <LinkButton OnClickAsync="@(async () => { + ShowJoins ^= true; + ShowLeaves ^= true; + ShowKnocks ^= true; + ShowInvites ^= true; + ShowBans ^= true; + StateHasChanged(); + })">Toggle all </LinkButton> </p> <p> @@ -56,25 +56,25 @@ <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> - } + <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> @@ -129,30 +129,30 @@ </p> <p> - <LinkButton OnClick="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; - StateHasChanged(); - })">Un-disambiguate all + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; + StateHasChanged(); + })">Un-disambiguate all </LinkButton> - <LinkButton OnClick="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; - StateHasChanged(); - })">Disambiguate all + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; + StateHasChanged(); + })">Disambiguate all </LinkButton> - <LinkButton OnClick="@(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 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> } @@ -306,18 +306,61 @@ 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 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 ShowProfileUpdates { get => field && DisambiguateProfileUpdates; @@ -399,7 +442,7 @@ #endregion private ObservableCollection<string> Log { get; set; } = new(); - private List<StateEventResponse> Memberships { get; set; } = []; + private List<MatrixEventResponse> Memberships { get; set; } = []; private AuthenticatedHomeserverGeneric Homeserver { get; set; } [Parameter, SupplyParameterFromQuery(Name = "room")] @@ -444,10 +487,10 @@ private readonly struct MembershipEntry { public required MembershipTransition State { get; init; } - public required StateEventResponse Event { get; init; } - public required StateEventResponse? Previous { get; init; } + public required MatrixEventResponse Event { get; init; } + public required MatrixEventResponse? Previous { get; init; } - public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + public void Deconstruct(out MembershipTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) { transition = State; evt = Event; prev = Previous; @@ -474,7 +517,7 @@ KnockRetracted } - private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> evts) { + private static IEnumerable<MembershipEntry> GetTransitions(List<MatrixEventResponse> 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!"); @@ -528,7 +571,7 @@ { MembershipTransition.KnockRejected, MembershipTransition.Leave }, { MembershipTransition.KnockRetracted, MembershipTransition.Leave } }.ToFrozenDictionary(); - + foreach (var entry in entries) { if (!DoDisambiguate) { yield return entry; diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index 736e59a..a8ae603 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -8,13 +8,13 @@ <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> @@ -55,7 +55,7 @@ <td>@sets.Item2[0].Room.RoomId</td> <td>@((sets.Item2[i].Member.TypedContent as RoomMemberEventContent).Membership)</td> <td>@(roomNames.ContainsKey(sets.Item2[i].Room) ? roomNames[sets.Item2[i].Room] : "")</td> - <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td> + <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td> } else { <td/> @@ -88,7 +88,7 @@ [Parameter, SupplyParameterFromQuery(Name = "b")] public string ImportSetBSpaceId { get; set; } = ""; - Dictionary<string, Dictionary<GenericRoom, StateEventResponse>> roomMembers { get; set; } = new(); + Dictionary<string, Dictionary<GenericRoom, MatrixEventResponse>> roomMembers { get; set; } = new(); Dictionary<string, (List<Match>, List<Match>)> matches { get; set; } = new(); @@ -127,7 +127,7 @@ var setBusers = new Dictionary<string, List<Match>>(); await Task.WhenAll(GetMembers(RoomsA, setAusers), GetMembers(RoomsB, setBusers)); - + Log.Add($"Got {setAusers.Count} users in set A"); Log.Add($"Got {setBusers.Count} users in set B"); Log.Add("Calculating intersections..."); @@ -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(); @@ -191,7 +191,7 @@ public class Match { public GenericRoom Room { get; set; } - public StateEventResponse Member { get; set; } + public MatrixEventResponse Member { get; set; } } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index c3cc09c..d160922 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -11,7 +11,7 @@ <InputTextArea @bind-Value="@UserIdString"></InputTextArea> <br/> <InputText @bind-Value="@ImportFromRoomId"></InputText> -<LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton> +<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton> <details> <summary>Rooms to be searched (@rooms.Count)</summary> @@ -21,15 +21,15 @@ } </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <details> <summary>Results</summary> - @foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) { + @foreach (var (userId, events) in matches.OrderBy(x => x.Key)) { <h4>@userId</h4> <table> - @foreach (var match in events.OrderBy(x=>x.RoomName)) { + @foreach (var match in events.OrderBy(x => x.RoomName)) { <tr> <td>@match.RoomName (<span>@match.Room.RoomId</span>)</td> <td> @@ -139,7 +139,7 @@ private class Match { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; public string RoomName { get; set; } } @@ -161,7 +161,7 @@ } return null; - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var result in results) { if (result is not null) { yield return result; @@ -169,7 +169,7 @@ } } - public string SummarizeMembership(StateEventResponse state) { + public string SummarizeMembership(MatrixEventResponse state) { var membership = state.ContentAs<RoomMemberEventContent>(); var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value); return membership switch { @@ -186,10 +186,9 @@ _ => $"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 ac3c651..d6ae945 100644 --- a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor +++ b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
@@ -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/> @@ -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 e5ffd5b..acc86a2 100644 --- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor +++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -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> @@ -42,7 +42,7 @@ private async Task Execute() { foreach (var hs in hss) { var rooms = await hs.GetJoinedRooms(); - var tasks = rooms.Select(x=>ApplyPowerlevelsInRoom(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); @@ -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 c373a37..ee17f1d 100644 --- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -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> @@ -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/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index ccd3e7b..2b7b6cf 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -12,11 +12,15 @@ <div> <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)) { @@ -26,7 +30,9 @@ <br/> @* <details> *@ - <h4>Room profiles<hr></h4> + <h4>Room profiles + <hr> + </h4> @foreach (var room in Rooms) { <details class="details-compact"> @@ -41,10 +47,16 @@ @* <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)) { @@ -117,7 +129,7 @@ }); roomInfoTasks.Add(task); } - + await Task.WhenAll(roomInfoTasks); StateHasChanged(); @@ -126,7 +138,7 @@ // 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(); + // }).ToAsyncResultEnumerable(); // await foreach (var (roomId, roomName) in roomNameTasks) { // Status = $"Got room name for {roomId}: {roomName}"; diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 14cf2fc..58b66c1 100644 --- a/MatrixUtils.Web/Program.cs +++ b/MatrixUtils.Web/Program.cs
@@ -1,8 +1,10 @@ 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; @@ -18,17 +20,16 @@ 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(); -builder.Services.AddWebWorkerService(webWorkerService => -{ +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 = 2; + 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; + // webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0; }); try { @@ -47,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; @@ -77,7 +77,13 @@ builder.Services.AddScoped<TieredStorageService>(x => ) ); +MatrixHttpClient.LogRequests = false; + builder.Services.AddRoryLibMatrixServices(); builder.Services.AddScoped<RmuSessionStore>(); +builder.Services.AddSingleton<BlazorSaveFileService>(); +builder.Services.AddSingleton<JsConsoleService>(); + // await builder.Build().RunAsync(); -await builder.Build().BlazorJSRunAsync(); \ No newline at end of file +var host = App.Host = builder.Build(); +await host.BlazorJSRunAsync(); \ No newline at end of file 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/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/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
index 11ba18a..b49358d 100644 --- a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -5,8 +5,9 @@ @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"> +<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> @@ -14,25 +15,58 @@ <option value="@type">@mappedType.GetFriendlyName().ToLower()</option> } </select><br/> - + <span>Reason:</span> - <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/> - + <FancyTextBox @bind-Value="@Reason"></FancyTextBox> + <br/> + <span>Recommendation:</span> - <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/> + <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox> + <br/> <span>Entities:</span><br/> - <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/> - - + <FancyTextBox Multiline="true" @bind-Value="@Entities"></FancyTextBox> + <br/> + + @* <details> *@ @* <summary>JSON data</summary> *@ @* <pre> *@ @* $1$ @PolicyEvent.ToJson(true, true) #1# *@ @* </pre> *@ @* </details> *@ - <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton> - <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton> + @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> @@ -47,33 +81,118 @@ [Parameter] public required GenericRoom Room { get; set; } - public string Recommendation { get; set; } = "m.ban"; - public string Reason { get; set; } = "spam"; - public string Users { get; set; } = ""; + private string Recommendation { get; set; } = "m.ban"; + private string Reason { get; set; } = "spam"; - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + private string Entities { get; set; } = ""; + + private string? Response { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool VerifyIntent { get; set; } + + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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() { + 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 DoActualSave(); + await SaveAll(entities); } catch (Exception e) { - Console.WriteLine($"Failed to save: {e}"); + Response = $"Failed to save: {e}"; } } - private async Task DoActualSave() { - Console.WriteLine($"Saving ---"); - Console.WriteLine($"Users = {Users}"); - var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList(); - var tasks = users.Select(x => ExecuteBan(Room, x)).ToList(); - await Task.WhenAll(tasks); - + 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 MatrixEvent() { + Type = MappedType, + TypedContent = content, + StateKey = content.GetDraupnir2StateKey() + }; + }); + + foreach (var chunk in events.Chunk(50)) + await Room.BulkSendEventsAsync(chunk); + OnSaved.Invoke(); } @@ -81,7 +200,7 @@ bool success = false; while (!success) { try { - var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent; + 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; 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 5819bee..501ca99 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"> @@ -52,7 +52,12 @@ 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> + <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) { @@ -61,13 +66,22 @@ 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> + <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> + <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> } } @@ -88,8 +102,8 @@ @PolicyEvent.ToJson(true, true) </pre> </details> - <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton> - <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton> + <LinkButton OnClickAsync="@InvokeOnClose">Cancel</LinkButton> + <LinkButton OnClickAsync="@InvokeOnSave">Save</LinkButton> } else { <p>Policy data is null</p> @@ -99,7 +113,7 @@ @code { [Parameter] - public StateEventResponse? PolicyEvent { + public MatrixEventResponse? PolicyEvent { get => _policyEvent; set { if (value is not null && value != _policyEvent) @@ -111,19 +125,41 @@ } [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<MatrixEventResponse>? OnSave { get; set; } + + [Parameter] + public Func<MatrixEventResponse, 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; } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.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 StateEventResponse? _policyEvent; + private MatrixEventResponse? _policyEvent; private string? MappedType { get => _policyEvent?.Type; diff --git a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
index 27f0499..471f586 100644 --- a/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor +++ b/MatrixUtils.Web/Shared/RoomListComponents/RoomListSpace.razor
@@ -45,7 +45,7 @@ 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 rooms = Space.Room.AsSpace().GetChildrenAsync(); var joinedRooms = await Homeserver.GetJoinedRooms(); await foreach (var room in rooms) { if (Breadcrumbs.Contains(room.RoomId)) continue; diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
index f107eb3..80c69f2 100644 --- a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor +++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -6,19 +6,19 @@ @code { [Parameter] - public StateEventResponse Event { get; set; } + public MatrixEventResponse Event { get; set; } [Parameter] - public List<StateEventResponse> Events { get; set; } + public List<MatrixEventResponse> Events { get; set; } [Parameter] public AuthenticatedHomeserverGeneric Homeserver { get; set; } - public IEnumerable<StateEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId); + public IEnumerable<MatrixEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId); - public IEnumerable<StateEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey); + public IEnumerable<MatrixEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey); - public StateEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault(); + public MatrixEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault(); public RoomMemberEventContent? CurrentSenderMemberEventContent => EventsBefore.LastOrDefault(x => x.Type == "m.room.member" && x.StateKey == Event.Sender)? .TypedContent as RoomMemberEventContent; @@ -27,6 +27,4 @@ public bool HasPreviousMessage => EventsBefore.Last() is { Type: "m.room.message" } response && response.Sender == Event.Sender; - - } \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/UserListItem.razor b/MatrixUtils.Web/Shared/UserListItem.razor
index 5084807..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;"> @@ -26,7 +31,7 @@ [Parameter] public AuthenticatedHomeserverGeneric _homeserver { get; set; } - private SvgIdenticonGenerator _identiconGenerator = new(); + private static SvgIdenticonGenerator _identiconGenerator = new(); protected override async Task OnInitializedAsync() { // _homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); @@ -37,7 +42,14 @@ 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/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 7425de2..fa233b3 100644 --- a/MatrixUtils.Web/wwwroot/index.html +++ b/MatrixUtils.Web/wwwroot/index.html
@@ -3,42 +3,35 @@ <head> <meta charset="utf-8"/> - <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> <title>MatrixUtils.Web</title> <base href="/"/> - <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet"/> - <link href="css/app.css" rel="stylesheet"/> + <link rel="preload" id="webassembly"/> + <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css"/> + <link rel="stylesheet" href="css/app.css"/> + <link rel="icon" type="image/png" href="favicon.png"/> + <link href="MatrixUtils.Web.styles.css" rel="stylesheet"/> <link rel="manifest" href="rmu.webmanifest"/> <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="apple-touch-icon" sizes="192x192" href="icon-192.png"/> + <script type="importmap"></script> </head> <body> <div id="app"> <svg class="loading-progress"> - <circle cx="50%" cy="50%" r="40%"/> - <circle cx="50%" cy="50%" r="40%"/> + <circle r="40%" cx="50%" cy="50%"/> + <circle r="40%" cx="50%" cy="50%"/> </svg> <div class="loading-progress-text"></div> </div> <div id="blazor-error-ui"> An unhandled error has occurred. - <a class="reload" href="">Reload</a> - <a class="dismiss">🗙</a> + <a href="." class="reload">Reload</a> + <span class="dismiss">🗙</span> </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; @@ -59,11 +52,11 @@ } setImageStream = async (element, imageStream) => { - if(!(element instanceof HTMLElement)) { + 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); @@ -74,8 +67,8 @@ image.src = url; } </script> - <script src="_framework/blazor.webassembly.js"></script> -<!-- <script>navigator.serviceWorker.register('service-worker.js');</script>--> + <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> + <!-- <script>navigator.serviceWorker.register('service-worker.js');</script>--> <script src="sw-registrator.js"></script> </body> diff --git a/MatrixUtils.Web/wwwroot/service-worker.published.js b/MatrixUtils.Web/wwwroot/service-worker.published.js
index 9219755..3e28e6c 100644 --- a/MatrixUtils.Web/wwwroot/service-worker.published.js +++ b/MatrixUtils.Web/wwwroot/service-worker.published.js
@@ -9,7 +9,7 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event))); const cacheNamePrefix = 'offline-cache-'; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; const offlineAssetsInclude = [// Standard resources - /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /* Extra known-static paths */ + /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.webmanifest$/, /* Extra known-static paths */ /\/_matrix\/media\/.{2}\/download\//, /api\.dicebear\.com\/6\.x\/identicon\/svg/]; const offlineAssetsExclude = [/^service-worker\.js$/]; @@ -22,13 +22,13 @@ async function onInstall(event) { console.info('Service worker: Install'); // Activate the new service worker as soon as the old one is retired. - self.skipWaiting(); + await self.skipWaiting(); // Fetch and cache all matching items from the assets manifest const assetsRequests = self.assetsManifest.assets .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) - .map(asset => new Request(asset.url, {integrity: asset.hash, cache: 'no-cache'})); + .map(asset => new Request(asset.url, {cache: 'no-cache'})); /* integrity: asset.hash */ await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); } @@ -48,7 +48,8 @@ async function onFetch(event) { // For all navigation requests, try to serve index.html from cache, // unless that request is for an offline resource. // If you need some URLs to be server-rendered, edit the following check to exclude those URLs - const shouldServeIndexHtml = event.request.mode === 'navigate' && !manifestUrlList.some(url => url === event.request.url); + const shouldServeIndexHtml = event.request.mode === 'navigate' + && !manifestUrlList.some(url => url === event.request.url); const request = shouldServeIndexHtml ? 'index.html' : event.request; const shouldCache = offlineAssetsInclude.some(pattern => pattern.test(request.url)); @@ -72,7 +73,7 @@ async function onFetch(event) { fetched, shouldCache, request, exception, cachedResponse, url: request.url, } Object.keys(consoleLog).forEach(key => consoleLog[key] == null && delete consoleLog[key]) - if(consoleLog.exception) + if (consoleLog.exception) console.log("Service worker caching: ", consoleLog) } diff --git a/MatrixUtils.Web/wwwroot/sw-registrator.js b/MatrixUtils.Web/wwwroot/sw-registrator.js
index 94b96b2..b57d26a 100644 --- a/MatrixUtils.Web/wwwroot/sw-registrator.js +++ b/MatrixUtils.Web/wwwroot/sw-registrator.js
@@ -8,14 +8,14 @@ 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})`); // detect updates every minute setInterval(() => { registration.update(); - }, 5 * 1000); // 60000ms -> check each minute + }, 30 * 1000); // 60000ms -> check each minute registration.onupdatefound = () => { const installingServiceWorker = registration.installing; diff --git a/MatrixUtils.sln b/MatrixUtils.sln
index 5fb0c1f..35840d0 100644 --- a/MatrixUtils.sln +++ b/MatrixUtils.sln
@@ -1,6 +1,5 @@  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}" @@ -43,8 +42,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{ 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}" @@ -67,124 +64,399 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxApiExtensions", "MxApiExt 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tmp", "tmp\tmp.csproj", "{EC817938-3A2B-44AD-B144-0C439A69433E}" +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 - {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}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43ECF2DB-CBA6-4A31-BD6A-B059CEA03CA0}.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 {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 + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x64.Build.0 = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Debug|x86.Build.0 = Debug|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|Any CPU.Build.0 = Release|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x64.ActiveCfg = Release|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x64.Build.0 = Release|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.Release|x86.ActiveCfg = Release|Any CPU + {EC817938-3A2B-44AD-B144-0C439A69433E}.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} @@ -202,7 +474,6 @@ Global {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} @@ -211,5 +482,7 @@ Global {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/global.json b/global.json deleted file mode 100644
index 6d77f62..0000000 --- a/global.json +++ /dev/null
@@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "9.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true - } -} diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 7c5086f..1f86275 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh
@@ -12,4 +12,4 @@ rm -rf **/bin/Release cd MatrixUtils.Web dotnet publish -c Release dotnet restore # restore debug deps -rsync --delete -raP bin/Release/net9.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/ +rsync --delete -raP bin/Release/net10.0/publish/wwwroot/ rory.gay:/data/nginx/html_mru/