@page "/HSAdmin/Synapse/ResyncState" @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes.Spec.State.RoomInfo @using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests

Resync room state with other server


@if (!Executing) {

WARNING: Will likely not work on invite-only/knock rooms! May also mess with history visibility!

If the room is using mjolnir/draupnir, it's probably recommended to set the "via" to the server it's hosted on.

Room ID:
Via:
Execute } @if (Executing) {

Execution in progress. DO NOT CLOSE THIS PAGE!

} @* stage 1 *@ @if (Stage >= 1) { @if (Members is null) {

Loading members...

} else {

Got @Members.Count local members

} } @* stage 2 *@ @if (Stage == 2) {

Purging room, please wait...

@DeleteStatus.ToJson(ignoreNull: true)
} @* stage 3 *@ @if (Stage == 3) {

Rejoining room, please wait...

Members left to restore:

string members = ""; foreach (var member in Members) { members += $"{member.StateKey} ({member.ContentAs()?.ToJson(indent: false, ignoreNull: true)})\n"; }
        @members
    
} @if (Stage == 4) {

Execution finished. You may now close the page :)

} @if (Error is not null) {

Error: @Error.Message

        @Error.ToString()
    
} @code { [Parameter] [SupplyParameterFromQuery] public string? RoomId { get; set; } [Parameter] [SupplyParameterFromQuery(Name = "via")] public string? Via { get; set; } private AuthenticatedHomeserverSynapse? Homeserver { get; set; } // Execution flow private int Stage { get; set; } private bool Executing { get; set; } private Exception? Error { get; set; } // Stage 1 private List? Members { get; set; } // Stage 2 private SynapseAdminRoomDeleteStatus? DeleteStatus { get; set; } protected override async Task OnInitializedAsync() { if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverSynapse hs) return; Homeserver = hs; StateHasChanged(); } private Task Execute() => Execute(0); private async Task Execute(int startStage) { if (string.IsNullOrWhiteSpace(RoomId)) return; if (string.IsNullOrWhiteSpace(Via)) return; Executing = true; StateHasChanged(); await ExecuteStages(startStage); StateHasChanged(); } private async Task ExecuteStages(int startStage) { if (startStage <= 1) if (!await TryGetRoomMembers()) return; if (startStage <= 2) if (!await TryPurgeRoom()) return; if (startStage <= 3) if (!await TryRestoreRoom()) return; Stage = 4; Executing = false; StateHasChanged(); } private async Task TryGetRoomMembers() { Stage = 1; try { Members = (await Homeserver.Admin.GetRoomStateAsync(RoomId, type: RoomMemberEventContent.EventId)) .Events.Where(m => (m.StateKey?.EndsWith(':' + Homeserver.ServerName) ?? false) && m.ContentAs()!.Membership == "join") .ToList(); Console.WriteLine(Members.ToJson(ignoreNull: true)); StateHasChanged(); return true; } catch (Exception e) { Error = e; return Executing = false; } } private async Task TryPurgeRoom() { Stage = 2; try { var resp = await Homeserver.Admin.DeleteRoom(RoomId, new SynapseAdminRoomDeleteRequest { Block = true, Purge = true, // ForcePurge = true // This causes synapse to early return and not actually purge stuff... }, waitForCompletion: false); while (true) { // we dont want API failure to break this step try { DeleteStatus = await Homeserver.Admin.GetRoomDeleteStatus(resp.DeleteId); StateHasChanged(); if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Complete) { return true; } if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Failed) { Error = new Exception("Failed to delete room: " + DeleteStatus.ToJson()); return Executing = false; } await Task.Delay(1000); } catch { } } StateHasChanged(); return true; } catch (Exception e) { Error = e; return Executing = false; } } private async Task TryRestoreRoom() { Stage = 3; try { await Homeserver.Admin.BlockRoom(RoomId, block: false); Members = Random.Shared.GetItems(Members.ToArray(), Members.Count).ToList(); StateHasChanged(); foreach (var member in Members) { while (true) { try { var hs = member.StateKey == Homeserver.WhoAmI.UserId ? Homeserver : await Homeserver.Admin.GetHomeserverForUserAsync(member.StateKey!, TimeSpan.FromMinutes(120)); await hs.GetRoom(RoomId).JoinAsync([Via], reason: "Reconciling state with " + Via, false); await hs.GetRoom(RoomId).SendStateEventAsync(RoomMemberEventContent.EventId, member.StateKey, member.RawContent); Members = Members.Skip(1).ToList(); StateHasChanged(); break; } catch (Exception e) { Error = new Exception($"{DateTime.Now:u} Failed to join room: {member.StateKey}, retrying\n", e); } } } return true; } catch (Exception e) { Error = e; return Executing = false; } } }