@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;
}
}
}