about summary refs log tree commit diff
path: root/MatrixUtils.Web/Pages/HSAdmin
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixUtils.Web/Pages/HSAdmin')
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor10
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor43
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor201
-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.css (renamed from MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css)0
-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.razor420
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css0
10 files changed, 809 insertions, 203 deletions
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor

index 9c61431..e1b46e2 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -10,7 +10,13 @@ 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> + <hr/> + <a href="/HSAdmin/HSE/ManageExternalProfiles">Manage external profiles</a> } else { <p>Homeserver type @Homeserver.GetType().Name does not have any administration tools in RMU.</p> @@ -24,7 +30,7 @@ else { public ServerVersionResponse? ServerVersionResponse { get; set; } protected override async Task OnInitializedAsync() { - Homeserver = await RMUStorage.GetCurrentSessionOrNavigate(); + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; ServerVersionResponse = await (Homeserver.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)); await base.OnInitializedAsync(); diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor new file mode 100644
index 0000000..87600c6 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -0,0 +1,43 @@ +@page "/HSAdmin/HSE/ManageExternalProfiles" +@using ArcaneLibs.Extensions +@using LibMatrix.Responses +<h3>Manage external profiles</h3> + +<LinkButton OnClick="AddAllLocalProfiles">Add local sessions</LinkButton> + +@foreach(var p in ExternalProfiles) +{ + <h4>@p.Key</h4> + <pre>@p.Value.ToJson(indent: true)</pre> +} + +@code { + public AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private Dictionary<string, LoginResponse> ExternalProfiles = new(); + + protected override async Task OnInitializedAsync() + { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; + await LoadProfiles(); + await base.OnInitializedAsync(); + } + + private async Task LoadProfiles() { + if(Homeserver is AuthenticatedHomeserverHSE hse) + { + ExternalProfiles = await hse.GetExternalProfilesAsync(); + } + StateHasChanged(); + } + + private async Task AddAllLocalProfiles() { + if(Homeserver is AuthenticatedHomeserverHSE hse) { + var sessions = await sessionStore.GetAllSessions(); + foreach(var session in sessions) { + await hse.SetExternalProfile(session.Value.Auth.UserId, session.Value.Auth); + } + await LoadProfiles(); + } + } +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor deleted file mode 100644
index 11df261..0000000 --- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor +++ /dev/null
@@ -1,201 +0,0 @@ -@page "/HSAdmin/RoomQuery" -@using LibMatrix.Responses.Admin -@using LibMatrix.Filters -@using ArcaneLibs.Extensions - -<h3>Homeserver Administration - Room Query</h3> - -<label>Search name: </label> -<InputText @bind-Value="SearchTerm"/><br/> -<label>Order by: </label> -<select @bind="OrderBy"> - @foreach (var item in validOrderBy) { - <option value="@item.Key">@item.Value</option> - } -</select><br/> -<label>Ascending: </label> -<InputCheckbox @bind-Value="Ascending"/><br/> -<details> - <summary> - <span>Local filtering (slow)</span> - - </summary> - <div style="margin-left: 8px; margin-bottom: 8px;"> - <u style="display: block;">String contains</u> - <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span> - <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span> - <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span> - <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span> - <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span> - <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span> - <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span> - <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span> - <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span> - - <u style="display: block;">Optional checks</u> - <span class="tile tile150"> - <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated: - @if (Filter.CheckFederation) { - <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox> - } - </span> - <span class="tile tile150"> - <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public: - @if (Filter.CheckPublic) { - <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox> - } - </span> - - <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> - </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> - </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> - </span> - </div> -</details> -<button class="btn btn-primary" @onclick="Search">Search</button> -<br/> - -@if (Results.Count > 0) { - <p>Found @Results.Count rooms</p> - <details> - <summary>TSV data (copy/paste)</summary> - <pre style="font-size: 0.6em;"> - <table> - @foreach (var res in Results) { - <tr> - <td style="padding: 8px;">@res.RoomId@("\t")</td> - <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td> - <td style="padding: 8px;">@res.Creator@("\t")</td> - <td style="padding: 8px;">@res.Name</td> - </tr> - } - </table> - </pre> - </details> -} - -@foreach (var res in Results) { - <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;"> - @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ - <p> - @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) { - <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span> - <br/> - } - else { - <span>@res.RoomId (@res.Name)</span> - <br/> - } - @if (!string.IsNullOrWhiteSpace(res.Creator)) { - @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@ - <span>Created by @res.Creator</span> - <br/> - } - </p> - <span>@res.StateEvents state events</span><br/> - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> - <details> - <summary>Full result data</summary> - <pre>@res.ToJson(ignoreNull: true)</pre> - </details> - </div> -} - -<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 { - - [Parameter] - [SupplyParameterFromQuery(Name = "order_by")] - public string? OrderBy { get; set; } - - [Parameter] - [SupplyParameterFromQuery(Name = "name_search")] - public string SearchTerm { get; set; } - - [Parameter] - [SupplyParameterFromQuery(Name = "ascending")] - public bool Ascending { get; set; } - - public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new(); - - private string Status { get; set; } - - public LocalRoomQueryFilter Filter { get; set; } = new(); - - protected override Task OnParametersSetAsync() { - if (Ascending == null) - Ascending = true; - OrderBy ??= "name"; - return Task.CompletedTask; - } - - private async Task Search() { - Results.Clear(); - var hs = await RMUStorage.GetCurrentSessionOrNavigate(); - if (hs is AuthenticatedHomeserverSynapse 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) - StateHasChanged(); - } - } - - StateHasChanged(); - } - - private readonly Dictionary<string, string> validOrderBy = new() { - { "name", "Room name" }, - { "canonical_alias", "Main alias address" }, - { "joined_members", "Number of members (reversed)" }, - { "joined_local_members", "Number of local members (reversed)" }, - { "version", "Room version" }, - { "creator", "Creator of the room" }, - { "encryption", "End-to-end encryption algorithm" }, - { "federatable", "Is room federated" }, - { "public", "Visibility in room list" }, - { "join_rules", "Join rules" }, - { "guest_access", "Guest access" }, - { "history_visibility", "Visibility of history" }, - { "state_events", "Number of state events" } - }; - -} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor new file mode 100644
index 0000000..d855cba --- /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 sessionStore.GetCurrentHomeserver(navigateOnFailure: true) 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..d07ff08 --- /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 sessionStore.GetCurrentHomeserver(navigateOnFailure: true) 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/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
index e69de29..e69de29 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/RoomQuery.razor.css +++ 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 new file mode 100644
index 0000000..79e7357 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -0,0 +1,420 @@ +@page "/HSAdmin/Synapse/RoomQuery" +@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> + +<label>Search name: </label> +<InputText @bind-Value="SearchTerm"/><br/> +<label>Order by: </label> +<select @bind="OrderBy"> + @foreach (var item in validOrderBy) { + <option value="@item.Key">@item.Value</option> + } +</select><br/> +<label>Ascending: </label> +<InputCheckbox @bind-Value="Ascending"/><br/> +<details> + <summary> + <span>Local filtering (slow)</span> + + </summary> + <div style="margin-left: 8px; margin-bottom: 8px;"> + <u style="display: block;">String contains</u> + <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span> + <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span> + <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span> + <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span> + <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span> + <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span> + <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span> + <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span> + <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span> + + <u style="display: block;">Optional checks</u> + <span class="tile tile150"> + <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated: + @if (Filter.CheckFederation) { + <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox> + } + </span> + <span class="tile tile150"> + <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public: + @if (Filter.CheckPublic) { + <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox> + } + </span> + + <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> + </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> + </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> + </span> + </div> +</details> +<button class="btn btn-primary" @onclick="Search">Search</button> +<br/> + +@if (Results.Count > 0) { + <p>Found @Results.Count rooms</p> + <details> + <summary>TSV data (copy/paste)</summary> + <pre style="font-size: 0.6em;"> + <table> + @foreach (var res in Results) { + <tr> + <td style="padding: 8px;">@res.RoomId@("\t")</td> + <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td> + <td style="padding: 8px;">@res.Creator@("\t")</td> + <td style="padding: 8px;">@res.Name</td> + </tr> + } + </table> + </pre> + </details> +} + +@foreach (var res in Results) { + <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;"> + @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ + <p> + @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) { + <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span> + <br/> + } + else { + <span>@res.RoomId (@res.Name)</span> + <br/> + } + @if (!string.IsNullOrWhiteSpace(res.Creator)) { + @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@ + <span>Created by @res.Creator</span> + <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/> + @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; + } +</style> + +@code { + + [Parameter] + [SupplyParameterFromQuery(Name = "order_by")] + public string? OrderBy { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "name_search")] + public string SearchTerm { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "ascending")] + public bool Ascending { get; set; } + + private List<RoomInfo> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; + + private string Status { get; set; } + + 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; + } + + private async Task Search() { + Results.Clear(); + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + 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; + + 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" }, + { "canonical_alias", "Main alias address" }, + { "joined_members", "Number of members (reversed)" }, + { "joined_local_members", "Number of local members (reversed)" }, + { "version", "Room version" }, + { "creator", "Creator of the room" }, + { "encryption", "End-to-end encryption algorithm" }, + { "federatable", "Is room federated" }, + { "public", "Visibility in room list" }, + { "join_rules", "Join rules" }, + { "guest_access", "Guest access" }, + { "history_visibility", "Visibility of history" }, + { "state_events", "Number of state events" } + }; + + private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { + public List<string>? LocalMembers { get; set; } + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css new file mode 100644
index 0000000..e69de29 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css