From d33eb7c8965737db468804d3705a886e7db08dc5 Mon Sep 17 00:00:00 2001 From: Rory& Date: Thu, 11 Jan 2024 07:31:54 +0100 Subject: Policy list editor rewrite --- LibMatrix | 2 +- MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor | 421 ++++++++++----------- .../PolicyEditorComponents/PolicyEditorModal.razor | 92 +++++ MatrixRoomUtils.sln | 6 + 4 files changed, 298 insertions(+), 223 deletions(-) create mode 100644 MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor diff --git a/LibMatrix b/LibMatrix index 0f9f9e9..8dadf54 160000 --- a/LibMatrix +++ b/LibMatrix @@ -1 +1 @@ -Subproject commit 0f9f9e9201bbbed5981135d67e1265fd0f31aeff +Subproject commit 8dadf547033d71480fd7756809992c0f32549f59 diff --git a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor index dbe0648..b89d59c 100644 --- a/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixRoomUtils.Web/Pages/Rooms/PolicyList.razor @@ -1,192 +1,124 @@ @page "/Rooms/{RoomId}/Policies" @using LibMatrix -@using LibMatrix.Homeservers @using ArcaneLibs.Extensions @using LibMatrix.EventTypes.Spec.State @using LibMatrix.EventTypes.Spec.State.Policy @using System.Diagnostics -@using System.Diagnostics.CodeAnalysis -@using LibMatrix.Extensions -@using LibMatrix.Responses -

Policy list editor - Editing @RoomId

-
+@using LibMatrix.RoomTypes +@using System.Collections.Frozen +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix.EventTypes -

- This policy list contains @GetPolicyCount(typeof(ServerPolicyRuleEventContent)) server bans, - @GetPolicyCount(typeof(RoomPolicyRuleEventContent)) room bans and - @GetPolicyCount(typeof(UserPolicyRuleEventContent)) user bans. - @foreach (var (key, value) in PolicyEventsByType) { -

@key.Name: @value.Count

- } -

- +@using MatrixRoomUtils.Web.Shared.PolicyEditorComponents -

Server policies

+

Policy list editor - Editing @RoomId


-@if (!GetPolicyEventsByType(typeof(ServerPolicyRuleEventContent)).Any()) { -

No server policies

-} -else { - - - - - - - - - - - @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) { - var policyData = policyEvent.TypedContent as PolicyRuleEventContent; - - - - - - - } - -
ServerReasonExpiresActions
- Entity: @policyData.Entity -
State: @policyEvent.StateKey
-
@policyData.Reason - @policyData.ExpiryDateTime - - - @* *@ -
-
- Redacted or invalid events - - - - - - - - - @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) { - - - - - } - -
State keySerialised Contents
@policyEvent.StateKey@policyEvent.RawContent.ToJson(false, true)
-
+@* *@ +Create new policy + +@if (Loading) { +

Loading...

} -

Room policies

-
-@if (!GetPolicyEventsByType(typeof(RoomPolicyRuleEventContent)).Any()) { -

No room policies

+else if (PolicyEventsByType is not { Count: > 0 }) { +

No policies yet

} else { - - - - - - - - - - - @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) { - var policyData = policyEvent.TypedContent as PolicyRuleEventContent; - - - - - - - } - -
RoomReasonExpiresActions
Entity: @policyData.Entity
State: @policyEvent.StateKey
@policyData.Reason - @policyData.ExpiryDateTime - - -
-
- Redacted or invalid events - - - - - - - - - @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) { + @foreach (var (type, value) in PolicyEventsByType) { +

+ @(GetValidPolicyEventsByType(type).Count) active, + @(GetInvalidPolicyEventsByType(type).Count) invalid + (@value.Count total) + @(GetPolicyTypeName(type).ToLower()) +

+ } + + @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) { +
+ + + @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") + +
+
+
State keySerialised Contents
+ @{ + var policies = GetValidPolicyEventsByType(type); + var invalidPolicies = GetInvalidPolicyEventsByType(type); + // enumerate all properties with friendly name + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute() is null) + .ToFrozenSet(); + var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + } + - - + @foreach (var name in propNames) { + + } + - } - -
@policyEvent.StateKey@policyEvent.RawContent!.ToJson(false, true)@nameActions
-
-} -

User policies

-
-@if (!GetPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Any()) { -

No user policies

-} -else { - - - - @if (EnableAvatars) { - - } - - - - - - - - @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) { - var policyData = policyEvent.TypedContent as PolicyRuleEventContent; - - @if (EnableAvatars) { - + @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue())) { + + @{ + var typedContent = policy.TypedContent!; + var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenSet(); + Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); } - + @foreach (var prop in proxySafeProps ?? Enumerable.Empty()) { + + } + + } - - - - - - } - -
UserReasonExpiresActions
- @if (Avatars.ContainsKey(policyData.Entity)) { - + +
@prop.GetGetMethod()?.Invoke(typedContent, null) +
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { + Edit + Remove + @if (policy.IsLegacyType) { + Update policy type + } + } +
+
Entity: @string.Join("", policyData.Entity.Take(64))
State: @string.Join("", policyEvent.StateKey.Take(64))
@policyData.Reason - @policyData.ExpiryDateTime - - -
-
- Redacted or invalid events - - - - - - - - - @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) { - - - - - } - -
State keySerialised Contents
@policyEvent.StateKey@policyEvent.RawContent.ToJson(false, true)
-
+ + +
+ + + @("Invalid " + GetPolicyTypeName(type).ToLower()) + + + + + + + + + + + @foreach (var policy in invalidPolicies) { + + + + + } + +
State keyJson contents
@policy.StateKey +
@policy.RawContent.ToJson(true, false)
+
+
+ + } +} + +@if (CurrentlyEditingEvent is not null) { + } @code { @@ -197,47 +129,59 @@ else { private const bool Debug = false; #endif + private bool Loading { get; set; } = true; //get room list // - sync withroom list filter // Type = support.feline.msc3784 //support.feline.policy.lists.msc.v1 [Parameter] - public string? RoomId { get; set; } + public string RoomId { get; set; } = null!; private bool _enableAvatars; + private StateEventResponse? _currentlyEditingEvent; - static readonly Dictionary Avatars = new(); + // static readonly Dictionary Avatars = new(); // static readonly Dictionary Servers = new(); // private static List PolicyEvents { get; set; } = new(); private Dictionary> PolicyEventsByType { get; set; } = new(); - public bool EnableAvatars { - get => _enableAvatars; + private StateEventResponse? CurrentlyEditingEvent { + get => _currentlyEditingEvent; set { - _enableAvatars = value; - if (value) GetAllAvatars(); + _currentlyEditingEvent = value; + StateHasChanged(); } } + // public bool EnableAvatars { + // get => _enableAvatars; + // set { + // _enableAvatars = value; + // if (value) GetAllAvatars(); + // } + // } + + private AuthenticatedHomeserverGeneric Homeserver { get; set; } + private GenericRoom Room { get; set; } + private RoomPowerLevelEventContent PowerLevels { get; set; } + protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); await base.OnInitializedAsync(); - var hs = await MRUStorage.GetCurrentSessionOrNavigate(); - if (hs is null) return; - RoomId = RoomId.Replace('~', '.'); + Homeserver = (await MRUStorage.GetCurrentSessionOrNavigate())!; + if (Homeserver is null) return; + Room = Homeserver.GetRoom(RoomId!); + PowerLevels = (await Room.GetPowerLevelsAsync())!; await LoadStatesAsync(); Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); } private async Task LoadStatesAsync() { - var hs = await MRUStorage.GetCurrentSessionOrNavigate(); - if (hs is null) return; - - var room = hs.GetRoom(RoomId); - - var states = room.GetFullStateAsync(); + Loading = true; + var states = Room.GetFullStateAsync(); + PolicyEventsByType.Clear(); await foreach (var state in states) { if (state is null) continue; if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; @@ -245,46 +189,79 @@ else { PolicyEventsByType[state.MappedType].Add(state); } + Loading = false; StateHasChanged(); } - private async Task GetAllAvatars() { - // if (!_enableAvatars) return; - Console.WriteLine("Getting avatars..."); - var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue()).Where(x => x.Contains(':') && !x.Contains("*")).ToList(); - Console.WriteLine($"Got {users.Count} users!"); - var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList()); - Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!"); - var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable(); - await foreach (var server in homeserverTasks) { - if (server is null) continue; - var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList(); - await Task.WhenAll(profileTasks); - profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } }); - foreach (var profile in profileTasks.Select(x => x.Result!.Value)) { - // if (profile is null) continue; - if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) { - var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl); - Avatars.TryAdd(profile.Key, url); - } - else Avatars.TryAdd(profile.Key, null); - } - StateHasChanged(); - } + // private async Task GetAllAvatars() { + // // if (!_enableAvatars) return; + // Console.WriteLine("Getting avatars..."); + // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue()).Where(x => x.Contains(':') && !x.Contains("*")).ToList(); + // Console.WriteLine($"Got {users.Count} users!"); + // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList()); + // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!"); + // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable(); + // await foreach (var server in homeserverTasks) { + // if (server is null) continue; + // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList(); + // await Task.WhenAll(profileTasks); + // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } }); + // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) { + // // if (profile is null) continue; + // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) { + // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl); + // Avatars.TryAdd(profile.Key, url); + // } + // else Avatars.TryAdd(profile.Key, null); + // } + // + // StateHasChanged(); + // } + // } + // + // private async Task?> TryGetProfile(RemoteHomeserver server, string mxid) { + // try { + // return new KeyValuePair(mxid, await server.GetProfileAsync(mxid)); + // } + // catch { + // return null; + // } + // } + + private List GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + + private List GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue())).ToList(); + + private List GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue())).ToList(); + + private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() + ?? type.GetCustomAttributes() + .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + + private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + + private async Task RemovePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { }); + PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + await LoadStatesAsync(); } - private async Task?> TryGetProfile(RemoteHomeserver server, string mxid) { - try { - return new KeyValuePair(mxid, await server.GetProfileAsync(mxid)); - } - catch { - return null; - } + private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); + await LoadStatesAsync(); } - private List GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; - private List GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue())).ToList(); - private List GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue())).ToList(); - private int GetPolicyCount(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type].Count : 0; + private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { + policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; + await LoadStatesAsync(); + } + + 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); } \ No newline at end of file diff --git a/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor new file mode 100644 index 0000000..4fd151d --- /dev/null +++ b/MatrixRoomUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor @@ -0,0 +1,92 @@ +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Reflection +@using ArcaneLibs.Attributes +@using ArcaneLibs.Extensions +@using LibMatrix +@using System.Collections.Frozen +@using LibMatrix.EventTypes + + @{ + var policyData = (PolicyEvent.TypedContent as PolicyRuleEventContent)!; + } + @if (string.IsNullOrWhiteSpace(PolicyEvent.EventId)) { + Policy type: + + } + + + @{ + // enumerate all properties with friendly name + var props = PolicyEvent.MappedType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute() is null) + .ToFrozenSet(); + var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + } + + + + + + + + + @foreach (var prop in props) { + + + @{ + var getter = prop.GetGetMethod(); + var setter = prop.GetSetMethod(); + } + @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) { + case Type t when t == typeof(string): + {e}"); setter?.Invoke(policyData, [e]); StateHasChanged(); })"> + break; + default: +

Unsupported type: @prop.PropertyType

+ break; + } + + } + +
PropertyValue
+ @prop.GetFriendlyName() + @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) { + * + } +
+
+
+        @PolicyEvent.ToJson(true, false)
+    
+ Cancel + Save + @* Target entity: *@ + @*
*@ + @* Reason: *@ + @* *@ +
+ +@code { + + [Parameter] + public StateEventResponse? PolicyEvent { get; set; } + + [Parameter] + public required Action OnClose { get; set; } + + [Parameter] + public required Action OnSave { get; set; } + + private static FrozenSet KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + private static Dictionary PolicyTypes = KnownPolicyTypes + .ToDictionary(x => x.GetCustomAttributes().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + +} \ No newline at end of file diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln index a26bbaa..e5bf946 100644 --- a/MatrixRoomUtils.sln +++ b/MatrixRoomUtils.sln @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "Lib EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixRoomUtils.Abstractions", "MatrixRoomUtils.Abstractions\MatrixRoomUtils.Abstractions.csproj", "{FE20ED20-0D55-4D74-822B-E2AC7A54C487}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLIRainbow", "CLIRainbow\CLIRainbow.csproj", "{D237350C-C1BB-4E67-B3D5-066889E1FB8B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,6 +134,10 @@ Global {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE20ED20-0D55-4D74-822B-E2AC7A54C487}.Release|Any CPU.Build.0 = Release|Any CPU + {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D237350C-C1BB-4E67-B3D5-066889E1FB8B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} -- cgit 1.5.1