about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-05-27 04:08:25 +0200
committerRory& <root@rory.gay>2025-06-06 20:20:41 +0200
commita6946e0f82e65c7a9e090909e8618066a2f073bf (patch)
treec66bf894eeed930ef2620a8cfc4adde6cb7d754d
parentClean up later... (diff)
downloadMatrixUtils-a6946e0f82e65c7a9e090909e8618066a2f073bf.tar.xz
Fix room list
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor433
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index.razor2
3 files changed, 441 insertions, 1 deletions
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
new file mode 100644

index 0000000..3e38ee2 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
@@ -0,0 +1,433 @@ +@page "/HSAdmin/Synapse/RoomQuery" +@using System.Diagnostics.CodeAnalysis +@using Microsoft.AspNetCore.WebUtilities +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery +@inject ILogger<RoomQuery> Logger + +<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> + <SynapseRoomQueryFilter Filter="@Filter"/> +</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 room in Results) { + <div class="room-list-item"> + @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ + <p> + @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { + <span>@room.CanonicalAlias - </span> + } + <span>@room.RoomId</span> + @if (!string.IsNullOrWhiteSpace(room.Name)) { + <span> (@room.Name)</span> + } + <br/> + + @if (!string.IsNullOrWhiteSpace(room.Creator)) { + <span>Created by @room.Creator</span> + <br/> + } + </p> + <p> + <LinkButton OnClick="@(() => DeleteRoom(room))">Delete room</LinkButton> + <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton> + </p> + + @{ + List<string?> flags = []; + if (true || room.JoinedLocalMembers > 0) { + flags.Add(room.JoinRules switch { + "public" => "Public", + "invite" => "Invite only", + "knock" => "Knock", + "restricted" => "Restricted", + "knock_restricted" => "Knock + restricted", + // TODO: default? + null => null, + "" => null, + _ => "unknown join rule: " + room.JoinRules + }); + + if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted"); + if (!room.Federatable) flags.Add("unfederated"); + + flags.Add(room.HistoryVisibility switch { + "world_readable" => "world readable history", + "shared" => "shared history", + "invited" => "history since invite", + "joined" => "history since join", + // TODO: default? + null => null, + "" => null, + _ => "unknown history setting: " + room.HistoryVisibility + }); + + flags.Add(room.GuestAccess switch { + "can_join" => "guests allowed", + "forbidden" => null, + // TODO: default? + null => null, + "" => null, + _ => "unknown guest access: " + room.GuestAccess, + }); + + flags = flags.Where(x => x != null).ToList(); + } + } + <span>@string.Join(", ", flags)</span> + @if (room.JoinedLocalMembers == 0 && flags.Count > 0) { + <span> at the time of leaving</span> + } + <br/> + + <span>@room.StateEvents state events, room version @(room.Version ?? "1")</span><br/> + @if (room.TombstoneEvent is not null) { + var tombstoneContent = room.TombstoneEvent.ContentAs<RoomTombstoneEventContent>()!; + <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span><br/> + } + + @{ + var memberSummary = room.MemberSummary; + if (room.LocalMembers is not null) { + memberSummary += $": {string.Join(", ", room.LocalMembers)}"; + } + } + <span>@memberSummary</span> + <details> + <summary>Full result data</summary> + <pre>@room.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) { + <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => { + DeleteRequests.Remove(roomId); + StateHasChanged(); + })"> + <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> + </ModalWindow> +} + +<style> + +</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; } = true; + + private List<RoomInfo> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; + + private SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); + + private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = []; + + // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new(); + + private NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext> TaskMap { get; set; } = null!; + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is not AuthenticatedHomeserverSynapse synapse) { + NavigationManager.NavigateTo("/"); + return; + } + + Homeserver = synapse; + TaskMap = new NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + DeleteRequests = (await TaskMap.ReadCacheMapAsync()).Where(x => x.Value.DeleteId is not null).ToDictionary(); + StateHasChanged(); + } + + protected override Task OnParametersSetAsync() { + 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 searchRooms = Homeserver.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); + while (await searchRooms.MoveNextAsync()) { + var room = searchRooms.Current; + + 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 ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) { + StateHasChanged(); + await Task.Yield(); + await Task.Delay(1); + } + } + + StateHasChanged(); + + var getLocalMembersTasks = Results + .Where(x => x.JoinedLocalMembers is > 0 and < 100) + .Select(async r => { + var members = (await Homeserver.Admin.GetRoomMembersAsync(r.RoomId)).Members.Where(x => x.EndsWith(":" + Homeserver.ServerName)).ToList(); + r.LocalMembers = members; + } + ); + await Task.WhenAll(getLocalMembersTasks); + + var getTombstoneTasks = Results + .Select(async r => { + var state = await Homeserver.Admin.GetRoomStateAsync(r.RoomId, type: "m.room.tombstone"); + var tombstone = state.Events.FirstOrDefault(x => x is { StateKey: "", Type: "m.room.tombstone" }); + if (tombstone is { } tombstoneEvent) { + r.TombstoneEvent = tombstoneEvent; + } + }); + await Task.WhenAll(getTombstoneTasks); + + StateHasChanged(); + } + + Task DeleteRoom(RoomInfo room) { + DeleteRequests.TryAdd(room.RoomId, new() { RoomId = room.RoomId, RoomDetails = room, DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false } }); + StateHasChanged(); + + return Task.CompletedTask; + } + + // + // 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; } + public StateEventResponse? TombstoneEvent { get; set; } + + [field: AllowNull, MaybeNull] + public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server"; + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css new file mode 100644
index 0000000..62941e5 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css
@@ -0,0 +1,7 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor
index 611bf56..115c903 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -210,7 +210,7 @@ Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!"; if (sync.Rooms?.Join != null) foreach (var joinedRoom in sync.Rooms.Join) - if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) { + if (joinedRoom.Value.StateAfter?.Events?.Count > 0) { joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI.UserId); // We can't trust servers to give us what we ask for, and this ruins performance // Thanks, Conduit.