@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 Logger

Homeserver Administration - Room Query




Local filtering (slow)

@if (Results.Count > 0) {

Found @Results.Count rooms

@*
*@ @* TSV data (copy/paste) *@ @*
 *@
    @*              *@
    @*                 @foreach (var res in Results) { *@
    @*                      *@
    @*                          *@
    @*                          *@
    @*                          *@
    @*                          *@
    @*                      *@
    @*                 } *@
    @*             
@res.RoomId@("\t")@res.CanonicalAlias@("\t")@res.Creator@("\t")@res.Name
*@ @*
*@ @*
*@ } @foreach (var room in Results) {
@* *@

@if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { @room.CanonicalAlias - } @room.RoomId @if (!string.IsNullOrWhiteSpace(room.Name)) { (@room.Name) }
@if (!string.IsNullOrWhiteSpace(room.Creator)) { Created by @room.Creator
}

Delete room Resync state

@{ List 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(); } } @string.Join(", ", flags) @if (room.JoinedLocalMembers == 0 && flags.Count > 0) { at the time of leaving }
@room.StateEvents state events, room version @(room.Version ?? "1")
@if (room.TombstoneEvent is not null) { var tombstoneContent = room.TombstoneEvent.ContentAs()!; Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body
} @{ var memberSummary = room.MemberSummary; if (room.LocalMembers is not null) { memberSummary += $": {string.Join(", ", room.LocalMembers)}"; } } @memberSummary
Full result data
@room.ToJson(ignoreNull: true)
} @* *@ @* @if (DeleteRequest.HasValue) { *@ @* *@ @* *@ @* *@ @* } *@ @* @foreach (var (roomId, status) in DeleteStatuses) { *@ @* *@ @*
@status.ToJson()
*@ @*
*@ @* } *@ @foreach (var (roomId, deleteRequest) in DeleteRequests) { } @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 Results { get; set; } = new(); private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; private SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); private Dictionary DeleteRequests { get; set; } = []; // private Dictionary DeleteStatuses { get; set; } = new(); private NamedCache 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(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 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 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? LocalMembers { get; set; } public StateEventResponse? TombstoneEvent { get; set; } [field: AllowNull, MaybeNull] public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server"; } }