about summary refs log tree commit diff
path: root/MatrixUtils.Web
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj8
-rw-r--r--MatrixUtils.Web/App.razor9
-rw-r--r--MatrixUtils.Web/Classes/LocalStorageProviderService.cs18
-rw-r--r--MatrixUtils.Web/Classes/RmuSessionStore.cs128
-rw-r--r--MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs2
-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/WellKnownRes.razor1
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor4
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor82
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor163
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor463
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor3
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs10
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor10
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor14
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor4
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor1
-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/PolicyList.razor249
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs8
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor42
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor55
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor49
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor96
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyLists.razor12
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor8
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Space.razor80
-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.razor10
-rw-r--r--MatrixUtils.Web/Pages/StreamTest.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor1
-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.razor13
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor181
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor8
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor13
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor204
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor6
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor15
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/StickerManager.razor80
-rw-r--r--MatrixUtils.Web/Pages/User/Profile.razor30
-rw-r--r--MatrixUtils.Web/Program.cs7
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/BooleanFilterComponent.razor17
-rw-r--r--MatrixUtils.Web/Shared/FilterComponents/StringFilterComponent.razor31
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor10
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor10
-rw-r--r--MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor12
-rw-r--r--MatrixUtils.Web/appsettings.Development.json2
-rw-r--r--MatrixUtils.Web/wwwroot/css/app.css5
-rw-r--r--MatrixUtils.Web/wwwroot/index.html29
-rw-r--r--MatrixUtils.Web/wwwroot/service-worker.published.js11
-rw-r--r--MatrixUtils.Web/wwwroot/sw-registrator.js4
59 files changed, 1771 insertions, 690 deletions
diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj

index f446bf3..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.8" /> + <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/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 9df8837..1611b83 100644 --- a/MatrixUtils.Web/Classes/RmuSessionStore.cs +++ b/MatrixUtils.Web/Classes/RmuSessionStore.cs
@@ -26,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; @@ -39,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); } @@ -52,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) { @@ -134,6 +150,53 @@ 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 @@ -170,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, @@ -178,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 @@ -190,29 +255,42 @@ public class RmuSessionStore( } private async Task MigrateFromMru() { - logger.LogInformation("Migrating from MRU token namespace!"); var dsp = storageService.DataStorageProvider!; - if (await dsp.ObjectExistsAsync("token")) { - var oldToken = await dsp.LoadObjectAsync<UserAuth>("token"); - if (oldToken != null) { - await dsp.SaveObjectAsync("rmu.token", oldToken); - await dsp.DeleteObjectAsync("token"); + 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"); + } } } } diff --git a/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index 215ad14..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() { 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 18204d0..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.8" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.8" PrivateAssets="all" /> - <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.8" /> - <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.8" /> - <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.17.1" /> + <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/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index c636c56..722f9b3 100644 --- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor +++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -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/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
index e9d0cd2..5ccaca9 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -96,7 +96,7 @@ } } - private StateEventResponse? Event { get; set; } + private MatrixEventResponse? Event { get; set; } private string? EventJson { get; @@ -140,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
index eb168f4..f1c5907 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor
@@ -1,46 +1,70 @@ @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">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> + <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"> - <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> + <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"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"/> - <span class="range-sep">state events</span> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"/> + <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"> - <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> + <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"> - <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> + <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 { diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
index 999e331..b0e6a89 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -1,8 +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)) { +@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) { <span>Block room: </span> <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/> <br/> @@ -34,6 +38,12 @@ <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> @@ -50,7 +60,19 @@ <br/> </details> - <LinkButton OnClickAsync="@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 { @@ -61,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(); @@ -81,10 +141,11 @@ 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 5e45e5b..05899c8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,5 +1,7 @@ @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 @@ -10,6 +12,7 @@ @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> @@ -21,40 +24,56 @@ <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> + <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 room in Results) { <div class="room-list-item"> @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ <p> + @if (EnableMultiPurge) { + <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/> + <span> </span> + } @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { <span>@room.CanonicalAlias - </span> } @@ -71,12 +90,14 @@ </p> <p> <LinkButton OnClickAsync="@(() => DeleteRoom(room))">Delete room</LinkButton> - <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton> + <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.OriginHomeserver}")">Resync state</LinkButton> + <LinkButton OnClickAsync="@(() => ExportState(room))">@(room.JoinedLocalMembers == 0 ? "Try to export state" : "Export state")</LinkButton> + <LinkButton OnClickAsync="@(() => ForceJoin(room))">Force Join</LinkButton> </p> @{ List<string?> flags = []; - if (true || room.JoinedLocalMembers > 0) { + if (room.JoinedLocalMembers > 0) { flags.Add(room.JoinRules switch { "public" => "Public", "invite" => "Invite only", @@ -88,7 +109,7 @@ "" => null, _ => "unknown join rule: " + room.JoinRules }); - + if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted"); if (!room.Federatable) flags.Add("unfederated"); @@ -124,7 +145,8 @@ <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/> + <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span> + <br/> } @{ @@ -133,11 +155,23 @@ memberSummary += $": {string.Join(", ", room.LocalMembers)}"; } } - <span>@memberSummary</span> - <details> - <summary>Full result data</summary> - <pre>@room.ToJson(ignoreNull: true)</pre> - </details> + <span>@memberSummary</span><br/> + @if (!string.IsNullOrWhiteSpace(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)) { + <details> + <summary>Room topic</summary> + <pre>@(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)</pre> + </details> + } + @foreach (var ex in room.Exceptions) { + <span style="color: red;">@ex</span> + <br/> + } + @if (ShowFullResultData) { + <details> + <summary>Full result data</summary> + <pre>@room.ToJson(ignoreNull: true)</pre> + </details> + } </div> } @* *@ @@ -162,10 +196,6 @@ </ModalWindow> } -<style> - -</style> - @code { [Parameter] @@ -180,6 +210,18 @@ [SupplyParameterFromQuery(Name = "ascending")] public bool Ascending { get; set; } = true; + [Parameter] + [SupplyParameterFromQuery(Name = "FetchV12PlusCreatorServer")] + public bool FetchV12PlusCreatorServer { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "SummarizeLocalMembers")] + public bool SummarizeLocalMembers { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "FetchTombstones")] + public bool FetchTombstones { get; set; } = true; + private List<RoomInfo> Results { get; set; } = new(); private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; @@ -192,6 +234,21 @@ 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) { @@ -213,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(); @@ -282,7 +367,27 @@ private async Task Search() { Results.Clear(); - var searchRooms = Homeserver.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); + 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; @@ -300,12 +405,27 @@ HistoryVisibility = room.HistoryVisibility, StateEvents = room.StateEvents, JoinedMembers = room.JoinedMembers, - JoinedLocalMembers = room.JoinedLocalMembers + JoinedLocalMembers = room.JoinedLocalMembers, + OriginHomeserver = + Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId + ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty) + : string.Empty }; + if (string.IsNullOrWhiteSpace(roomInfo.OriginHomeserver) && FetchV12PlusCreatorServer) { + try { + if (joinedRooms.Any(x => x.RoomId == room.RoomId)) + roomInfo.OriginHomeserver = await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync(); + else roomInfo.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]; + } + catch (MatrixException e) { + roomInfo.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + } + Results.Add(roomInfo); - if ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) { + if ((Results.Count <= 200 && Results.Count % 10 == 0 && FetchV12PlusCreatorServer) || Results.Count % 1000 == 0) { StateHasChanged(); await Task.Yield(); await Task.Delay(1); @@ -314,97 +434,29 @@ StateHasChanged(); - var getLocalMembersTasks = Results - .Where(x => x.JoinedLocalMembers is > 0 and < 100) - .Select(async r => { - var members = (await Homeserver.Admin.GetRoomMembersAsync(r.RoomId)).Members.Where(x => x.EndsWith(":" + Homeserver.ServerName)).ToList(); - r.LocalMembers = members; - } - ); - await Task.WhenAll(getLocalMembersTasks); - - var getTombstoneTasks = Results - .Select(async r => { - 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; - } - }); - await Task.WhenAll(getTombstoneTasks); + if (FetchV12PlusCreatorServer) await FetchV12PlusCreatorServersAsync(false); + if (SummarizeLocalMembers) await FetchLocalMemberEventsAsync(false); + // if (CheckTombstone) await FetchTombstoneEventsAsync(false); StateHasChanged(); } - Task DeleteRoom(RoomInfo room) { - DeleteRequests.TryAdd(room.RoomId, new() { RoomId = room.RoomId, RoomDetails = room, DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false } }); + 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 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 void PurgeSelection() { + foreach (var room in Results.Where(x => x.MultiPurgeSelected)) { + DeleteRoom(room, true); + } + } private readonly Dictionary<string, string> validOrderBy = new() { { "name", "Room name" }, @@ -424,10 +476,143 @@ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { public List<string>? LocalMembers { get; set; } - public StateEventResponse? TombstoneEvent { 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/SubTools/SynapseRoomStateResync.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
index c2446a2..3cc5a6a 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -39,7 +39,6 @@ } @* stage 3 *@ - @if (Stage == 3) { <p>Rejoining room, please wait...</p> <p>Members left to restore: </p> @@ -82,7 +81,7 @@ private Exception? Error { get; set; } // Stage 1 - private List<StateEventResponse>? Members { get; set; } + private List<MatrixEventResponse>? Members { get; set; } // Stage 2 private SynapseAdminRoomDeleteStatus? DeleteStatus { get; set; } 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 360548d..7199934 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -26,7 +26,7 @@ <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> Create sub-spaces per user </p> - + <br/> <LinkButton OnClickAsync="@Disband" Color="#FF0000">Disband</LinkButton> <LinkButton OnClickAsync="@Execute">Next</LinkButton> @@ -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 25d1629..ed65e94 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -38,7 +38,9 @@ else { } @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 OnClickAsync="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> @@ -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,8 +183,8 @@ 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(); } @@ -192,7 +194,7 @@ else { 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 2bed22e..686894c 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -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, 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/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/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index 412d8c9..f2ab186 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -5,22 +5,26 @@ @using System.Diagnostics @using LibMatrix.RoomTypes @using System.Collections.Frozen +@using System.Collections.Immutable @using System.Reflection @using System.Text.Json @using ArcaneLibs.Attributes +@using ArcaneLibs.Blazor.Components.Services @using LibMatrix.EventTypes @using LibMatrix.EventTypes.Interop.Draupnir @using LibMatrix.EventTypes.Spec.State.RoomInfo @using SpawnDev.BlazorJS.WebWorkers @using MatrixUtils.Web.Pages.Rooms.PolicyListComponents +@using SpawnDev.BlazorJS @inject WebWorkerService WebWorkerService @inject ILogger<PolicyList> logger +@inject BlazorJSRuntime JsRuntime @if (!IsInitialised) { <p>Connecting to homeserver...</p> } else { - <PolicyListEditorHeader Room="@Room" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader> + <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader> @if (Loading) { <p>Loading...</p> } @@ -39,14 +43,36 @@ else { </p> } + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans + </p> + } + + @if (RedundantBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @RedundantBans.Value.ActivePolicies.Count redundant bans + </p> + } + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); // var renderSw2 = Stopwatch.StartNew(); // IOrderedEnumerable<Type> policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count); // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}"); + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@DuplicateBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } + + @if (RedundantBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@RedundantBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { - <PolicyListCategoryComponent PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent> + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent> } // foreach (var type in policiesByType) { @@ -119,7 +145,7 @@ else { // } // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); - logger.LogInformation($"Rendered in {renderTotalSw.Elapsed}"); + logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed); } } @@ -138,11 +164,17 @@ else { public required string RoomId { get; set; } [Parameter, SupplyParameterFromQuery] - public bool RenderEventInfo { get; set; } + public bool RenderEventInfo { + get; + set { + field = value; + StateHasChanged(); + } + } - private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); + private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new(); - public StateEventResponse? ServerPolicyToMakePermanent { + public MatrixEventResponse? ServerPolicyToMakePermanent { get; set { field = value; @@ -155,7 +187,20 @@ else { private RoomPowerLevelEventContent PowerLevels { get; set; } = null!; public bool CurrentUserIsDraupnir { get; set; } - public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = []; + 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(); @@ -172,18 +217,19 @@ else { StateHasChanged(); await LoadStateAsync(firstLoad: true); Loading = false; - logger.LogInformation($"Policy list editor initialized in {sw.Elapsed}!"); + logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed); } 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 {sw.Elapsed}"); + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); foreach (var type in KnownPolicyTypes) { if (!PolicyCollections.ContainsKey(type)) { @@ -197,7 +243,7 @@ else { 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($"{proxySafeProps?.Count} proxy safe props found in {type.FullName} ({filterPropSw.Elapsed})"); + 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 = [], @@ -210,9 +256,11 @@ else { var count = 0; var parseSw = Stopwatch.StartNew(); foreach (var evt in states) { - var sw2 = Stopwatch.StartNew(); var mappedType = evt.MappedType; - logger.LogInformation($"Processing state #{count++:000000} {evt.Type} @ {sw.Elapsed} (took {parseSw.Elapsed:c} so far to process)"); + 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]; @@ -226,34 +274,37 @@ else { if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue<string>())) { collection.ActivePolicies.Remove(key); if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) { - if (StateEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue; + 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 (StateEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue; + if (MatrixEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue; collection.ActivePolicies[key] = policyInfo; } } } - logger.LogInformation($"LoadStatesAsync: Processed state in {sw.Elapsed}"); + logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed); foreach (var collection in PolicyCollections) { - logger.LogInformation($"Policy collection {collection.Key.FullName} has {collection.Value.ActivePolicies.Count} active and {collection.Value.RemovedPolicies.Count} removed policies."); + logger.LogInformation("Policy collection {KeyFullName} has {ActivePoliciesCount} active and {RemovedPoliciesCount} removed policies.", collection.Key.FullName, collection.Value.ActivePolicies.Count, collection.Value.RemovedPolicies.Count); } + await Task.Delay(1); + Loading = false; StateHasChanged(); await Task.Delay(100); + // return; logger.LogInformation("LoadStatesAsync: Scanning for redundant policies..."); var scanSw = Stopwatch.StartNew(); - var allPolicyInfos = PolicyCollections.Values - .SelectMany(x => x.ActivePolicies.Values) - .ToList(); + // 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(); @@ -277,7 +328,7 @@ else { // foreach (var (policyInfo, policyContent) in allPolicies) { // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) { // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event - // if (StateEvent.TypeKeyPairMatches(policyInfo.Policy, otherPolicyInfo.Policy)) { + // 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 // } @@ -290,28 +341,71 @@ else { // } // } - Console.WriteLine($"Scanning for redundant policies in {allPolicyInfos.Count} total policies... ({scanSw.Elapsed})"); + 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(allPolicyInfos.Select(x => x.Policy)); - var ranges = Enumerable.Range(0, allPolicyInfos.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount); - foreach (var range in ranges) - tasks.Add(WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJson, range.First(), range.Last())); - + var policiesJson = JsonSerializer.Serialize(aggregatedPolicies); + var policiesJsonMarshalled = JsRuntime.ReturnMe<SpawnDev.BlazorJS.JSObjects.String>(policiesJson); + var ranges = Enumerable.Range(0, aggregatedPolicies.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount); + await taskPoolReadyTask; + tasks.AddRange(ranges.Select(range => WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJsonMarshalled, range.First(), range.Last()))); + + Console.WriteLine($"Main: started {tasks.Count} workers in {scanSw.Elapsed}"); // tasks.Add(CheckDuplicatePoliciesAsync(allPolicyInfos, range.First() .. range.Last())); - await foreach (var modifiedPolicyInfos in tasks.ToAsyncEnumerable()) { - Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}"); + // 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 = modifiedPolicyInfo.DuplicatedBy; - original.MadeRedundantBy = modifiedPolicyInfo.MadeRedundantBy; + 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); + } + + 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($"Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed}"); + Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})"); } - Console.WriteLine($"Processed {allPolicyInfos.Count} policies in {scanSw.Elapsed}"); + Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}"); // // scan for wildcard matches // foreach (var policy in allPolicies) { @@ -381,17 +475,27 @@ else { } [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); + } + + [return: WorkerTransfer] private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) { - Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON"); - return await CheckDuplicatePoliciesAsync(JsonSerializer.Deserialize<List<StateEventResponse>>(policiesJson), start .. 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); } [return: WorkerTransfer] - private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, int start, int end) + private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, int start, int end) => CheckDuplicatePoliciesAsync(policies, start .. end); [return: WorkerTransfer] - private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<StateEventResponse> policies, Range range) { + 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)!)) @@ -400,20 +504,33 @@ else { var modifiedPolicies = new List<PolicyCollection.PolicyInfo>(); foreach (var (policyEvent, policyContent) in toCheck) { - List<StateEventResponse> duplicatedBy = []; - List<StateEventResponse> madeRedundantBy = []; + List<MatrixEventResponse> duplicatedBy = []; + List<MatrixEventResponse> madeRedundantBy = []; foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) { if (policyEvent == otherPolicyEvent) continue; // same event - if (StateEvent.TypeKeyPairMatches(policyEvent, otherPolicyEvent)) { + 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 } + // if(!policyContent.IsHashedRule()) + if (!string.IsNullOrWhiteSpace(policyContent.Entity) && policyContent.Entity == otherPolicyContent.Entity) { + // Console.WriteLine($"Found duplicate policy: {policyEvent.EventId} is duplicated by {otherPolicyEvent.EventId}"); + duplicatedBy.Add(otherPolicyEvent); + } } if (duplicatedBy.Count > 0 || madeRedundantBy.Count > 0) { + var summary = $"Policy {policyEvent.EventId} is:"; + if (duplicatedBy.Count > 0) + summary += $"\n- Duplicated by {duplicatedBy.Count} policies: {string.Join(", ", duplicatedBy.Select(x => x.EventId))}"; + if (madeRedundantBy.Count > 0) + summary += $"\n- Made redundant by {madeRedundantBy.Count} policies: {string.Join(", ", madeRedundantBy.Select(x => x.EventId))}"; + // Console.WriteLine(summary); + await jsConsole.Info(summary); + await Task.Delay(1); modifiedPolicies.Add(new() { Policy = policyEvent, DuplicatedBy = duplicatedBy, @@ -424,6 +541,8 @@ else { // await Task.Delay(1); } + await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}"); + return modifiedPolicies; } @@ -437,12 +556,12 @@ else { var states = await Room.GetFullStateAsListAsync(); // PolicyEventsByType.Clear(); - logger.LogInformation($"LoadStatesAsync: Loaded state in {sw.Elapsed}"); + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); foreach (var type in KnownPolicyTypes) { if (!PolicyEventsByType.ContainsKey(type)) PolicyEventsByType.Add(type, new List - <StateEventResponse>(16000)); + <MatrixEventResponse>(16000)); } int count = 0; @@ -454,16 +573,16 @@ else { e1 = _spsw.Elapsed; var targetPolicies = PolicyEventsByType[state.MappedType]; e2 = _spsw.Elapsed; - if (!firstLoad && targetPolicies.FirstOrDefault(x => StateEvent.TypeKeyPairMatches(x, state)) is { } evt) { + if (!firstLoad && targetPolicies.FirstOrDefault(x => MatrixEvent.TypeKeyPairMatches(x, state)) is { } evt) { e3 = _spsw.Elapsed; - if (StateEvent.Equals(evt, state)) { + if (MatrixEvent.Equals(evt, state)) { if (count % 100 == 0) { await Task.Delay(10); await Task.Yield(); } e4 = _spsw.Elapsed; - logger.LogInformation($"[E] LoadStatesAsync: Processed state #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={e3:c}, e4={e4:c}, e5={TimeSpan.Zero:c},t={_spsw.Elapsed:c})"); + 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; } @@ -473,36 +592,36 @@ else { targetPolicies.Add(state); e6 = _spsw.Elapsed; t = _spsw.Elapsed; - logger.LogInformation($"[M] LoadStatesAsync: Processed state #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={e3:c}, e4={e4:c}, e5={e5:c}, e6={e6:c},t={t:c})"); + logger.LogInformation("[M] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={E5:c}, e6={E6:c},t={TimeSpan1:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, e5, e6, t); } else { targetPolicies.Add(state); t = _spsw.Elapsed; - logger.LogInformation($"[N] LoadStatesAsync: Processed state #{count++:000000} {state.Type} @ {sw.Elapsed} (e1={e1:c}, e2={e2:c}, e3={TimeSpan.Zero:c}, e4={TimeSpan.Zero:c}, e5={TimeSpan.Zero:c}, e6={TimeSpan.Zero:c}, t={t:c})"); + logger.LogInformation("[N] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={Zero:c}, e4={TimeSpan1:c}, e5={Zero1:c}, e6={TimeSpan2:c}, t={TimeSpan3:c})", count++, state.Type, sw.Elapsed, e1, e2, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, t); } // await Task.Delay(10); // await Task.Yield(); } - logger.LogInformation($"LoadStatesAsync: Processed state in {sw.Elapsed}"); + 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 {sw.Elapsed}"); + logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed); } - 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 => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); // - // private List<StateEventResponse> GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // 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() @@ -511,31 +630,27 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); - - // event types, unnamed - private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes - .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); - - private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes - .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); - - Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new(); - 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), StateEventResponse> InvalidPolicies { 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 StateEventResponse Policy { get; init; } - public required List<StateEventResponse> MadeRedundantBy { get; set; } - public required List<StateEventResponse> DuplicatedBy { get; set; } + public required MatrixEventResponse Policy { get; init; } + public required List<MatrixEventResponse> MadeRedundantBy { get; set; } + public required List<MatrixEventResponse> DuplicatedBy { get; set; } + } + + public enum SpecialViewType { + None, + Duplicates, + Redundant, } } diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
index 0106c6e..6f45041 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -8,12 +8,11 @@ using SpawnDev.BlazorJS.WebWorkers; namespace MatrixUtils.Web.Pages.Rooms; public partial class PolicyList { - #region Draupnir interop private SemaphoreSlim ss = new(16, 16); - private async Task DraupnirKickMatching(StateEventResponse policy) { + private async Task DraupnirKickMatching(MatrixEventResponse policy) { try { var content = policy.TypedContent! as PolicyRuleEventContent; if (content is null) return; @@ -94,7 +93,7 @@ public partial class PolicyList { #region Nasty, nasty internals, please ignore! private static class NastyInternalsPleaseIgnore { - public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List<string> roomIds) { + 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(); @@ -131,7 +130,7 @@ public partial class PolicyList { } } - private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { + private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, MatrixEventResponse policy) { Console.WriteLine($"Checking {roomId}..."); Console.WriteLine(policy.EventId); } @@ -140,5 +139,4 @@ public partial class PolicyList { #endregion #endregion - } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
index 5d5bb5d..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 OnClickAsync="@(() => { 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,14 +75,22 @@ else { } <div style="display: ruby;"> @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - <LinkButton OnClickAsync="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton> + <LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = policy; + return Task.CompletedTask; + })">Edit + </LinkButton> <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> @if (policy.IsLegacyType) { <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> } @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) { - <LinkButton OnClickAsync="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton> + <LinkButton OnClickAsync="@(() => { + ServerPolicyToMakePermanent = policy; + return Task.CompletedTask; + })">Make permanent (wildcard) + </LinkButton> @if (CurrentUserIsDraupnir) { <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> } @@ -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
index b52e03f..932e0fe 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -10,43 +10,45 @@ <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> } - <th>Actions</th> </tr> </thead> <tbody> @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) { - <PolicyListRowComponent RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent> + <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent> } </tbody> </table> - <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) { + @if (RenderInvalidSection) { + <details> + <summary> + <u> + @("Invalid " + PolicyCollection.Name.ToLower()) + </u> + </summary> + <table class="table table-striped table-hover table-bordered align-middle"> + <thead> <tr> - <td>@policy.Policy.StateKey</td> - <td> - <pre>@policy.Policy.RawContent.ToJson(true, false)</pre> - </td> + <th>State key</th> + <th>Json contents</th> </tr> - } - </tbody> - </table> - </details> + </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 { @@ -60,6 +62,9 @@ [Parameter] public bool RenderEventInfo { get; set; } + [Parameter] + public bool RenderInvalidSection { get; set; } = true; + protected override bool ShouldRender() { // if (PolicyCollection is null) return false; diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
index e82f17d..b57beae 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
@@ -12,14 +12,14 @@ <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 + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy </LinkButton> <LinkButton OnClickAsync="@(() => { - MassCreatePolicies = true; - return Task.CompletedTask; - })">Create many new policies + MassCreatePolicies = true; + return Task.CompletedTask; + })">Create many new policies </LinkButton> <LinkButton OnClickAsync="@(() => ReloadStateAsync())">Refresh</LinkButton> @@ -33,20 +33,43 @@ // _ = LoadStatesAsync(); })"></MassPolicyEditorModal> } +<br/> +<InputCheckbox Value="@RenderEventInfo" ValueChanged="@RenderEventInfoChanged" ValueExpression="@(() => RenderEventInfo)"/> +<span> Render event info</span> @code { + [Parameter] public required GenericRoom Room { get; set; } - + [Parameter] public required Func<Task> ReloadStateAsync { get; set; } + [Parameter] + public required bool RenderEventInfo { get; set; } + + [Parameter] + public required EventCallback<bool> RenderEventInfoChanged { get; set; } + private string? RoomName { get; set; } private string? RoomAlias { get; set; } private string? DraupnirShortcode { get; set; } - - private StateEventResponse? CurrentlyEditingEvent { get; set { field = value; StateHasChanged(); } } - private bool MassCreatePolicies { get; set { field = value; StateHasChanged(); } } + + private MatrixEventResponse? CurrentlyEditingEvent { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool MassCreatePolicies { + get; + set { + field = value; + StateHasChanged(); + } + } protected override async Task OnInitializedAsync() { await Task.WhenAll( @@ -54,12 +77,12 @@ Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }) ); - + StateHasChanged(); } - private async Task UpdatePolicyAsync(StateEventResponse evt) { - Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implementeD!"); + 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
index 9ac5077..3ded78f 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
@@ -1,4 +1,5 @@ @using System.Reflection +@using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes.Spec.State.Policy @using LibMatrix.RoomTypes @@ -6,30 +7,6 @@ @if (_isInitialized && IsVisible) { <tr id="@PolicyInfo.Policy.EventId"> - @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> - @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTs - </pre> - } - </td> - } - else { - <td>@prop.GetGetMethod()?.Invoke(TypedContent, null)</td> - } - } <td> <div style="display: flex; flex-direction: row; gap: 0.5em;"> @* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@ @@ -41,7 +18,7 @@ </LinkButton> <LinkButton OnClickAsync="@RemovePolicyAsync">Remove</LinkButton> @if (Policy.IsLegacyType) { - <LinkButton OnClickAsync="@RemovePolicyAsync">Update policy type</LinkButton> + <LinkButton OnClickAsync="@RemovePolicyAsync">Update type</LinkButton> } @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) { @@ -66,6 +43,30 @@ } </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) { @@ -95,7 +96,10 @@ [Parameter] public bool RenderEventInfo { get; set; } - private StateEventResponse Policy => PolicyInfo.Policy; + [Parameter] + public required Action PolicyCollectionStateHasChanged { get; set; } + + private MatrixEventResponse Policy => PolicyInfo.Policy; private bool IsEditing { get; @@ -142,13 +146,47 @@ private async Task RemovePolicyAsync() { await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { }); - IsVisible = false; - StateHasChanged(); + 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(StateEventResponse evt) { + private async Task UpdatePolicyAsync(MatrixEventResponse evt) { await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, Policy.RawContent); // CurrentlyEditingEvent = null; // await LoadStatesAsync(); diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
index 6df56ba..a84ef8c 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -12,9 +12,9 @@ <h3> <span>Policy lists </span> <LinkButton OnClickAsync="@(() => { - ShowPolicyListCreationWindow = true; - return Task.CompletedTask; - })"> + ShowPolicyListCreationWindow = true; + return Task.CompletedTask; + })"> <span class="oi oi-plus" aria-hidden="true"> Create</span> </LinkButton> </h3> @@ -137,7 +137,7 @@ 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) { @@ -145,7 +145,7 @@ StateHasChanged(); } } - + isLoading = false; Status = ""; Status2 = ""; @@ -188,7 +188,7 @@ Server } - 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, diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
index 99facbf..2b1d90a 100644 --- a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
@@ -5,7 +5,7 @@ <tr> <td style="vertical-align: top;">Initial room state:</td> <td> - @foreach (var (displayName, events) in new Dictionary<string, List<StateEvent>>() { + @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 }, }) { @@ -47,10 +47,10 @@ @* if (string.IsNullOrWhiteSpace(json)) *@ @* events.Remove(initialState); *@ @* else *@ - @* events.Replace(initialState, JsonSerializer.Deserialize<StateEvent>(json)); *@ + @* events.Replace(initialState, JsonSerializer.Deserialize<MatrixEvent>(json)); *@ @* StateHasChanged(); *@ @* })"></FancyTextBox> *@ - <FancyTextBoxLazyJson T="StateEvent" Value="@initialState" ValueChanged="@(evt => { events.Replace(initialState, evt); })"></FancyTextBoxLazyJson> + <FancyTextBoxLazyJson T="MatrixEvent" Value="@initialState" ValueChanged="@(evt => { events.Replace(initialState, evt); })"></FancyTextBoxLazyJson> <br/> </div> } @@ -71,7 +71,7 @@ [Parameter] public AuthenticatedHomeserverGeneric Homeserver { get; set; } - private RenderFragment GetRemoveButton(List<StateEvent> events, StateEvent initialState) { + private RenderFragment GetRemoveButton(List<MatrixEvent> events, MatrixEvent initialState) { return @<span> <LinkButton InlineText="true" OnClick="@(() => { events.Remove(initialState); diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 86a4c13..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,24 +123,25 @@ 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() { 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 2af819b..f9137b0 100644 --- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -22,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; } @@ -44,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), @@ -57,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/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/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index b0f7dbf..067036e 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -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 4a44753..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/> 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 fcdb3d0..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 OnClickAsync="@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> @@ -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())).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/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
index 1fd0ff6..9139561 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -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/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 11c4a80..ec1d190 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -31,23 +31,23 @@ </p> <p> <LinkButton OnClickAsync="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; - StateHasChanged(); - })">Hide all + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; + StateHasChanged(); + })">Hide all </LinkButton> <LinkButton OnClickAsync="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; - StateHasChanged(); - })">Show all + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; + StateHasChanged(); + })">Show all </LinkButton> <LinkButton OnClickAsync="@(async () => { - ShowJoins ^= true; - ShowLeaves ^= true; - ShowKnocks ^= true; - ShowInvites ^= true; - ShowBans ^= true; - StateHasChanged(); - })">Toggle all + 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> @@ -130,29 +130,29 @@ <p> <LinkButton OnClickAsync="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; - StateHasChanged(); - })">Un-disambiguate all + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; + StateHasChanged(); + })">Un-disambiguate all </LinkButton> <LinkButton OnClickAsync="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; - StateHasChanged(); - })">Disambiguate all + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; + StateHasChanged(); + })">Disambiguate all </LinkButton> <LinkButton OnClickAsync="@(async () => { - DisambiguateProfileUpdates ^= true; - DisambiguateKicks ^= true; - DisambiguateUnbans ^= true; - DisambiguateInviteAccepted ^= true; - DisambiguateInviteRejected ^= true; - DisambiguateInviteRetracted ^= true; - DisambiguateKnockAccepted ^= true; - DisambiguateKnockRejected ^= true; - DisambiguateKnockRetracted ^= true; - DisambiguateKnockActions ^= true; - DisambiguateInviteActions ^= true; - StateHasChanged(); - })">Toggle all + 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 ee77532..a8ae603 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -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..."); @@ -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 f39a2eb..d160922 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -26,10 +26,10 @@ <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/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/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index b893970..acc86a2 100644 --- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor +++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -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 748f2fb..ee17f1d 100644 --- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -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 290b92a..2b7b6cf 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -12,9 +12,13 @@ <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/> + <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> @@ -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,9 +47,15 @@ @* <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/> + <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/> @@ -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 bc047e8..58b66c1 100644 --- a/MatrixUtils.Web/Program.cs +++ b/MatrixUtils.Web/Program.cs
@@ -29,7 +29,7 @@ builder.Services.AddWebWorkerService(webWorkerService => { 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 { @@ -82,5 +82,8 @@ 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/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
index bb4b672..b49358d 100644 --- a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -96,7 +96,7 @@ private bool VerifyIntent { 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); @@ -177,20 +177,20 @@ // var tasks = entities.Select(x => ExecuteBan(Room, x)).ToList(); // await Task.WhenAll(tasks); - + var events = entities.Select(entity => { var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content"); content.Recommendation = Recommendation; content.Reason = Reason; content.Entity = entity; - return new StateEvent() { + return new MatrixEvent() { Type = MappedType, TypedContent = content, StateKey = content.GetDraupnir2StateKey() }; }); - - foreach(var chunk in events.Chunk(50)) + + foreach (var chunk in events.Chunk(50)) await Room.BulkSendEventsAsync(chunk); OnSaved.Invoke(); diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 0205e16..501ca99 100644 --- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -113,7 +113,7 @@ @code { [Parameter] - public StateEventResponse? PolicyEvent { + public MatrixEventResponse? PolicyEvent { get => _policyEvent; set { if (value is not null && value != _policyEvent) @@ -139,10 +139,10 @@ } [Parameter] - public Action<StateEventResponse>? OnSave { get; set; } + public Action<MatrixEventResponse>? OnSave { get; set; } [Parameter] - public Func<StateEventResponse, Task>? OnSaveAsync { get; set; } + public Func<MatrixEventResponse, Task>? OnSaveAsync { get; set; } private async Task InvokeOnSave() { if (OnSave is not null) @@ -154,12 +154,12 @@ 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/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/appsettings.Development.json b/MatrixUtils.Web/appsettings.Development.json
index 826edbf..1555d4e 100644 --- a/MatrixUtils.Web/appsettings.Development.json +++ b/MatrixUtils.Web/appsettings.Development.json
@@ -4,6 +4,8 @@ "Default": "Trace", "System": "Information", "Microsoft": "Information", + "Microsoft.AspNetCore.StaticAssets": "Warning", + "Microsoft.AspNetCore.EndpointMiddleware": "Warning", "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 f25d549..fa233b3 100644 --- a/MatrixUtils.Web/wwwroot/index.html +++ b/MatrixUtils.Web/wwwroot/index.html
@@ -3,30 +3,33 @@ <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 getWidth(element) { @@ -49,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); @@ -64,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;