From 7182e3f9650e4de9f944d8c4e897fe4a24a3b8bc Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 29 Oct 2025 19:05:58 +0100 Subject: dotnet 10, synapse admin room list improvements --- .../RoomQuery/SynapseRoomQueryFilter.razor | 82 ++-- .../SynapseRoomShutdownWindowContent.razor | 161 +++++++- .../Pages/HSAdmin/Synapse/RoomQuery.razor | 439 +++++++++++++++------ 3 files changed, 525 insertions(+), 157 deletions(-) (limited to 'MatrixUtils.Web/Pages/HSAdmin') diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor index eb168f4..f1c5907 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor @@ -1,46 +1,70 @@ @using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using MatrixUtils.Web.Shared.FilterComponents
String contains - Room ID: - Room name: - Canonical alias: - Creator: - Room version: - Encryption algorithm: - Join rules: - Guest access: - History visibility: + + + + + + + + + + Optional checks - - Is federated: - @if (Filter.CheckFederation) { - - } - - - Is public: - @if (Filter.CheckPublic) { - - } - + + + Ranges - - state events - + + @if (!Filter.StateEvents.Enabled) { + State events + } + else { + + + + state events + + + } - members + + @if (!Filter.JoinedMembers.Enabled) { + Joined members + } + else { + + + + members + + + } - local members + + + @if (!Filter.JoinedLocalMembers.Enabled) { + Joined local members + } + else { + + + local members + + + }
+@* @{ *@ +@* Console.WriteLine($"Rendered SynapseRoomQueryFilter with filter: {Filter.ToJson()}"); *@ +@* } *@ @code { diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor index 48aea86..fc9f8e8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor @@ -1,8 +1,12 @@ +@using System.Text.Json.Serialization +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Homeservers.Extensions.NamedCaches @using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests @using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses -@if (string.IsNullOrWhiteSpace(Context.DeleteId)) { +@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) { Block room:
@@ -34,6 +38,12 @@
Delete ALL local user media: +
+ Follow tombstone (if any): + + @if (!EditorOnly) { + Exec + }
@@ -50,7 +60,19 @@
- Execute + @if (!EditorOnly) { + Execute + } +} +else { +
+        @(_status?.ToJson() ?? "Loading status...")
+    
+
+ [Stop tracking] + if (_status?.Status == SynapseAdminRoomDeleteStatus.Failed) { + [Force delete] + } } @code { @@ -61,14 +83,52 @@ [Parameter] public required AuthenticatedHomeserverSynapse Homeserver { get; set; } + [Parameter] + public bool EditorOnly { get; set; } + private NamedCache TaskMap { get; set; } = null!; + private SynapseAdminRoomDeleteStatus? _status = null; + private bool _isTracking = false; protected override async Task OnInitializedAsync() { + if (EditorOnly) return; TaskMap = new NamedCache(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + var existing = await TaskMap.GetValueAsync(Context.RoomId); + if (existing is not null) { + Context = existing; + } + + if (Context.ExecuteImmediately) + await DeleteRoom(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (EditorOnly) return; + if (!_isTracking) { + if (!string.IsNullOrWhiteSpace(Context.DeleteId)) { + _isTracking = true; + _ = Task.Run(async () => { + do { + _status = await Homeserver.Admin.GetRoomDeleteStatus(Context.DeleteId); + StateHasChanged(); + if (_status.Status == SynapseAdminRoomDeleteStatus.Complete) { + await OnComplete(); + break; + } + + await Task.Delay(1000); + } while (_status.Status != SynapseAdminRoomDeleteStatus.Failed && _status.Status != SynapseAdminRoomDeleteStatus.Complete); + }); + } + } } public class RoomShutdownContext { public required string RoomId { get; set; } + + [JsonIgnore] // do NOT persist - this triggers immediate purging + public bool ExecuteImmediately { get; set; } + public string? DeleteId { get; set; } public ExtraDeleteOptions ExtraOptions { get; set; } = new(); @@ -81,10 +141,11 @@ public SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom? RoomDetails { get; set; } public class ExtraDeleteOptions { - // room options + public bool FollowTombstone { get; set; } + + // media options public bool QuarantineLocalMedia { get; set; } public bool QuarantineRemoteMedia { get; set; } - public bool DeleteRemoteMedia { get; set; } // user options @@ -95,9 +156,14 @@ } public async Task OnComplete() { + if (EditorOnly) return; + Console.WriteLine($"Room shutdown task for {Context.RoomId} completed, removing from map."); await OnCompleteLock.WaitAsync(); try { - await TaskMap.RemoveValueAsync(Context.DeleteId!); + await TaskMap.RemoveValueAsync(Context.RoomId!); + } + catch (Exception e) { + Console.WriteLine("Failed to remove completed room shutdown task from map: " + e); } finally { OnCompleteLock.Release(); @@ -105,6 +171,11 @@ } public async Task DeleteRoom() { + if (EditorOnly) return; + if (Context.ExtraOptions.FollowTombstone) await FollowTombstoneAsync(); + + Console.WriteLine($"Deleting room {Context.RoomId} with options: " + Context.DeleteRequest.ToJson()); + var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, Context.DeleteRequest, false); Context.DeleteId = resp.DeleteId; await TaskMap.SetValueAsync(Context.RoomId, Context); @@ -112,4 +183,84 @@ private static readonly SemaphoreSlim OnCompleteLock = new(1, 1); + private async Task FollowTombstoneAsync() { + if (EditorOnly) return; + var tomb = await TryGetTombstoneAsync(); + var content = tomb?.ContentAs(); + if (content != null && !string.IsNullOrWhiteSpace(content.ReplacementRoom)) { + Console.WriteLine("Tombstone: " + tomb.ToJson()); + if (!content.ReplacementRoom.StartsWith('!')) { + Console.WriteLine($"Invalid replacement room ID in tombstone: {content.ReplacementRoom}, ignoring!"); + } + else { + var oldMembers = await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true); + var isKnownRoom = await Homeserver.Admin.CheckRoomKnownAsync(content.ReplacementRoom); + var targetMembers = isKnownRoom + ? await Homeserver.Admin.GetRoomMembersAsync(Context.RoomId, localOnly: true) + : new() { Members = [] }; + + var members = oldMembers.Members.Except(targetMembers.Members).ToList(); + Console.WriteLine("To migrate: " + members.ToJson()); + foreach (var member in members) { + var success = false; + do { + var sess = member == Homeserver.WhoAmI.UserId ? Homeserver : await Homeserver.Admin.GetHomeserverForUserAsync(member, TimeSpan.FromSeconds(15)); + var oldRoom = sess.GetRoom(Context.RoomId); + var room = sess.GetRoom(content.ReplacementRoom); + try { + var servers = (await oldRoom.GetMembersByHomeserverAsync(joinedOnly: true)) + .Select(x => new KeyValuePair(x.Key, x.Value.Count)) + .OrderByDescending(x => x.Key == "matrix.org" ? 0 : x.Value); // try anything else first, to reduce load on matrix.org + + await room.JoinAsync(servers.Take(10).Select(x => x.Key).ToArray(), reason: "Automatically following tombstone as old room is being purged.", checkIfAlreadyMember: isKnownRoom); + Console.WriteLine($"Migrated {member} from {Context.RoomId} to {content.ReplacementRoom}"); + success = true; + } + catch (Exception e) { + if (e is MatrixException { ErrorCode: "M_FORBIDDEN" }) { + Console.WriteLine($"Cannot migrate {member} to {content.ReplacementRoom}: {(e as MatrixException)!.GetAsJson()}"); + success = true; // give up + continue; + } + + Console.WriteLine($"Failed to invite {member} to {content.ReplacementRoom}: {e}"); + success = false; + await Task.Delay(1000); + } + } while (!success); + } + } + } + } + + private async Task TryGetTombstoneAsync() { + if (EditorOnly) return null; + try { + return (await Homeserver.Admin.GetRoomStateAsync(Context.RoomId, RoomTombstoneEventContent.EventId)).Events.FirstOrDefault(x => x.StateKey == ""); + } + catch { + return null; + } + } + + private async Task ForceDelete() { + if (EditorOnly) return; + Console.WriteLine($"Forcing purge for {Context.RoomId}!"); + await OnCompleteLock.WaitAsync(); + try { + var resp = await Homeserver.Admin.DeleteRoom(Context.RoomId, new() { + ForcePurge = true + }, waitForCompletion: false); + Context.DeleteId = resp.DeleteId; + await TaskMap.SetValueAsync(Context.RoomId, Context); + StateHasChanged(); + } + catch (Exception e) { + Console.WriteLine("Failed to remove completed room shutdown task from map: " + e); + } + finally { + OnCompleteLock.Release(); + } + } + } \ 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 07a3dd2..05899c8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor @@ -1,5 +1,7 @@ @page "/HSAdmin/Synapse/RoomQuery" @using System.Diagnostics.CodeAnalysis +@using System.Text.Json +@using ArcaneLibs.Blazor.Components.Services @using Microsoft.AspNetCore.WebUtilities @using ArcaneLibs.Extensions @using LibMatrix @@ -10,6 +12,7 @@ @using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components @using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery @inject ILogger Logger +@inject BlazorSaveFileService BlazorSaveFileService

Homeserver Administration - Room Query

@@ -21,15 +24,42 @@ }
- -
+ +
+ + + (Execute manually)
+ + + (Execute manually)
+ + + (Execute manually)
+ +
+ + +@if (EnableMultiPurge) { + + [Invert selection] + +
+ Edit purge options + +
+} +else { +
+}
- - Local filtering (slow) - + Local filtering (slow)
- +Search + +@if (EnableMultiPurge) { + Purge selected rooms +}
@if (Results.Count > 0) { @@ -40,6 +70,10 @@
@* *@

+ @if (EnableMultiPurge) { + + + } @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { @room.CanonicalAlias - } @@ -57,6 +91,8 @@

Delete room Resync state + @(room.JoinedLocalMembers == 0 ? "Try to export state" : "Export state") + Force Join

@{ @@ -119,11 +155,23 @@ memberSummary += $": {string.Join(", ", room.LocalMembers)}"; } } - @memberSummary -
- Full result data -
@room.ToJson(ignoreNull: true)
-
+ @memberSummary
+ @if (!string.IsNullOrWhiteSpace(room.TopicEvent?.ContentAs()?.Topic)) { +
+ Room topic +
@(room.TopicEvent?.ContentAs()?.Topic)
+
+ } + @foreach (var ex in room.Exceptions) { + @ex +
+ } + @if (ShowFullResultData) { +
+ Full result data +
@room.ToJson(ignoreNull: true)
+
+ }
} @* *@ @@ -148,10 +196,6 @@ } - - @code { [Parameter] @@ -166,6 +210,18 @@ [SupplyParameterFromQuery(Name = "ascending")] public bool Ascending { get; set; } = true; + [Parameter] + [SupplyParameterFromQuery(Name = "FetchV12PlusCreatorServer")] + public bool FetchV12PlusCreatorServer { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "SummarizeLocalMembers")] + public bool SummarizeLocalMembers { get; set; } = true; + + [Parameter] + [SupplyParameterFromQuery(Name = "FetchTombstones")] + public bool FetchTombstones { get; set; } = true; + private List Results { get; set; } = new(); private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; @@ -178,6 +234,21 @@ private NamedCache TaskMap { get; set; } = null!; + private SynapseRoomShutdownWindowContent.RoomShutdownContext DefaultShutdownContext { get; set; } = new() { + RoomId = "", + DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false } + }; + + public bool ShowFullResultData { + get; + set { + field = value; + StateHasChanged(); + } + } + + public bool EnableMultiPurge { get; set; } + protected override async Task OnInitializedAsync() { var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (hs is not AuthenticatedHomeserverSynapse synapse) { @@ -199,67 +270,95 @@ foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) { switch (key) { case "RoomIdContains": - Filter.RoomIdContains = value[0]!; + Filter.RoomId.Enabled = Filter.RoomId.CheckValueContains = true; + Filter.RoomId.ValueContains = value[0]!; break; case "NameContains": - Filter.NameContains = value[0]!; + Filter.Name.Enabled = Filter.Name.CheckValueContains = true; + Filter.Name.ValueContains = value[0]!; break; case "CanonicalAliasContains": - Filter.CanonicalAliasContains = value[0]!; + Filter.CanonicalAlias.Enabled = Filter.CanonicalAlias.CheckValueContains = true; + Filter.CanonicalAlias.ValueContains = value[0]!; break; case "VersionContains": - Filter.VersionContains = value[0]!; + Filter.Version.Enabled = Filter.Version.CheckValueContains = true; + Filter.Version.ValueContains = value[0]!; break; case "CreatorContains": - Filter.CreatorContains = value[0]!; + Filter.Creator.Enabled = Filter.Creator.CheckValueContains = true; + Filter.Creator.ValueContains = value[0]!; break; case "EncryptionContains": - Filter.EncryptionContains = value[0]!; + Filter.Encryption.Enabled = Filter.Encryption.CheckValueContains = true; + Filter.Encryption.ValueContains = value[0]!; break; case "JoinRulesContains": - Filter.JoinRulesContains = value[0]!; + Filter.JoinRules.Enabled = Filter.JoinRules.CheckValueContains = true; + Filter.JoinRules.ValueContains = value[0]!; break; case "GuestAccessContains": - Filter.GuestAccessContains = value[0]!; + Filter.GuestAccess.Enabled = Filter.GuestAccess.CheckValueContains = true; + Filter.GuestAccess.ValueContains = value[0]!; break; case "HistoryVisibilityContains": - Filter.HistoryVisibilityContains = value[0]!; + Filter.HistoryVisibility.Enabled = Filter.HistoryVisibility.CheckValueContains = true; + Filter.HistoryVisibility.ValueContains = value[0]!; break; case "Federatable": - Filter.Federatable = bool.Parse(value[0]!); - Filter.CheckFederation = true; + Filter.Federation = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; break; case "Public": - Filter.Public = value[0] == "true"; - Filter.CheckPublic = true; + Filter.Public = new() { + Enabled = true, + Value = bool.Parse(value[0]!) + }; break; case "JoinedMembersGreaterThan": - Filter.JoinedMembersGreaterThan = int.Parse(value[0]!); + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedMembers.GreaterThan = int.Parse(value[0]!); break; case "JoinedMembersLessThan": - Filter.JoinedMembersLessThan = int.Parse(value[0]!); + Filter.JoinedMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedMembers.LessThan = int.Parse(value[0]!); break; case "JoinedLocalMembersGreaterThan": - Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!); + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckGreaterThan = true; + Filter.JoinedLocalMembers.GreaterThan = int.Parse(value[0]!); break; case "JoinedLocalMembersLessThan": - Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!); + Filter.JoinedLocalMembers.Enabled = Filter.JoinedLocalMembers.CheckLessThan = true; + Filter.JoinedLocalMembers.LessThan = int.Parse(value[0]!); break; case "StateEventsGreaterThan": - Filter.StateEventsGreaterThan = int.Parse(value[0]!); + Filter.StateEvents.Enabled = Filter.StateEvents.CheckGreaterThan = true; + Filter.StateEvents.GreaterThan = int.Parse(value[0]!); break; case "StateEventsLessThan": - Filter.StateEventsLessThan = int.Parse(value[0]!); + Filter.StateEvents.Enabled = Filter.StateEvents.CheckLessThan = true; + Filter.StateEvents.LessThan = int.Parse(value[0]!); break; case "Execute": execute = true; break; + case "order_by": + case "name_search": + case "ascending": + case "FetchV12PlusCreatorServer": + case "SummarizeLocalMembers": + case "FetchTombstones": + break; default: Console.WriteLine($"Unknown query parameter: {key}"); break; } } + StateHasChanged(); + if (execute) _ = Search(); @@ -268,7 +367,26 @@ private async Task Search() { Results.Clear(); - var searchRooms = Homeserver.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator(); + Console.WriteLine("Starting search... Parameters: " + new { + orderBy = OrderBy!, + dir = Ascending ? "f" : "b", + searchTerm = SearchTerm, + localFilter = Filter, + chunkLimit = 1000, + fetchTombstones = FetchTombstones, + fetchTopics = true, + fetchCreateEvents = true + }.ToJson()); + var searchRooms = Homeserver.Admin.SearchRoomsAsync( + orderBy: OrderBy!, + dir: Ascending ? "f" : "b", + searchTerm: SearchTerm, + localFilter: Filter, + chunkLimit: 1000, + fetchTombstones: FetchTombstones, + fetchTopics: true, + fetchCreateEvents: true + ).GetAsyncEnumerator(); var joinedRooms = await Homeserver.GetJoinedRooms(); while (await searchRooms.MoveNextAsync()) { var room = searchRooms.Current; @@ -288,15 +406,26 @@ StateEvents = room.StateEvents, JoinedMembers = room.JoinedMembers, JoinedLocalMembers = room.JoinedLocalMembers, - OriginHomeserver = joinedRooms.Any(x => x.RoomId == room.RoomId) - ? await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync() - : (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1] - ?? string.Empty + OriginHomeserver = + Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId + ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty) + : string.Empty }; + if (string.IsNullOrWhiteSpace(roomInfo.OriginHomeserver) && FetchV12PlusCreatorServer) { + try { + if (joinedRooms.Any(x => x.RoomId == room.RoomId)) + roomInfo.OriginHomeserver = await Homeserver.GetRoom(room.RoomId).GetOriginHomeserverAsync(); + else roomInfo.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(room.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]; + } + catch (MatrixException e) { + roomInfo.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + } + Results.Add(roomInfo); - if ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) { + if ((Results.Count <= 200 && Results.Count % 10 == 0 && FetchV12PlusCreatorServer) || Results.Count % 1000 == 0) { StateHasChanged(); await Task.Yield(); await Task.Delay(1); @@ -305,97 +434,29 @@ 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); + if (FetchV12PlusCreatorServer) await FetchV12PlusCreatorServersAsync(false); + if (SummarizeLocalMembers) await FetchLocalMemberEventsAsync(false); + // if (CheckTombstone) await FetchTombstoneEventsAsync(false); StateHasChanged(); } - Task DeleteRoom(RoomInfo room) { - DeleteRequests.TryAdd(room.RoomId, new() { RoomId = room.RoomId, RoomDetails = room, DeleteRequest = new() { Block = true, Purge = true, ForcePurge = false } }); + private Task DeleteRoom(RoomInfo room, bool executeWithoutConfirmation = false) { + var dc = JsonSerializer.Deserialize(DefaultShutdownContext.ToJson())!; + dc.RoomId = room.RoomId; + dc.RoomDetails = room; + dc.ExecuteImmediately = executeWithoutConfirmation; + DeleteRequests.TryAdd(room.RoomId, dc); 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 void PurgeSelection() { + foreach (var room in Results.Where(x => x.MultiPurgeSelected)) { + DeleteRoom(room, true); + } + } private readonly Dictionary validOrderBy = new() { { "name", "Room name" }, @@ -415,11 +476,143 @@ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { public List? LocalMembers { get; set; } - public StateEventResponse? TombstoneEvent { get; set; } public required string OriginHomeserver { get; set; } [field: AllowNull, MaybeNull] public string MemberSummary => field ??= $"{JoinedMembers} members, of which {JoinedLocalMembers} are on this server"; + + public List Exceptions { get; set; } = []; + public bool MultiPurgeSelected { get; set; } + } + + private async Task ExportState(RoomInfo room) { + try { + var state = await Homeserver.Admin.GetRoomStateAsync(room.RoomId); + var json = state.ToJson(); + await BlazorSaveFileService.SaveFileAsync($"{room.RoomId.Replace(":", "_")}_state.json", System.Text.Encoding.UTF8.GetBytes(json), "application/json"); + } + catch (Exception e) { + Logger.LogError(e, "Failed to export room state for {RoomId}", room.RoomId); + } + } + + private async Task ForceJoin(RoomInfo room) { + try { + await Homeserver.GetRoom(room.RoomId).JoinAsync([Homeserver.ServerName]); + } + catch (Exception e) { + Logger.LogError(e, "Failed to force-join room {RoomId}", room.RoomId); + // await Homeserver.Admin.room + } + } + + private SemaphoreSlim _concurrencyLimiter = new SemaphoreSlim(16, 16); + + private async Task FetchV12PlusCreatorServersAsync() => await FetchV12PlusCreatorServersAsync(true); + + private async Task FetchV12PlusCreatorServersAsync(bool rerender) { + var joinedRooms = await Homeserver.GetJoinedRooms(); + var tasks = Results + .Where(x => string.IsNullOrWhiteSpace(x.OriginHomeserver)) + .Select(async r => { + if (!string.IsNullOrWhiteSpace(r.Creator) && r.Creator.Contains(':')) { + r.OriginHomeserver = r.Creator.Split(':', 2)[1]; + return; + } + + if (r.CreateEvent != null && !string.IsNullOrWhiteSpace(r.CreateEvent.Sender) && r.CreateEvent.Sender.Contains(':')) { + r.OriginHomeserver = r.CreateEvent.Sender.Split(':', 2)[1]; + return; + } + + await _concurrencyLimiter.WaitAsync(); + try { + if (joinedRooms.Any(x => x.RoomId == r.RoomId)) + r.OriginHomeserver = await Homeserver.GetRoom(r.RoomId).GetOriginHomeserverAsync(); + else r.OriginHomeserver = (await Homeserver.Admin.GetRoomStateAsync(r.RoomId, RoomCreateEventContent.EventId)).Events.FirstOrDefault()?.Sender?.Split(':', 2)[1]; + } + catch (MatrixException e) { + r.Exceptions.Add($"While getting origin homeserver: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to get origin homeserver for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(tasks); + + if (rerender) + StateHasChanged(); + } + + private async Task FetchTombstoneEventsAsync() => await FetchTombstoneEventsAsync(true); + + private async Task FetchTombstoneEventsAsync(bool rerender) { + var getTombstoneTasks = Results + .Where(x => x.TombstoneEvent is null) + .Select(async r => { + await _concurrencyLimiter.WaitAsync(); + try { + 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; + } + } + catch (MatrixException e) { + r.Exceptions.Add($"While checking for tombstone: {e.GetAsObject().ToJson(indent: false, ignoreNull: true)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to check tombstone for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(getTombstoneTasks); + + if (rerender) + StateHasChanged(); + } + + private async Task FetchLocalMemberEventsAsync() => await FetchLocalMemberEventsAsync(true); + + private async Task FetchLocalMemberEventsAsync(bool rerender) { + var getLocalMembersTasks = Results + .Where(x => x.LocalMembers is null && x.JoinedLocalMembers is > 0 and < 100) + .Select(async r => { + await _concurrencyLimiter.WaitAsync(); + try { + var members = (await Homeserver.Admin.GetRoomMembersAsync(r.RoomId)).Members.Where(x => x.EndsWith(":" + Homeserver.ServerName)).ToList(); + r.LocalMembers = members; + } + catch (MatrixException e) { + r.Exceptions.Add($"While fetching local members: {e.GetAsObject().ToJson(ignoreNull: true, indent: false)}"); + } + catch (Exception e) { + Console.WriteLine($"Failed to fetch local members for {r.RoomId}, unhandled exception: " + e); + } + finally { + _concurrencyLimiter.Release(); + } + }); + + await Task.WhenAll(getLocalMembersTasks); + + if (rerender) + StateHasChanged(); + } + + private void MultiPurgeInvertSelection() { + foreach (var room in Results) { + room.MultiPurgeSelected ^= true; + } + + StateHasChanged(); } } -- cgit 1.5.1