From d105d1a7ec709ddb6240a286bbd7be292a9acd1c Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 21 Sep 2025 16:03:44 +0200 Subject: Various fixes --- Benchmarks/Benchmarks.csproj | 2 +- LibMatrix | 2 +- .../MatrixUtils.RoomUpgradeCLI.csproj | 2 +- .../appsettings.Development.json | 2 +- .../appsettings.SynapseDev.json | 0 .../MatrixUtils.Web.Server.csproj | 2 +- MatrixUtils.Web/App.razor | 9 +- .../Classes/LocalStorageProviderService.cs | 18 +- MatrixUtils.Web/Classes/RmuSessionStore.cs | 128 ++++++++++--- MatrixUtils.Web/Classes/UserAuth.cs | 14 +- MatrixUtils.Web/MatrixUtils.Web.csproj | 11 +- MatrixUtils.Web/Pages/Dev/DevOptions.razor | 16 +- .../SynapseRoomShutdownWindowContent.razor | 2 + .../Pages/HSAdmin/Synapse/RoomQuery.razor | 32 ++-- .../RoomsIndex2ByRoomTypeTab.razor | 1 - MatrixUtils.Web/Pages/Rooms/PolicyList.razor | 203 +++++++++++++++----- .../PolicyListCategoryComponent.razor | 55 +++--- .../PolicyListEditorHeader.razor | 47 +++-- .../PolicyListRowComponent.razor | 92 +++++++--- MatrixUtils.Web/Pages/Rooms/Space.razor | 3 +- MatrixUtils.Web/Pages/Tools/Index.razor | 1 + .../Pages/Tools/Room/SpacePermissions.razor | 204 +++++++++++++++++++++ .../Pages/Tools/User/StickerManager.razor | 80 ++++++++ MatrixUtils.Web/Program.cs | 5 +- MatrixUtils.Web/appsettings.Development.json | 2 + MatrixUtils.Web/wwwroot/css/app.css | 5 + 26 files changed, 753 insertions(+), 185 deletions(-) delete mode 100644 MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json create mode 100644 MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor create mode 100644 MatrixUtils.Web/Pages/Tools/User/StickerManager.razor diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index e61ef31..a72013b 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -11,7 +11,7 @@ - + diff --git a/LibMatrix b/LibMatrix index 013f169..91319ba 160000 --- a/LibMatrix +++ b/LibMatrix @@ -1 +1 @@ -Subproject commit 013f1693885a5de01ae357af2909589e925863d5 +Subproject commit 91319ba62de889bde645b6f1df4dd6a960ee7de4 diff --git a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj index 4d0299a..b0167e2 100644 --- a/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj +++ b/MatrixUtils.RoomUpgradeCLI/MatrixUtils.RoomUpgradeCLI.csproj @@ -8,7 +8,7 @@ - + diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json index f42db52..621d281 100644 --- a/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json +++ b/MatrixUtils.RoomUpgradeCLI/appsettings.Development.json @@ -9,7 +9,7 @@ "LibMatrixBot": { // Homeserver to connect to. // Note: Homeserver resolution is applied here, but a direct base URL can be used. - "Homeserver": "rory.gay", +// "Homeserver": "rory.gay", // Absolute path to the file containing the access token "AccessTokenPath": "/home/Rory/matrix_access_token" diff --git a/MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json b/MatrixUtils.RoomUpgradeCLI/appsettings.SynapseDev.json deleted file mode 100644 index e69de29..0000000 diff --git a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj index f446bf3..11781a8 100644 --- a/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj +++ b/MatrixUtils.Web.Server/MatrixUtils.Web.Server.csproj @@ -8,7 +8,7 @@ - + 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 @@ - +@using Microsoft.AspNetCore.Components.WebAssembly.Hosting + @@ -10,3 +11,9 @@ + +@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(string key, T value) { throw new NotImplementedException(); } Task IStorageProvider.LoadAllChildrenAsync(string key) where T : default => throw new NotImplementedException(); - async Task IStorageProvider.SaveObjectAsync(string key, T value) => await _localStorageService.SetItemAsync(key, value); + async Task IStorageProvider.SaveObjectAsync(string key, T value) => await localStorageService.SetItemAsync(key, value); - async Task IStorageProvider.LoadObjectAsync(string key) where T : default => await _localStorageService.GetItemAsync(key); + async Task IStorageProvider.LoadObjectAsync(string key) where T : default => await localStorageService.GetItemAsync(key); - async Task IStorageProvider.ObjectExistsAsync(string key) => await _localStorageService.ContainKeyAsync(key); + async Task IStorageProvider.ObjectExistsAsync(string key) => await localStorageService.ContainKeyAsync(key); - async Task> IStorageProvider.GetAllKeysAsync() => (await _localStorageService.KeysAsync()).ToList(); + async Task> 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 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("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 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("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("token"); + if (oldToken != null) { + await dsp.SaveObjectAsync("mru.token", oldToken); + await dsp.DeleteObjectAsync("token"); + } } - } - if (await dsp.ObjectExistsAsync("tokens")) { - var oldTokens = await dsp.LoadObjectAsync>("tokens"); - if (oldTokens != null) { - await dsp.SaveObjectAsync("rmu.tokens", oldTokens); - await dsp.DeleteObjectAsync("tokens"); + if (await dsp.ObjectExistsAsync("tokens")) { + var oldTokens = await dsp.LoadObjectAsync>("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>("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("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>("mru.tokens"); + if (oldTokens != null) { + await dsp.SaveObjectAsync("rmu.tokens", oldTokens); + await dsp.DeleteObjectAsync("mru.tokens"); + } } } } 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..44fce2d 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj @@ -39,11 +39,12 @@ - - - - - + + + + + + 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 @@

Manage local sessions - +
@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("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(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/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor index 999e331..48aea86 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor @@ -105,6 +105,8 @@ } public async Task DeleteRoom() { + var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, Context.DeleteRequest, false); + Context.DeleteId = resp.DeleteId; await TaskMap.SetValueAsync(Context.RoomId, Context); } diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor index 5e45e5b..07a3dd2 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor @@ -34,21 +34,6 @@ @if (Results.Count > 0) {

Found @Results.Count rooms

- @*
*@ - @* TSV data (copy/paste) *@ - @*
 *@
-    @*              *@
-    @*                 @foreach (var res in Results) { *@
-    @*                      *@
-    @*                          *@
-    @*                          *@
-    @*                          *@
-    @*                          *@
-    @*                      *@
-    @*                 } *@
-    @*             
@res.RoomId@("\t")@res.CanonicalAlias@("\t")@res.Creator@("\t")@res.Name
*@ - @*
*@ - @*
*@ } @foreach (var room in Results) { @@ -71,12 +56,12 @@

Delete room - Resync state + Resync state

@{ List flags = []; - if (true || room.JoinedLocalMembers > 0) { + if (room.JoinedLocalMembers > 0) { flags.Add(room.JoinRules switch { "public" => "Public", "invite" => "Invite only", @@ -88,7 +73,7 @@ "" => null, _ => "unknown join rule: " + room.JoinRules }); - + if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted"); if (!room.Federatable) flags.Add("unfederated"); @@ -124,7 +109,8 @@ @room.StateEvents state events, room version @(room.Version ?? "1")
@if (room.TombstoneEvent is not null) { var tombstoneContent = room.TombstoneEvent.ContentAs()!; - Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body
+ Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body +
} @{ @@ -283,6 +269,7 @@ private async Task Search() { Results.Clear(); var searchRooms = Homeserver.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); + var joinedRooms = await Homeserver.GetJoinedRooms(); while (await searchRooms.MoveNextAsync()) { var room = searchRooms.Current; @@ -300,7 +287,11 @@ HistoryVisibility = room.HistoryVisibility, StateEvents = room.StateEvents, JoinedMembers = room.JoinedMembers, - JoinedLocalMembers = room.JoinedLocalMembers + JoinedLocalMembers = room.JoinedLocalMembers, + OriginHomeserver = joinedRooms.Any(x => x.RoomId == room.RoomId) + ? await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync() + : (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1] + ?? string.Empty }; Results.Add(roomInfo); @@ -425,6 +416,7 @@ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { public List? 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"; 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/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor index 412d8c9..5876861 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 logger +@inject BlazorJSRuntime JsRuntime @if (!IsInitialised) {

Connecting to homeserver...

} else { - + @if (Loading) {

Loading...

} @@ -39,14 +43,36 @@ else {

} + @if (DuplicateBans?.ActivePolicies.Count > 0) { +

+ Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans +

+ } + + @if (RedundantBans?.ActivePolicies.Count > 0) { +

+ Found @RedundantBans.Value.ActivePolicies.Count redundant bans +

+ } + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); // var renderSw2 = Stopwatch.StartNew(); // IOrderedEnumerable 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) { + + } + + @if (RedundantBans?.ActivePolicies.Count > 0) { + + } + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { - + } // 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,7 +164,13 @@ else { public required string RoomId { get; set; } [Parameter, SupplyParameterFromQuery] - public bool RenderEventInfo { get; set; } + public bool RenderEventInfo { + get; + set { + field = value; + StateHasChanged(); + } + } private Dictionary> PolicyEventsByType { get; set; } = new(); @@ -157,6 +189,19 @@ else { public Dictionary ActiveKicks { get; set; } = []; + private static FrozenSet KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + // event types, unnamed + // private static Dictionary PolicyTypes = KnownPolicyTypes + // .ToDictionary(x => x.GetCustomAttributes().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + // + // private static Dictionary PolicyTypeIds = KnownPolicyTypes + // .ToDictionary(x => x, x => x.GetCustomAttributes().Select(y => y.EventName).ToArray()); + + Dictionary PolicyCollections { get; set; } = new(); + PolicyCollection? DuplicateBans { get; set; } + PolicyCollection? RedundantBans { get; set; } + protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); await base.OnInitializedAsync(); @@ -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]; @@ -239,21 +287,24 @@ else { } } - 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(x => (x, (x.Policy.TypedContent as PolicyRuleEventContent)!)) // .ToList(); @@ -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(), (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>> 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(policiesJson); + var ranges = Enumerable.Range(0, aggregatedPolicies.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount); + await taskPoolReadyTask; + tasks.AddRange(ranges.Select(range => WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJsonMarshalled, range.First(), range.Last()))); + + Console.WriteLine($"Main: started {tasks.Count} workers in {scanSw.Elapsed}"); // tasks.Add(CheckDuplicatePoliciesAsync(allPolicyInfos, range.First() .. range.Last())); + // var allPolicyEvents = aggregatedPolicies.Select(x => x.Policy).ToList(); + + DuplicateBans = new() { + Name = "Duplicate bans", + ViewType = PolicyCollection.SpecialViewType.Duplicates, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + RedundantBans = new() { + Name = "Redundant bans", + ViewType = PolicyCollection.SpecialViewType.Redundant, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + var allPolicyInfos = PolicyCollections.Values + .SelectMany(x => x.ActivePolicies.Values) + .ToArray(); + await foreach (var modifiedPolicyInfos in tasks.ToAsyncEnumerable()) { - Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}"); + 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 => StateEvent.Equals(x, y))).ToList(); + original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => StateEvent.Equals(x, y))).ToList(); + modifiedPolicyInfo.DuplicatedBy = modifiedPolicyInfo.MadeRedundantBy = []; // Early dereference + if (original.DuplicatedBy.Count > 0) { + if (!DuplicateBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + DuplicateBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } + + if (original.MadeRedundantBy.Count > 0) { + if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } + // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}"); } - Console.WriteLine($"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) { @@ -380,10 +474,18 @@ else { // StateHasChanged(); } + [return: WorkerTransfer] + private static async Task> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize>(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> 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>(policiesJson), start .. end); + var policies = JsonSerializer.Deserialize>(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] @@ -392,6 +494,8 @@ else { [return: WorkerTransfer] private static async Task> CheckDuplicatePoliciesAsync(List policies, Range range) { + var sw = Stopwatch.StartNew(); + var jsConsole = App.Host.Services.GetService()!; 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)!)) @@ -410,10 +514,23 @@ else { 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,7 +556,7 @@ 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)) @@ -463,7 +582,7 @@ else { } 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,25 +592,25 @@ 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 GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; @@ -511,19 +630,9 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; - private static FrozenSet KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); - - // event types, unnamed - private static Dictionary PolicyTypes = KnownPolicyTypes - .ToDictionary(x => x.GetCustomAttributes().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); - - private static Dictionary PolicyTypeIds = KnownPolicyTypes - .ToDictionary(x => x, x => x.GetCustomAttributes().Select(y => y.EventName).ToArray()); - - Dictionary 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; } @@ -537,6 +646,12 @@ else { public required List MadeRedundantBy { get; set; } public required List DuplicatedBy { get; set; } } + + public enum SpecialViewType { + None, + Duplicates, + Redundant, + } } // private struct PolicyStats { 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 @@ + @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) { } - @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue())) { - + }
Actions@nameActions
-
- - - @("Invalid " + PolicyCollection.Name.ToLower()) - - - - - - - - - - - @foreach (var policy in PolicyCollection.RemovedPolicies.Values) { + @if (RenderInvalidSection) { +
+ + + @("Invalid " + PolicyCollection.Name.ToLower()) + + +
State keyJson contents
+ - - + + - } - -
@policy.Policy.StateKey -
@policy.Policy.RawContent.ToJson(true, false)
-
State keyJson contents
-
+ + + @foreach (var policy in PolicyCollection.RemovedPolicies.Values) { + + @policy.Policy.StateKey + +
@policy.Policy.RawContent.ToJson(true, false)
+ + + } + + + + } @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..8585561 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor @@ -12,14 +12,14 @@
@* *@ Create new policy + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy Create many new policies + MassCreatePolicies = true; + return Task.CompletedTask; + })">Create many new policies Refresh @@ -33,20 +33,43 @@ // _ = LoadStatesAsync(); })"> } +
+ + Render event info @code { + [Parameter] public required GenericRoom Room { get; set; } - + [Parameter] public required Func ReloadStateAsync { get; set; } + [Parameter] + public required bool RenderEventInfo { get; set; } + + [Parameter] + public required EventCallback 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 StateEventResponse? 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!"); + 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..cd432c9 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) { - @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) { - if (prop.Name == "Entity") { - - @TruncateMxid(TypedContent.Entity) - @foreach (var dup in PolicyInfo.DuplicatedBy) { -
- Duplicated by @dup.FriendlyTypeName.ToLower() @TruncateMxid(dup.RawContent["entity"]?.GetValue()) - } - @foreach (var dup in PolicyInfo.MadeRedundantBy) { -
- Also matched by @dup.FriendlyTypeName.ToLower() @TruncateMxid(dup.RawContent["entity"]?.GetValue()) - } - @if (RenderEventInfo) { -
-
-                            @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTs 
-                        
- } - - } - else { - @prop.GetGetMethod()?.Invoke(TypedContent, null) - } - }
@* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@ @@ -41,7 +18,7 @@ Remove @if (Policy.IsLegacyType) { - Update policy type + Update type } @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) { @@ -66,6 +43,30 @@ }
+ @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) { + if (prop.Name == "Entity") { + + @TruncateMxid(TypedContent.Entity) + @foreach (var dup in PolicyInfo.DuplicatedBy) { +
+ Duplicated by @dup.FriendlyTypeName.ToLower() @TruncateMxid(dup.RawContent["entity"]?.GetValue()) + } + @foreach (var dup in PolicyInfo.MadeRedundantBy) { +
+ Also matched by @dup.FriendlyTypeName.ToLower() @TruncateMxid(dup.RawContent["entity"]?.GetValue()) + } + @if (RenderEventInfo) { +
+
+                            @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTimestamp
+                        
+ } + + } + else { + @prop.GetGetMethod()?.Invoke(TypedContent, null) + } + } @if (IsEditing) { @@ -95,6 +96,9 @@ [Parameter] public bool RenderEventInfo { get; set; } + [Parameter] + public required Action PolicyCollectionStateHasChanged { get; set; } + private StateEventResponse Policy => PolicyInfo.Policy; private bool IsEditing { @@ -142,8 +146,42 @@ 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 => StateEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.DuplicatedBy.RemoveAll(x => StateEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} duplicates from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + if (PolicyCollection.ViewType == PolicyList.PolicyCollection.SpecialViewType.Duplicates && matchingEntry.DuplicatedBy.Count == 0) { + PolicyCollection.ActivePolicies.Remove((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey)); + PolicyCollection.RemovedPolicies.Add((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey), matchingEntry); + Console.WriteLine($"Also removed {matchingEntry.Policy.EventId} as it is now redundant"); + } + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (PolicyInfo.MadeRedundantBy.Count > 0) { + foreach (var evt in PolicyInfo.MadeRedundantBy) { + var matchingEntry = PolicyCollection.ActivePolicies + .FirstOrDefault(x => StateEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => StateEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} redundants from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (shouldUpdateVisibility) { + IsVisible = false; + StateHasChanged(); + } // PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); // await LoadStatesAsync(); } diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor index 86a4c13..fc9c9bf 100644 --- a/MatrixUtils.Web/Pages/Rooms/Space.razor +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor @@ -120,7 +120,7 @@ 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(); @@ -132,6 +132,7 @@ } } joined = true; + await Task.Delay(1000); } } catch (Exception e) { 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 @@ Join room across all session
Copy highest powerlevel across all session
View account data
+Manage custom stickers and emojis

Room tools


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 +

Space Permissions

+
+Space ID: + +Execute +
+ + Auto-recurse into child spaces +
+ +@if (RoomPowerLevels.Count == 0) { +

No data loaded.

+} +else { + Loaded @LoadedSpaceRooms.Count spaces. +
+ @if (SpaceRooms.Count > 0) { +

Load more spaces:

+ @foreach (var room in SpaceRooms) { + @room.Value + } + } + +

By event type:

+ + + + @foreach (var key in OrderedEventTypes) { + + } + + + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Events!.Values.Average())) { + + + @foreach (var eventType in OrderedEventTypes) { + if (!powerLevels.Events!.ContainsKey(eventType.Key)) { + + continue; + } + + + } + + } + +
Room@key.Key +
+ ~ @Math.Round(key.Value, 2) +
@roomName-@(powerLevels.Events![eventType.Key])
+
+

By user:

+ + + + @foreach (var key in OrderedUsers) { + + } + + + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Users!.Values.Average())) { + + + @foreach (var eventType in OrderedUsers) { + if (!powerLevels.Users!.ContainsKey(eventType.Key)) { + + continue; + } + + + } + + } + +
Room@key.Key +
+ ~ @Math.Round(key.Value, 2) +
@roomName-@(powerLevels.Users![eventType.Key])
+} + +@code { + + [Parameter, SupplyParameterFromQuery] + public string? SpaceId { get; set; } + + [Parameter, SupplyParameterFromQuery] + public bool AutoRecurseSpaces { get; set; } + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private List AllHomeservers { get; set; } = []; + private Dictionary> JoinedHomeserversByRoom { get; set; } = []; + + private Dictionary RoomPowerLevels { get; set; } = []; + private Dictionary SpaceRooms { get; set; } = []; + private List LoadedSpaceRooms { get; set; } = []; + + private Dictionary OrderedEventTypes { get; set; } = new(); + private Dictionary 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 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/StickerManager.razor b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor new file mode 100644 index 0000000..984130f --- /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 Logger +

Sticker/emoji manager

+ +@if (TotalStepsProgress is not null) { + +
+} +@if (_observableProgressState is not null) { + +
+} + +@code { + + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private Msc2545EmoteRoomsAccountDataEventContent? EnabledEmoteRooms { get; set; } + private Dictionary 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.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.ToAsyncEnumerable()) { + _observableProgressState.Label.Value = roomScanResult; + } + } + + private class StickerRoom { } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs index bc047e8..e48782f 100644 --- a/MatrixUtils.Web/Program.cs +++ b/MatrixUtils.Web/Program.cs @@ -82,5 +82,8 @@ MatrixHttpClient.LogRequests = false; builder.Services.AddRoryLibMatrixServices(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + // 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/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; -- cgit 1.5.1