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
-<h3>Policy list editor - Editing @RoomId</h3>
-<hr/>
+@using LibMatrix.RoomTypes
+@using System.Collections.Frozen
+@using System.Reflection
+@using ArcaneLibs.Attributes
+@using LibMatrix.EventTypes
-<p>
- 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) {
- <p>@key.Name: @value.Count</p>
- }
-</p>
-<InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label>
+@using MatrixRoomUtils.Web.Shared.PolicyEditorComponents
-<h3>Server policies</h3>
+<h3>Policy list editor - Editing @RoomId</h3>
<hr/>
-@if (!GetPolicyEventsByType(typeof(ServerPolicyRuleEventContent)).Any()) {
- <p>No server policies</p>
-}
-else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">Server</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- <td>
- <span>Entity: @policyData.Entity</span>
- <span><br/>State: @policyEvent.StateKey</span>
- </td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Edit</button>
- @* <button class="btn btn-danger" $1$ @onclick="async () => await RemovePolicyAsync(policyEvent)" #1#>Remove</button> *@
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(ServerPolicyRuleEventContent))) {
- <tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent.ToJson(false, true)</td>
- </tr>
- }
- </tbody>
- </table>
- </details>
+@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@
+<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton>
+
+@if (Loading) {
+ <p>Loading...</p>
}
-<h3>Room policies</h3>
-<hr/>
-@if (!GetPolicyEventsByType(typeof(RoomPolicyRuleEventContent)).Any()) {
- <p>No room policies</p>
+else if (PolicyEventsByType is not { Count: > 0 }) {
+ <p>No policies yet</p>
}
else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">Room</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- <td>Entity: @policyData.Entity<br/>State: @policyEvent.StateKey</td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th style="max-width: 50vw;">State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(RoomPolicyRuleEventContent))) {
+ @foreach (var (type, value) in PolicyEventsByType) {
+ <p>
+ @(GetValidPolicyEventsByType(type).Count) active,
+ @(GetInvalidPolicyEventsByType(type).Count) invalid
+ (@value.Count total)
+ @(GetPolicyTypeName(type).ToLower())
+ </p>
+ }
+
+ @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) {
+ <details>
+ <summary>
+ <span>
+ @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies")
+ </span>
+ <hr style="margin: revert;"/>
+ </summary>
+ <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ @{
+ var policies = GetValidPolicyEventsByType(type);
+ var invalidPolicies = GetInvalidPolicyEventsByType(type);
+ // enumerate all properties with friendly name
+ var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null)
+ .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null)
+ .ToFrozenSet();
+ var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet();
+ }
+ <thead>
<tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent!.ToJson(false, true)</td>
+ @foreach (var name in propNames) {
+ <th style="border-width: 1px">@name</th>
+ }
+ <th style="border-width: 1px">Actions</th>
</tr>
- }
- </tbody>
- </table>
- </details>
-}
-<h3>User policies</h3>
-<hr/>
-@if (!GetPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Any()) {
- <p>No user policies</p>
-}
-else {
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- @if (EnableAvatars) {
- <th></th>
- }
- <th style="max-width: 0.2vw; word-wrap: anywhere;">User</th>
- <th>Reason</th>
- <th>Expires</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
- var policyData = policyEvent.TypedContent as PolicyRuleEventContent;
- <tr>
- @if (EnableAvatars) {
- <td>
- @if (Avatars.ContainsKey(policyData.Entity)) {
- <img class="avatar48" src="@Avatars[policyData.Entity]"/>
+ </thead>
+ <tbody style="border-width: 1px;">
+ @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) {
+ <tr>
+ @{
+ var typedContent = policy.TypedContent!;
+ var proxySafeProps = typedContent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => props.Any(y => y.Name == x.Name))
+ .ToFrozenSet();
+ Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}");
}
- </td>
+ @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) {
+ <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td>
+ }
+ <td>
+ <div style="display: ruby;">
+ @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) {
+ <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton>
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton>
+ @if (policy.IsLegacyType) {
+ <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton>
+ }
+ }
+ </div>
+ </td>
+ </tr>
}
- <td style="word-wrap: anywhere;">Entity: @string.Join("", policyData.Entity.Take(64))<br/>State: @string.Join("", policyEvent.StateKey.Take(64))</td>
- <td>@policyData.Reason</td>
- <td>
- @policyData.ExpiryDateTime
- </td>
- <td>
- <button class="btn btn-danger" @* @onclick="async () => await RemovePolicyAsync(policyEvent)" *@>Remove</button>
- </td>
- </tr>
- }
- </tbody>
- </table>
- <details>
- <summary>Redacted or invalid events</summary>
- <table class="table table-striped table-hover" style="width: fit-content;">
- <thead>
- <tr>
- <th>State key</th>
- <th>Serialised Contents</th>
- </tr>
- </thead>
- <tbody>
- @foreach (var policyEvent in GetInvalidPolicyEventsByType(typeof(UserPolicyRuleEventContent))) {
- <tr>
- <td>@policyEvent.StateKey</td>
- <td>@policyEvent.RawContent.ToJson(false, true)</td>
- </tr>
- }
- </tbody>
- </table>
- </details>
+ </tbody>
+ </table>
+ <details>
+ <summary>
+ <u>
+ @("Invalid " + GetPolicyTypeName(type).ToLower())
+ </u>
+ </summary>
+ <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;">
+ <thead>
+ <tr>
+ <th style="border-width: 1px">State key</th>
+ <th style="border-width: 1px">Json contents</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var policy in invalidPolicies) {
+ <tr>
+ <td>@policy.StateKey</td>
+ <td>
+ <pre>@policy.RawContent.ToJson(true, false)</pre>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ </details>
+ </details>
+ }
+}
+
+@if (CurrentlyEditingEvent is not null) {
+ <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal>
}
@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<string, string?> Avatars = new();
+ // static readonly Dictionary<string, string?> Avatars = new();
// static readonly Dictionary<string, RemoteHomeserver> Servers = new();
// private static List<StateEventResponse> PolicyEvents { get; set; } = new();
private Dictionary<Type, List<StateEventResponse>> 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<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
- Console.WriteLine($"Got {users.Count} users!");
- var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
- Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
- var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
- await foreach (var server in homeserverTasks) {
- if (server is null) continue;
- var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
- await Task.WhenAll(profileTasks);
- profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
- foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
- // if (profile is null) continue;
- if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
- var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
- Avatars.TryAdd(profile.Key, url);
- }
- else Avatars.TryAdd(profile.Key, null);
- }
- StateHasChanged();
- }
+ // private async Task GetAllAvatars() {
+ // // if (!_enableAvatars) return;
+ // Console.WriteLine("Getting avatars...");
+ // var users = GetValidPolicyEventsByType(typeof(UserPolicyRuleEventContent)).Select(x => x.RawContent!["entity"]!.GetValue<string>()).Where(x => x.Contains(':') && !x.Contains("*")).ToList();
+ // Console.WriteLine($"Got {users.Count} users!");
+ // var usersByHomeServer = users.GroupBy(x => x!.Split(':')[1]).ToDictionary(x => x.Key!, x => x.ToList());
+ // Console.WriteLine($"Got {usersByHomeServer.Count} homeservers!");
+ // var homeserverTasks = usersByHomeServer.Keys.Select(x => RemoteHomeserver.TryCreate(x)).ToAsyncEnumerable();
+ // await foreach (var server in homeserverTasks) {
+ // if (server is null) continue;
+ // var profileTasks = usersByHomeServer[server.BaseUrl].Select(x => TryGetProfile(server, x)).ToList();
+ // await Task.WhenAll(profileTasks);
+ // profileTasks.RemoveAll(x => x.Result is not { Value: { AvatarUrl: not null } });
+ // foreach (var profile in profileTasks.Select(x => x.Result!.Value)) {
+ // // if (profile is null) continue;
+ // if (!string.IsNullOrWhiteSpace(profile.Value.AvatarUrl)) {
+ // var url = await hsResolver.ResolveMediaUri(server.BaseUrl, profile.Value.AvatarUrl);
+ // Avatars.TryAdd(profile.Key, url);
+ // }
+ // else Avatars.TryAdd(profile.Key, null);
+ // }
+ //
+ // StateHasChanged();
+ // }
+ // }
+ //
+ // private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
+ // try {
+ // return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
+ // }
+ // catch {
+ // return null;
+ // }
+ // }
+
+ private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
+
+ private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+
+ private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type)
+ .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
+
+ private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull()
+ ?? type.GetCustomAttributes<MatrixEventAttribute>()
+ .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName;
+
+ private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
+
+ private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
+ PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
+ await LoadStatesAsync();
}
- private async Task<KeyValuePair<string, UserProfileResponse>?> TryGetProfile(RemoteHomeserver server, string mxid) {
- try {
- return new KeyValuePair<string, UserProfileResponse>(mxid, await server.GetProfileAsync(mxid));
- }
- catch {
- return null;
- }
+ private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
+ await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
+ await LoadStatesAsync();
}
- private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : [];
- private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
- private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type).Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList();
- private 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<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);
}
\ No newline at end of file
|