about summary refs log tree commit diff
path: root/MatrixUtils.Web
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-03-11 07:41:46 +0100
committerRory& <root@rory.gay>2025-03-11 07:41:46 +0100
commit90549fd2bce62723c35cc7d40551f1c5f2d2ba9d (patch)
tree100864824013a238a318e1f6e4f9b0952787e900 /MatrixUtils.Web
parentWork on rewriting homeserver resolution (diff)
downloadMatrixUtils-90549fd2bce62723c35cc7d40551f1c5f2d2ba9d.tar.xz
Synapse admin tooling, well known res work
Diffstat (limited to 'MatrixUtils.Web')
-rw-r--r--MatrixUtils.Web/MatrixUtils.Web.csproj1
-rw-r--r--MatrixUtils.Web/Pages/Dev/WellKnownRes.razor67
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor3
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor29
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor191
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css0
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor113
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor241
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor11
-rw-r--r--MatrixUtils.Web/Program.cs2
11 files changed, 633 insertions, 30 deletions
diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj

index acb4054..aa9f37f 100644 --- a/MatrixUtils.Web/MatrixUtils.Web.csproj +++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -41,6 +41,7 @@ <PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" /> + <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.1" /> <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.5.39" /> </ItemGroup> diff --git a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index 36fa247..9dc9556 100644 --- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor +++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -1,6 +1,7 @@ @page "/Dev/WellKnownRes" @using ArcaneLibs.Extensions -@using LibMatrix.Services.WellKnownResolvers +@using LibMatrix.Services.WellKnownResolver +@using LibMatrix.Services.WellKnownResolver.WellKnownResolvers @inject HomeserverResolverService legacyResolver @inject WellKnownResolverService rewriteResolver @inject ClientWellKnownResolver rewriteClientResolver @@ -9,18 +10,39 @@ <span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span> +<span>Stats:</span><br/> +<span>Server count: @entries.Count</span><br/> +<span>Client server resolution rate (N/O/T): @entries.Count(x => x.HasClientWellKnown)/@entries.Count(x => !string.IsNullOrWhiteSpace(x.LegacyResolutionResult?.Client))/@entries.Count</span> +<br/> +<span>Server server resolution rate (N/T): @entries.Count(x => x.HasServerWellKnown)/@entries.Count</span><br/> +<span>Support resolution rate (N/T): @entries.Count(x => x.HasSupportWellKnown)/@entries.Count</span><br/> <table class="table-bordered"> <thead> <td>Homeserver</td> - <td>Legacy client URL</td> - <td>Rewrite client URL</td> + <td>Client API</td> + <td>Server API</td> + <td>Has support record</td> </thead> @foreach (var entry in entries) { - <tr style="background-color: @GetColor(entry)"> + <tr> <td>@entry.Homeserver</td> - <td>@(entry.LegacyClientUrl ?? "null")</td> - <td>@(entry.RewriteClientUrl ?? "null")</td> + <td style="background-color: @GetClientColor(entry)"> + <span>L: @entry.LegacyResolutionResult?.Client</span><br/> + <span>R: @entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver.BaseUrl</span> + </td> + <td style="background-color: @GetServerColor(entry)"> + <span>L: @entry.LegacyResolutionResult?.Server</span><br/> + <span>R: @entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver</span> + </td> + <td>@(entry.HasSupportWellKnown ? "Y" : "X")</td> + </tr> + <tr> + <td colspan="6"> + <details> + <pre>@(entry.WellKnownResolutionResult?.ToJson() ?? "null")</pre> + </details> + </td> </tr> } </table> @@ -44,8 +66,12 @@ private class TableEntry { public required string Homeserver { get; set; } - public string? LegacyClientUrl { get; set; } - public string? RewriteClientUrl { get; set; } + public HomeserverResolverService.WellKnownUris? LegacyResolutionResult { get; set; } + public WellKnownResolverService.WellKnownRecords? WellKnownResolutionResult { get; set; } + + public bool HasClientWellKnown => WellKnownResolutionResult?.ClientWellKnown is { Content.Homeserver.BaseUrl: { Length: > 0 } }; + public bool HasServerWellKnown => WellKnownResolutionResult?.ServerWellKnown is { Content.Homeserver.Length: > 0 }; + public bool HasSupportWellKnown => WellKnownResolutionResult?.SupportWellKnown?.Content is not null and not { SupportPage: null, Contacts: null or { Count: 0 } }; } private async Task Execute() { @@ -55,7 +81,7 @@ foreach (var homeserver in homeservers) { var e = new TableEntry() { Homeserver = homeserver }; _ = TryResolveLegacy(e); - _ = TryResolveRewrite(e); + _ = TryFullResolveRewrite(e); entries.Add(e); } @@ -65,23 +91,32 @@ private async Task TryResolveLegacy(TableEntry entry) { try { - entry.LegacyClientUrl = (await legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false)).Client; + var cTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false); + var sTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableClient: false); + entry.LegacyResolutionResult = (await cTask); + entry.LegacyResolutionResult.Server = (await sTask).Server; StateHasChanged(); } catch { } } - private async Task TryResolveRewrite(TableEntry entry) { + private async Task TryFullResolveRewrite(TableEntry entry) { try { - entry.RewriteClientUrl = (await rewriteClientResolver.TryResolveClientWellKnown(entry.Homeserver)).WellKnown.Homeserver.BaseUrl; + entry.WellKnownResolutionResult = await rewriteResolver.TryResolveWellKnownRecords(entry.Homeserver); StateHasChanged(); } catch { } } - - private string GetColor(TableEntry entry) { - if (entry.LegacyClientUrl == entry.RewriteClientUrl && entry.RewriteClientUrl == null) return "#333333"; - if (entry.LegacyClientUrl == entry.RewriteClientUrl?.TrimEnd('/')) return "#008800"; + + private string GetClientColor(TableEntry entry) { + if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl && entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl == null) return "#333333"; + if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl?.TrimEnd('/')) return "#008800"; + return "#ff0000"; + } + + private string GetServerColor(TableEntry entry) { + if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver && entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver == null) return "#333333"; + if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver.TrimEnd('/')) return "#008800"; return "#ff0000"; } diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index 09cc2cd..6ccdce4 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -10,7 +10,8 @@ else { @if (Homeserver is AuthenticatedHomeserverSynapse) { <h4>Synapse tools</h4> <hr/> - <a href="/HSAdmin/RoomQuery">Query rooms</a> + <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/> + <a href="/HSAdmin/Synapse/BlockMedia">Block media</a> } else if (Homeserver is AuthenticatedHomeserverHSE) { <h4>Rory&amp;::LibMatrix.HomeserverEmulator tools</h4> diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor new file mode 100644
index 0000000..02bf88f --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
@@ -0,0 +1,29 @@ +@page "/HSAdmin/Synapse/BackgroundJobs" +@using ArcaneLibs.Extensions +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses + +<h3>Homeserver Administration - Background jobs</h3> +<pre>@BackgroundJobStatus?.ToJson(ignoreNull: true)</pre> + +@code { + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + private SynapseAdminBackgroundUpdateStatusResponse? BackgroundJobStatus { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await RmuStorage.GetCurrentSessionOrNavigate() as AuthenticatedHomeserverSynapse; + if (hs is null) return; + Homeserver = hs; + + while (true) { + try { + BackgroundJobStatus = await hs.Admin.GetBackgroundUpdatesStatusAsync(); + StateHasChanged(); + await Task.Delay(1000); + } + catch (Exception ex) { + Console.WriteLine(ex); + } + } + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor new file mode 100644
index 0000000..6de4e4d --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -0,0 +1,191 @@ +@page "/HSAdmin/Synapse/BlockMedia" +@using System.Text.Json +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec + +<h3>Homeserver Administration - Block media</h3> +@if (Homeserver is not null) { + <label>Event URL: </label> + <FancyTextBox @bind-Value="EventUrl"/> + <br/> + <label>Event JSON: </label> + <details> + <summary>@(string.IsNullOrEmpty(EventJson) ? "" : "{ ... }")</summary> + <FancyTextBox Multiline="true" @bind-Value="EventJson"/> + </details> + <br/> + <label>MXC URI: </label> + <FancyTextBox @bind-Value="MxcUrl"/> + <br/> + <label>Room ID: </label> + <FancyTextBox @bind-Value="RoomId"/> + <br/> + <pre>@MxcUri?.ToJson(ignoreNull: true)</pre> + + @if (Event is not null) { + <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton> + } + + @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) { + <p>User is a local user!</p> + <LinkButton OnClick="@DeactivateUser">Deactivate User</LinkButton> + <LinkButton OnClick="@QuarantineMediaByUser">Quarantine all media</LinkButton> + } +} + +<style> + .int-input { + width: 128px; + } + + .tile { + display: inline-block; + padding: 4px; + border: 1px solid #ffffff22; + } + + .tile280 { + min-width: 280px; + } + + .tile150 { + min-width: 150px; + } + + .range-sep { + display: inline-block; + padding: 4px; + width: 150px; + } + + .range-sep::before { + content: "@("<") "; + } + + .range-sep::after { + content: " @("<")"; + } + + .center-children { + text-align: center; + } +</style> + +@code { + + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + + protected override async Task OnInitializedAsync() { + var hs = await RmuStorage.GetCurrentSessionOrNavigate() as AuthenticatedHomeserverSynapse; + if (hs is null) return; + Homeserver = hs; + + if (!string.IsNullOrWhiteSpace(EventUrl)) { + _ = ExpandEventUrl(); + } + } + + [SupplyParameterFromQuery] + public string? EventUrl { + get; + set { + field = value?.Split('?')[0]; + _ = ExpandEventUrl(); + } + } + + private StateEventResponse? Event { get; set; } + + private string? EventJson { + get; + set { + field = value; + _ = ExpandEventJson(); + } + } + + private string? MxcUrl { + get; + set { + field = value; + _ = ExpandMxcUri(); + } + } + + private MxcUri? MxcUri { get; set; } + + private string? RoomId { + get => Event?.RoomId ?? field; + set; + } + + private async Task ExpandEventUrl() { + Console.WriteLine("Expanding event URL..."); + if (!string.IsNullOrWhiteSpace(EventUrl)) { + Console.WriteLine("Parsing event URL..."); + var data = ParseEventUrl(EventUrl); + Console.WriteLine($"Room: {data.room}, Event: {data.eventId}"); + RoomId = data.room; + var room = Homeserver.GetRoom(data.room); + var eventResponse = await room.GetEventAsync(data.eventId); + eventResponse.RoomId ??= data.room; + EventJson = eventResponse?.ToJson() ?? "null"; + } + + StateHasChanged(); + } + + private async Task ExpandEventJson() { + Console.WriteLine("Expanding event JSON..."); + if (!string.IsNullOrWhiteSpace(EventJson)) { + Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson); + MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url; + Console.WriteLine($"MXC URL: {MxcUrl}"); + + var possiblyRelated = await Homeserver.Admin.GetRoomMediaAsync(Event!.RoomId!); + } + + StateHasChanged(); + } + + private async Task ExpandMxcUri() { + Console.WriteLine("Expanding MXC URI..."); + if (!string.IsNullOrWhiteSpace(MxcUrl)) { + MxcUri = MxcUrl; + } + + StateHasChanged(); + } + + private (string room, string eventId) ParseEventUrl(string url) { + var parts = url.Split('/'); + Console.WriteLine($"Parts: {string.Join(", ", parts)}"); + return (parts[4].UrlDecode(), parts[5].Split('?')[0].UrlDecode()); + } + +#region Local user + + private async Task DeactivateUser() { + await Homeserver.Admin.DeactivateUserAsync(Event.Sender, true); + } + + private async Task QuarantineMediaByUser() { + if (Event is null) return; + var media = Homeserver.Admin.GetUserMediaEnumerableAsync(Event?.Sender!); + await foreach (var m in media) { + if (m is not null) { + // await Homeserver.Admin.QuarantineMedia(m); + // await Homeserver.Admin.DeleteMedia(m); + } + } + } + +#endregion + + private async Task RedactAllEvents() { + if (Event is null) return; + await Homeserver!.Admin.DeleteAllMessages(Event.Sender!); + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css new file mode 100644
index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor new file mode 100644
index 0000000..d598994 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
@@ -0,0 +1,5 @@ +<h3>SynapseRoomShutdownWindow</h3> + +@code { + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor new file mode 100644
index 0000000..d5daf75 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -0,0 +1,113 @@ +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests + +@if (string.IsNullOrWhiteSpace(Context.DeleteId)) { + <b>Media options</b> + <br/> + <hr/> + <span>Quarantine local media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalMedia"/> + <br/> + <span>Quarantine remote media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineRemoteMedia"/> + <br/> + <span>Delete remote media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteRemoteMedia"/> + <br/> + + <b>User options</b> + <br/> + <hr/> + <span>Suspend local users: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.SuspendLocalUsers"></InputCheckbox> + <br/> + <span>Quarantine <b>ALL</b> local user media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalUserMedia"></InputCheckbox> + <br/> + <span>Delete <b>ALL</b> local user media: </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox> + <br/> + + <b>Room deletion options</b> + <br/> + <hr/> + <span>Block room: </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/> + <br/> + <span>Purge room: </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.Purge"/> + <br/> + <span>Force purge room (unsafe): </span> + <InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox> + <br/> + <span>Warning room User ID (optional): </span> + <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/> + <br/> + @if (!string.IsNullOrWhiteSpace(Context.DeleteRequest.NewRoomUserId)) { + <span>Warning room name: </span> + <FancyTextBox @bind-Value="@Context.DeleteRequest.RoomName"/> + <br/> + <span>Warning room message (plaintext): </span> + <FancyTextBox Multiline="true" @bind-Value="@Context.DeleteRequest.Message"/> + <br/> + } + + <LinkButton OnClick="@DeleteRoom">Execute</LinkButton> +} + +@code { + + [Parameter] + public required RoomShutdownContext Context { get; set; } + + [Parameter] + public required AuthenticatedHomeserverSynapse Homeserver { get; set; } + + private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + TaskMap = new NamedCache<RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + } + + public class RoomShutdownContext { + public required string RoomId { get; set; } + public string? DeleteId { get; set; } + public ExtraDeleteOptions ExtraOptions { get; set; } = new(); + + public SynapseAdminRoomDeleteRequest DeleteRequest { get; set; } = new() { + Block = true, + Purge = true, + ForcePurge = false + }; + + public class ExtraDeleteOptions { + // room options + public bool QuarantineLocalMedia { get; set; } + public bool QuarantineRemoteMedia { get; set; } + + public bool DeleteRemoteMedia { get; set; } + + // user options + public bool SuspendLocalUsers { get; set; } + public bool QuarantineLocalUserMedia { get; set; } + public bool DeleteLocalUserMedia { get; set; } + } + } + + public async Task OnComplete() { + await OnCompleteLock.WaitAsync(); + try { + await TaskMap.RemoveValueAsync(Context.DeleteId!); + } + finally { + OnCompleteLock.Release(); + } + } + + public async Task DeleteRoom() { + await TaskMap.SetValueAsync(Context.RoomId, Context); + } + + private static readonly SemaphoreSlim OnCompleteLock = new(1, 1); + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
index 07af1dc..0a93df8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,7 +1,10 @@ @page "/HSAdmin/Synapse/RoomQuery" -@using LibMatrix.Responses.Admin -@using LibMatrix.Filters +@using Microsoft.AspNetCore.WebUtilities @using ArcaneLibs.Extensions +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components <h3>Homeserver Administration - Room Query</h3> @@ -48,13 +51,17 @@ <u style="display: block;">Ranges</u> <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber + max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber> </span> <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber + max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber> </span> <span class="tile center-children"> - <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber> + <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span + class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" + @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber> </span> </div> </details> @@ -98,41 +105,84 @@ <br/> } </p> + <p> + <LinkButton OnClick="@(() => { + DeleteRequests.Add(res.RoomId, new() { + RoomId = res.RoomId, + DeleteRequest = new() { + Block = true, + Purge = true, + ForcePurge = false + } + }); + + return Task.CompletedTask; + })">Delete room + </LinkButton> + </p> <span>@res.StateEvents state events</span><br/> - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> + @if (res.LocalMembers is null) { + <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> + } + else { + <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</span> + } <details> <summary>Full result data</summary> <pre>@res.ToJson(ignoreNull: true)</pre> </details> </div> } +@* *@ +@* @if (DeleteRequest.HasValue) { *@ +@* <ModalWindow MinWidth="600" Title="@("Delete " + DeleteRequest.Value.RoomId)" OnCloseClicked="@(() => { DeleteRequest = null; })"> *@ +@* *@ +@* </ModalWindow> *@ +@* } *@ + +@* @foreach (var (roomId, status) in DeleteStatuses) { *@ +@* <ModalWindow Title="@("Delete status for " + roomId)" MinWidth="600"> *@ +@* <pre>@status.ToJson()</pre> *@ +@* </ModalWindow> *@ +@* } *@ + +@foreach(var (roomId, deleteRequest) in DeleteRequests) { + <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> +} <style> .int-input { width: 128px; } + .tile { display: inline-block; padding: 4px; border: 1px solid #ffffff22; } + .tile280 { min-width: 280px; } + .tile150 { min-width: 150px; } + .range-sep { display: inline-block; padding: 4px; width: 150px; } + .range-sep::before { content: "@("<") "; } + .range-sep::after { content: " @("<")"; } + .center-children { text-align: center; } @@ -152,16 +202,92 @@ [SupplyParameterFromQuery(Name = "ascending")] public bool Ascending { get; set; } - public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new(); + private List<RoomInfo> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; private string Status { get; set; } - public LocalRoomQueryFilter Filter { get; set; } = new(); + public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); + + private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = []; + + // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new(); protected override Task OnParametersSetAsync() { if (Ascending == null) Ascending = true; OrderBy ??= "name"; + + var execute = false; + + foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) { + switch (key) { + case "RoomIdContains": + Filter.RoomIdContains = value[0]!; + break; + case "NameContains": + Filter.NameContains = value[0]!; + break; + case "CanonicalAliasContains": + Filter.CanonicalAliasContains = value[0]!; + break; + case "VersionContains": + Filter.VersionContains = value[0]!; + break; + case "CreatorContains": + Filter.CreatorContains = value[0]!; + break; + case "EncryptionContains": + Filter.EncryptionContains = value[0]!; + break; + case "JoinRulesContains": + Filter.JoinRulesContains = value[0]!; + break; + case "GuestAccessContains": + Filter.GuestAccessContains = value[0]!; + break; + case "HistoryVisibilityContains": + Filter.HistoryVisibilityContains = value[0]!; + break; + case "Federatable": + Filter.Federatable = bool.Parse(value[0]!); + Filter.CheckFederation = true; + break; + case "Public": + Filter.Public = value[0] == "true"; + Filter.CheckPublic = true; + break; + case "JoinedMembersGreaterThan": + Filter.JoinedMembersGreaterThan = int.Parse(value[0]!); + break; + case "JoinedMembersLessThan": + Filter.JoinedMembersLessThan = int.Parse(value[0]!); + break; + case "JoinedLocalMembersGreaterThan": + Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!); + break; + case "JoinedLocalMembersLessThan": + Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!); + break; + case "StateEventsGreaterThan": + Filter.StateEventsGreaterThan = int.Parse(value[0]!); + break; + case "StateEventsLessThan": + Filter.StateEventsLessThan = int.Parse(value[0]!); + break; + case "Execute": + execute = true; + break; + default: + Console.WriteLine($"Unknown query parameter: {key}"); + break; + } + } + + if (execute) + _ = Search(); + return Task.CompletedTask; } @@ -169,18 +295,107 @@ Results.Clear(); var hs = await RmuStorage.GetCurrentSessionOrNavigate(); if (hs is AuthenticatedHomeserverSynapse synapse) { + Homeserver = synapse; var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); while (await searchRooms.MoveNextAsync()) { var room = searchRooms.Current; - Console.WriteLine("Hit: " + room.ToJson(false)); - Results.Add(room); - if (Results.Count % 10 == 0) + + if (Results.Count < 100) + Console.WriteLine("Hit: " + room.ToJson(false)); + + var roomInfo = new RoomInfo { + RoomId = room.RoomId, + Name = room.Name, + CanonicalAlias = room.CanonicalAlias, + Creator = room.Creator, + Version = room.Version, + Encryption = room.Encryption, + Federatable = room.Federatable, + Public = room.Public, + JoinRules = room.JoinRules, + GuestAccess = room.GuestAccess, + HistoryVisibility = room.HistoryVisibility, + StateEvents = room.StateEvents, + JoinedMembers = room.JoinedMembers, + JoinedLocalMembers = room.JoinedLocalMembers + }; + + Results.Add(roomInfo); + + if (room.JoinedLocalMembers is > 0 and < 100) + roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList(); + + if (Results.Count < 200 || Results.Count % 1000 == 0) { StateHasChanged(); + await Task.Yield(); + } } } StateHasChanged(); } + // + // private async Task DeleteRoom() { + // if (DeleteRequest is { } deleteRequest) { + // var media = await Homeserver.Admin.GetRoomMediaAsync(deleteRequest.RoomId); + // if (deleteRequest.DeleteRequest.QuarantineRemoteMedia) { + // foreach (var remoteMedia in media.Remote) { + // await Homeserver.Admin.QuarantineMediaById(remoteMedia); + // } + // } + // + // if (deleteRequest.DeleteRequest.DeleteRemoteMedia) { + // foreach (var remoteMedia in media.Remote) { + // await Homeserver.Admin.DeleteMediaById(remoteMedia); + // } + // } + // else if (deleteRequest.DeleteRequest.QuarantineLocalMedia) { + // foreach (var localMedia in media.Local) { + // await Homeserver.Admin.QuarantineMediaById(localMedia); + // } + // } + // + // var deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false); + // DeleteRequest = null; + // List<string> alreadyCleanedUsers = []; + // while (true) { + // var status = await Homeserver.Admin.GetRoomDeleteStatus(deleteId.DeleteId); + // DeleteStatuses[deleteRequest.RoomId] = status; + // StateHasChanged(); + // await Task.Delay(5000); + // if (status.Status == "complete") { + // DeleteStatuses.Remove(deleteRequest.RoomId); + // StateHasChanged(); + // break; + // } + // + // if (status.Status == "failed") { + // deleteId = await Homeserver.Admin.DeleteRoom(deleteRequest.RoomId, deleteRequest.DeleteRequest, waitForCompletion: false); + // } + // + // var newCleanedUsers = status.ShutdownRoom?.KickedUsers?.Except(alreadyCleanedUsers).ToList(); + // if (newCleanedUsers is not null) { + // alreadyCleanedUsers.AddRange(newCleanedUsers); + // foreach (var user in newCleanedUsers) { + // if (deleteRequest.DeleteRequest.SuspendLocalUsers) { + // // await Homeserver.Admin.(user); + // } + // + // if (deleteRequest.DeleteRequest.QuarantineLocalUserMedia) { + // await Homeserver.Admin.QuarantineMediaByUserId(user); + // } + // + // if (deleteRequest.DeleteRequest.DeleteLocalUserMedia) { + // var userMedia = Homeserver.Admin.GetUserMediaEnumerableAsync(user); + // await foreach (var mediaEntry in userMedia) { + // await Homeserver.Admin.DeleteMediaById(mediaEntry.MediaId); + // } + // } + // } + // } + // } + // } + // } private readonly Dictionary<string, string> validOrderBy = new() { { "name", "Room name" }, @@ -198,4 +413,8 @@ { "state_events", "Number of state events" } }; + private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { + public List<string>? LocalMembers { get; set; } + } + } diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 2549c12..d9503ee 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -2,6 +2,7 @@ @using System.Collections.Frozen @using System.Collections.ObjectModel @using System.Diagnostics +@using System.Text.Json @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes.Spec.State.RoomInfo @@ -422,7 +423,15 @@ var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] }; var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false)); await foreach (var resp in events) { - var all = resp.State.Concat(resp.Chunk); + var all = resp.State.Concat(resp.Chunk) + // ugly hack, because some users fuck around too much + .Select(x => { + if (x.RawContent?["displayname"]?.GetValueKind() != JsonValueKind.String) + x.RawContent?.Remove("displayname"); + if (x.RawContent?["avatar_url"]?.GetValueKind() is not JsonValueKind.String) + x.RawContent?.Remove("avatar_url"); + return x; + }); Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId)); Log.Add($"Got {resp.State.Count} state and {resp.Chunk.Count} timeline events."); diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 8bc2c8f..8cab0fb 100644 --- a/MatrixUtils.Web/Program.cs +++ b/MatrixUtils.Web/Program.cs
@@ -28,7 +28,7 @@ builder.Services.AddWebWorkerService(webWorkerService => webWorkerService.TaskPool.MaxPoolSize = 2; // Below is telling the WebWorkerService TaskPool to set the initial size to 2 if running in a Window scope and 0 otherwise // This starts up 2 WebWorkers to handle TaskPool tasks as needed - webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 2 : 0; + webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0; }); try {