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 ++++++++++++++++++++- 2 files changed, 209 insertions(+), 34 deletions(-) (limited to 'MatrixUtils.Web/Pages/HSAdmin/Synapse/Components') 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 -- cgit 1.5.1