about summary refs log tree commit diff
path: root/MatrixUtils.Web
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2024-09-22 20:38:53 +0200
committerRory& <root@rory.gay>2024-09-22 20:38:53 +0200
commit8bbe8c20b6b376bfe73fa8d8186e253cd351a642 (patch)
tree5f951bfac01bdbed5ff1c695dacf2eb9b90f439a /MatrixUtils.Web
parentAuthenticated media foundations (diff)
downloadMatrixUtils-8bbe8c20b6b376bfe73fa8d8186e253cd351a642.tar.xz
Changes
Diffstat (limited to 'MatrixUtils.Web')
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor101
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css9
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor213
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css32
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Space.razor43
-rw-r--r--MatrixUtils.Web/Pages/StreamTest.razor20
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor51
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor129
-rw-r--r--MatrixUtils.Web/Shared/MainLayout.razor.css1
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor102
-rw-r--r--MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor60
11 files changed, 594 insertions, 167 deletions
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor

index b7ebae2..4dc2adc 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -16,6 +16,7 @@ <hr/> @* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ <LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> +<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</LinkButton> @if (Loading) { <p>Loading...</p> @@ -24,6 +25,8 @@ else if (PolicyEventsByType is not { Count: > 0 }) { <p>No policies yet</p> } else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); @foreach (var (type, value) in PolicyEventsByType) { <p> @(GetValidPolicyEventsByType(type).Count) active, @@ -33,6 +36,8 @@ else { </p> } + Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}"); + @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) { <details> <summary> @@ -41,7 +46,7 @@ else { </span> <hr style="margin: revert;"/> </summary> - <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;"> + <table class="table table-striped table-hover"> @{ var policies = GetValidPolicyEventsByType(type); var invalidPolicies = GetInvalidPolicyEventsByType(type); @@ -51,13 +56,18 @@ else { .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) .ToFrozenSet(); var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + + var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenSet(); + Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); } <thead> <tr> @foreach (var name in propNames) { - <th style="border-width: 1px">@name</th> + <th>@name</th> } - <th style="border-width: 1px">Actions</th> + <th>Actions</th> </tr> </thead> <tbody style="border-width: 1px;"> @@ -65,10 +75,6 @@ else { <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()}"); } @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) { <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td> @@ -94,11 +100,11 @@ else { @("Invalid " + GetPolicyTypeName(type).ToLower()) </u> </summary> - <table class="table table-striped table-hover" style="width: fit-content; border-width: 1px; vertical-align: middle;"> + <table class="table table-striped table-hover"> <thead> <tr> - <th style="border-width: 1px">State key</th> - <th style="border-width: 1px">Json contents</th> + <th>State key</th> + <th>Json contents</th> </tr> </thead> <tbody> @@ -115,12 +121,19 @@ else { </details> </details> } + + Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); } @if (CurrentlyEditingEvent is not null) { <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal> } +@if (MassCreatePolicies) { + <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal> +} + @code { #if DEBUG @@ -130,21 +143,14 @@ else { #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; } = null!; private bool _enableAvatars; private StateEventResponse? _currentlyEditingEvent; + private bool _massCreatePolicies; - // 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(); private StateEventResponse? CurrentlyEditingEvent { @@ -155,18 +161,18 @@ else { } } - // 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; } + public bool MassCreatePolicies { + get => _massCreatePolicies; + set { + _massCreatePolicies = value; + StateHasChanged(); + } + } + protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); await base.OnInitializedAsync(); @@ -193,48 +199,13 @@ else { 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(); + .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["entity"]?.GetValue<string>())).ToList(); + .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() ?? type.GetCustomAttributes<MatrixEventAttribute>() @@ -243,13 +214,13 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; private async Task RemovePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { }); + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { }); PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); await LoadStatesAsync(); } private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent); CurrentlyEditingEvent = null; await LoadStatesAsync(); } diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css new file mode 100644
index 0000000..afe9fb0 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css
@@ -0,0 +1,9 @@ +th { + border-width: 1px; +} + +table { + width: fit-content; + border-width: 1px; + vertical-align: middle; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor new file mode 100644
index 0000000..adac385 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -0,0 +1,213 @@ +@page "/Rooms/{RoomId}/Policies2" +@using LibMatrix +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Diagnostics +@using LibMatrix.RoomTypes +@using System.Collections.Frozen +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix.EventTypes + +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +<h3>Policy list editor - Editing @RoomId</h3> +<hr/> +@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ +<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> + +@if (Loading) { + <p>Loading...</p> +} +else if (PolicyEventsByType is not { Count: > 0 }) { + <p>No policies yet</p> +} +else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); + @foreach (var (type, value) in PolicyEventsByType) { + <p> + @(GetValidPolicyEventsByType(type).Count) active, + @(GetInvalidPolicyEventsByType(type).Count) invalid + (@value.Count total) + @(GetPolicyTypeName(type).ToLower()) + </p> + } + + Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}"); + + @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> + <div class="flex-grid"> + @{ + var policies = GetValidPolicyEventsByType(type); + var invalidPolicies = GetInvalidPolicyEventsByType(type); + // enumerate all properties with friendly name + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) + .ToFrozenSet(); + var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + + var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenSet(); + Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); + } + @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { + <div class="flex-item"> + @{ + var typedContent = policy.TypedContent!; + } + @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) { + <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</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> + </div> + } + </div> + <details> + <summary> + <u> + @("Invalid " + GetPolicyTypeName(type).ToLower()) + </u> + </summary> + <table class="table table-striped table-hover"> + <thead> + <tr> + <th>State key</th> + <th>Json contents</th> + </tr> + </thead> + <tbody> + @foreach (var policy in invalidPolicies) { + <tr> + <td>@policy.StateKey</td> + <td> + <pre>@policy.RawContent.ToJson(true, false)</pre> + </td> + </tr> + } + </tbody> + </table> + </details> + </details> + } + + Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); +} + +@if (CurrentlyEditingEvent is not null) { + <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal> +} + +@code { + +#if DEBUG + private const bool Debug = true; +#else + private const bool Debug = false; +#endif + + private bool Loading { get; set; } = true; + + [Parameter] + public string RoomId { get; set; } = null!; + + private bool _enableAvatars; + private StateEventResponse? _currentlyEditingEvent; + + private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); + + private StateEventResponse? CurrentlyEditingEvent { + get => _currentlyEditingEvent; + set { + _currentlyEditingEvent = value; + StateHasChanged(); + } + } + + 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(); + Homeserver = (await RMUStorage.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() { + Loading = true; + var states = Room.GetFullStateAsync(); + PolicyEventsByType.Clear(); + await foreach (var state in states) { + if (state is null) continue; + if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); + PolicyEventsByType[state.MappedType].Add(state); + } + + Loading = false; + StateHasChanged(); + } + + 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?["recommendation"]?.GetValue<string>())).ToList(); + + private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.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.UrlEncode(), new { }); + PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + await LoadStatesAsync(); + } + + private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { + await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent); + CurrentlyEditingEvent = null; + await LoadStatesAsync(); + } + + 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 diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css new file mode 100644
index 0000000..d224737 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor.css
@@ -0,0 +1,32 @@ +th { + border-width: 1px; +} + +table { + width: fit-content; + border-width: 1px; + vertical-align: middle; +} + +.flex-grid { + display: grid; + /*grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));*/ + /*// fit based on content max width*/ + grid-template-columns: repeat(auto-fill, minmax(min-content, 1fr)); + + gap: 10px; +} + +.flex-item { + /*flex: 1 1 30%;*/ + /*margin: 0.25rem;*/ + /*position: relative;*/ + /*display: flex;*/ + /*flex-direction: column;*/ + min-width: 0; + word-wrap: break-word; + background-color: #fff1; + background-clip: border-box; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: .5rem +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 01ab1c4..8153224 100644 --- a/MatrixUtils.Web/Pages/Rooms/Space.razor +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -1,12 +1,14 @@ @page "/Rooms/{RoomId}/Space" +@using System.Collections.ObjectModel @using LibMatrix.RoomTypes @using ArcaneLibs.Extensions @using LibMatrix +@using MatrixUtils.Abstractions <h3>Room manager - Viewing Space</h3> <button onclick="@JoinAllRooms">Join all rooms</button> @foreach (var room in Rooms) { - <RoomListItem Room="room" ShowOwnProfile="true"></RoomListItem> + <RoomListItem RoomInfo="room" ShowOwnProfile="true"></RoomListItem> } @@ -27,7 +29,7 @@ private GenericRoom? Room { get; set; } private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>(); - private List<GenericRoom> Rooms { get; } = new(); + private List<RoomInfo> Rooms { get; } = new(); private List<string> ServersInSpace { get; } = new(); protected override async Task OnInitializedAsync() { @@ -43,7 +45,18 @@ var roomId = stateEvent.StateKey; var room = hs.GetRoom(roomId); if (room is not null) { - Rooms.Add(room); + Task.Run(async () => { + try { + Rooms.Add(new(Room, await room.GetFullStateAsListAsync())); + } + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) { + Rooms.Add(new(Room) { + RoomName = "M_FORBIDDEN" + }); + } + } + }); } break; } @@ -96,8 +109,32 @@ // List<Task<RoomIdResponse>> tasks = Rooms.Select(room => room.JoinAsync(ServersInSpace.ToArray())).ToList(); // await Task.WhenAll(tasks); foreach (var room in Rooms) { + await JoinRecursive(room.Room.RoomId); + } + } + + private async Task JoinRecursive(string roomId) { + var room = Room!.Homeserver.GetRoom(roomId); + if (room is null) return; + try { await room.JoinAsync(ServersInSpace.ToArray()); + var joined = false; + while (!joined) { + var ce = await room.GetCreateEventAsync(); + if(ce is null) continue; + if (ce.Type == "m.space") { + var children = room.AsSpace.GetChildrenAsync(true); + await foreach (var child in children) { + JoinRecursive(child.RoomId); + } + } + joined = true; + } + } + catch (Exception e) { + Console.WriteLine(e); } + } } diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 57d3557..541cfe8 100644 --- a/MatrixUtils.Web/Pages/StreamTest.razor +++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -11,8 +11,8 @@ @* <StreamedImage Stream="@Stream"/> *@ <br/> - @foreach (var stream in Streams.OrderBy(x=>x.GetHashCode())) { - <StreamedImage Stream="@stream" style="width: 12em; height: 12em;"/> + @foreach (var stream in Streams.OrderBy(x => x.GetHashCode())) { + <StreamedImage Stream="@stream" style="width: 12em; height: 12em; object-fit: cover;"/> } } @@ -47,20 +47,34 @@ var roomState = await Homeserver.GetRoom("!dSMpkVKGgQHlgBDSpo:matrix.org").GetFullStateAsListAsync(); var members = roomState.Where(x => x.Type == RoomMemberEventContent.EventId).ToList(); Console.WriteLine($"Got {members.Count()} members"); + var ss = new SemaphoreSlim(1, 1); foreach (var stateEventResponse in members) { // Console.WriteLine(stateEventResponse.ToJson()); var mc = stateEventResponse.TypedContent as RoomMemberEventContent; if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) { var uri = mc.AvatarUrl[6..].Split('/'); var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}"; + // Homeserver.GetMediaStreamAsync(mc?.AvatarUrl).ContinueWith(async x => { + // await ss.WaitAsync(); + // var stream = x.Result; + // Streams.Add(stream); + // StateHasChanged(); + await Task.Delay(100); + // ss.Release(); + // }); try { Homeserver.ClientHttpClient.GetStreamAsync(url).ContinueWith(async x => { + // await ss.WaitAsync(); var stream = x.Result; Streams.Add(stream); StateHasChanged(); + // await Task.Delay(100); + // ss.Release(); }); } - catch { } + catch (Exception e) { + Console.WriteLine(e); + } } } } diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index ea1e5f6..a4e3918 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,6 +1,7 @@ @page "/Tools/Moderation/MassCMEBan" @using System.Collections.ObjectModel @using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.RoomTypes <h3>User Trace</h3> <hr/> @@ -17,19 +18,19 @@ } @code { + // TODO: Properly implement page to be more useful private ObservableCollection<string> log { get; set; } = new(); private AuthenticatedHomeserverGeneric hs { get; set; } - + [Parameter, SupplyParameterFromQuery(Name = "room")] public string roomId { get; set; } - protected override async Task OnInitializedAsync() { log.CollectionChanged += (sender, args) => StateHasChanged(); hs = await RMUStorage.GetCurrentSessionOrNavigate(); if (hs is null) return; - + StateHasChanged(); Console.WriteLine("Rerendered!"); await base.OnInitializedAsync(); @@ -37,33 +38,41 @@ private async Task<string> Execute() { var room = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev"); - // var room = hs.GetRoom("!yf7OpOiRDXx6zUGpT6:conduit.rory.gay"); - var users = roomId.Split("\n").Select(x => x.Trim()).Where(x=>x.StartsWith('@')).ToList(); - foreach (var user in users) { - var exists = false; - try { - exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity); - } catch (Exception e) { - log.Add($"Failed to get {user}"); - } + // var room = hs.GetRoom("!IVSjKMsVbjXsmUTuRR:rory.gay"); + var users = roomId.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList(); + var tasks = users.Select(x => ExecuteBan(room, x)).ToList(); + await Task.WhenAll(tasks); + + StateHasChanged(); + + return ""; + } - if (!exists) { + private async Task ExecuteBan(GenericRoom room, string user) { + var exists = false; + try { + exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity); + } + catch (Exception e) { + log.Add($"Failed to get {user}"); + } + + if (!exists) { + try { var evt = await room.SendStateEventAsync(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'), new UserPolicyRuleEventContent() { Entity = user, - Reason = "spam (invite)", + Reason = "spam", Recommendation = "m.ban" }); log.Add($"Sent {evt.EventId} to ban {user}"); } - else { - log.Add($"User {user} already exists"); + catch (Exception e) { + log.Add($"Failed to ban {user}: {e}"); } } - - - StateHasChanged(); - - return ""; + else { + log.Add($"User {user} already exists"); + } } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index e5ba004..94afc9a 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -55,75 +55,92 @@ if (ChronologicalOrder) { filteredMemberships = filteredMemberships.Reverse(); } - if(!string.IsNullOrWhiteSpace(Sender)) { + + if (!string.IsNullOrWhiteSpace(Sender)) { filteredMemberships = filteredMemberships.Where(x => x.Sender == Sender); } - if(!string.IsNullOrWhiteSpace(User)) { + + if (!string.IsNullOrWhiteSpace(User)) { filteredMemberships = filteredMemberships.Where(x => x.StateKey == User); } + <table> @foreach (var membership in filteredMemberships) { RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent; - @switch (content.Membership) { - case RoomMemberEventContent.MembershipTypes.Invite: { - if (_showInvites) { - <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Ban: { - if (_showBans) { - <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } + StateEventResponse? previous = previousMemberships.GetValueOrDefault(membership.StateKey); + RoomMemberEventContent? previousContent = previous?.TypedContent as RoomMemberEventContent; + <tr> + <td>@DateTimeOffset.FromUnixTimeMilliseconds(membership.OriginServerTs ?? 0).ToString("g")</td> + <td> + @switch (content.Membership) { + case RoomMemberEventContent.MembershipTypes.Invite: { + if (_showInvites) { + <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + } + + break; + } + case RoomMemberEventContent.MembershipTypes.Ban: { + if (_showBans) { + <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + } - break; - } - case RoomMemberEventContent.MembershipTypes.Leave: { - if (membership.Sender == membership.StateKey) { - if (_showLeaves) { - <p style="color: #C66;">@membership.Sender left the room</p> + break; } - } - else { - if (_showKicks) { - <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + case RoomMemberEventContent.MembershipTypes.Leave: { + if (membership.Sender == membership.StateKey) { + if (_showLeaves) { + <p style="color: #C66;">@membership.Sender left the room</p> + } + } + else { + if (_showKicks) { + <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + } + } + + break; } - } - - break; - } - case RoomMemberEventContent.MembershipTypes.Knock: { - if (_showKnocks) { - <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> - } + case RoomMemberEventContent.MembershipTypes.Knock: { + if (_showKnocks) { + <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + } - break; - } - case RoomMemberEventContent.MembershipTypes.Join: { - if (previousMemberships.TryGetValue(membership.StateKey, out var previous) - && (previous.TypedContent as RoomMemberEventContent).Membership == RoomMemberEventContent.MembershipTypes.Join) { - if (_showUpdates) { - <p style="color: #777;">@membership.Sender changed their profile</p> + break; } - } - else { - if (_showJoins) { - <p style="color: #6C6;">@membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p> + case RoomMemberEventContent.MembershipTypes.Join: { + if (previousContent is { Membership: RoomMemberEventContent.MembershipTypes.Join }) { + if (_showUpdates) { + <p style="color: #777;"> + @membership.Sender changed their profile<br/> + Display name: @previousContent.DisplayName -> @content.DisplayName<br/> + Avatar URL: @previousContent.AvatarUrl -> @content.AvatarUrl + </p> + } + } + else { + if (_showJoins) { + <p style="color: #6C6;"> + @membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")<br/> + Display name: @content.DisplayName<br/> + Avatar URL: @content.AvatarUrl + </p> + } + } + + break; + } + default: { + <b>Unknown membership @content.Membership!</b> + break; } } - - break; - } - default: { - <b>Unknown membership @content.Membership!</b> - break; - } - } + </td> + </tr> previousMemberships[membership.StateKey] = membership; } - } + </table>} </details> <br/> @@ -217,9 +234,9 @@ StateHasChanged(); } } - + private string sender = ""; - + private string Sender { get => sender; set { @@ -227,9 +244,9 @@ StateHasChanged(); } } - + private string user = ""; - + private string User { get => user; set { diff --git a/MatrixUtils.Web/Shared/MainLayout.razor.css b/MatrixUtils.Web/Shared/MainLayout.razor.css
index 01a5066..924393b 100644 --- a/MatrixUtils.Web/Shared/MainLayout.razor.css +++ b/MatrixUtils.Web/Shared/MainLayout.razor.css
@@ -57,6 +57,7 @@ main { .sidebar { width: 250px; + min-width: 250px; height: 100vh; position: sticky; top: 0; diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor new file mode 100644
index 0000000..11ba18a --- /dev/null +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -0,0 +1,102 @@ +@using LibMatrix.EventTypes.Spec.State.Policy +@using System.Reflection +@using ArcaneLibs.Attributes +@using LibMatrix +@using System.Collections.Frozen +@using LibMatrix.EventTypes +@using LibMatrix.RoomTypes +<ModalWindow Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType??"") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))" + OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600"> + <span>Policy type:</span> + <select @bind="@MappedType"> + <option>Select a value</option> + @foreach (var (type, mappedType) in PolicyTypes) { + <option value="@type">@mappedType.GetFriendlyName().ToLower()</option> + } + </select><br/> + + <span>Reason:</span> + <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/> + + <span>Recommendation:</span> + <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/> + + <span>Entities:</span><br/> + <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/> + + + @* <details> *@ + @* <summary>JSON data</summary> *@ + @* <pre> *@ + @* $1$ @PolicyEvent.ToJson(true, true) #1# *@ + @* </pre> *@ + @* </details> *@ + <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton> + <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton> + +</ModalWindow> + +@code { + + [Parameter] + public required Action OnClose { get; set; } + + [Parameter] + public required Action OnSaved { get; set; } + + [Parameter] + public required GenericRoom Room { get; set; } + + public string Recommendation { get; set; } = "m.ban"; + public string Reason { get; set; } = "spam"; + public string Users { get; set; } = ""; + + private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes + .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + + private string? MappedType { get; set; } + + private async Task Save() { + try { + await DoActualSave(); + } + catch (Exception e) { + Console.WriteLine($"Failed to save: {e}"); + } + } + + private async Task DoActualSave() { + Console.WriteLine($"Saving ---"); + Console.WriteLine($"Users = {Users}"); + var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList(); + var tasks = users.Select(x => ExecuteBan(Room, x)).ToList(); + await Task.WhenAll(tasks); + + OnSaved.Invoke(); + } + + private async Task ExecuteBan(GenericRoom room, string entity) { + bool success = false; + while (!success) { + try { + var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent; + content.Recommendation = Recommendation; + content.Reason = Reason; + content.Entity = entity; + await room.SendStateEventAsync(MappedType!, content.GetDraupnir2StateKey(), content); + success = true; + } + catch (MatrixException e) { + if (e is not { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw; + Console.WriteLine(e); + } + catch (Exception e) { + //ignored + Console.WriteLine(e); + } + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 1bd00d1..fc536c0 100644 --- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor +++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -35,39 +35,61 @@ </thead> <tbody> @foreach (var prop in props) { + var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null; <tr> <td style="padding-right: 8px;"> <span>@prop.GetFriendlyName()</span> - @if (Nullable.GetUnderlyingType(prop.PropertyType) is not null) { + @if (Nullable.GetUnderlyingType(prop.PropertyType) is null) { <span style="color: red;">*</span> } </td> @{ var getter = prop.GetGetMethod(); var setter = prop.GetSetMethod(); - } - @switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) { - case Type t when t == typeof(string): - <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox> - break; - default: - <p style="color: red;">Unsupported type: @prop.PropertyType</p> - break; + if (getter is null) { + <p style="color: red;">Missing property getter: @prop.Name</p> + } + else { + switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) { + case Type t when t == typeof(string): + <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox> + break; + case Type t when t == typeof(DateTime): + if (!isNullable) { + <InputDate TValue="DateTime" Value="@(getter?.Invoke(PolicyData, null) as DateTime? ?? new DateTime())" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate> + } + else { + var value = getter?.Invoke(PolicyData, null) as DateTime?; + if (value is null) { + <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Add value</button> + } + else { + var notNullValue = Nullable.GetValueRefOrDefaultRef(ref value); + Console.WriteLine($"Value: {value?.ToString() ?? "null"}"); + <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate> + <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove value</button> + } + } + + break; + default: + <p style="color: red;">Unsupported type: @prop.PropertyType</p> + break; + } + } } </tr> } </tbody> </table> - <br/> - <pre> - @PolicyEvent.ToJson(true, false) - </pre> + <details> + <summary>JSON data</summary> + <pre> + @PolicyEvent.ToJson(true, true) + </pre> + </details> <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton> <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton> - @* <span>Target entity: </span> *@ - @* <FancyTextBox @bind-Value="@policyData.Entity"></FancyTextBox><br/> *@ - @* <span>Reason: </span> *@ - @* <FancyTextBox @bind-Value="@policyData.Reason"></FancyTextBox> *@ } else { <p>Policy data is null</p> @@ -102,7 +124,7 @@ .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); private StateEventResponse? _policyEvent; - + private string? MappedType { get => _policyEvent?.Type; set { @@ -110,9 +132,9 @@ PolicyEvent.Type = value; PolicyEvent.TypedContent ??= Activator.CreateInstance(PolicyTypes[value]) as PolicyRuleEventContent; PolicyData = PolicyEvent.TypedContent as PolicyRuleEventContent; + PolicyData.Recommendation ??= "m.ban"; } } } - } \ No newline at end of file