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
<div style="margin-left: 8px; margin-bottom: 8px;">
<u style="display: block;">String contains</u>
- <span class="tile tile280">Room ID: <FancyTextBox @bind-Value="@Filter.RoomIdContains"></FancyTextBox></span>
- <span class="tile tile280">Room name: <FancyTextBox @bind-Value="@Filter.NameContains"></FancyTextBox></span>
- <span class="tile tile280">Canonical alias: <FancyTextBox @bind-Value="@Filter.CanonicalAliasContains"></FancyTextBox></span>
- <span class="tile tile280">Creator: <FancyTextBox @bind-Value="@Filter.CreatorContains"></FancyTextBox></span>
- <span class="tile tile280">Room version: <FancyTextBox @bind-Value="@Filter.VersionContains"></FancyTextBox></span>
- <span class="tile tile280">Encryption algorithm: <FancyTextBox @bind-Value="@Filter.EncryptionContains"></FancyTextBox></span>
- <span class="tile tile280">Join rules: <FancyTextBox @bind-Value="@Filter.JoinRulesContains"></FancyTextBox></span>
- <span class="tile tile280">Guest access: <FancyTextBox @bind-Value="@Filter.GuestAccessContains"></FancyTextBox></span>
- <span class="tile tile280">History visibility: <FancyTextBox @bind-Value="@Filter.HistoryVisibilityContains"></FancyTextBox></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.RoomId" Label="Room ID"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Name" Label="Room name"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.CanonicalAlias" Label="Canonical alias"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Creator" Label="Creator"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Version" Label="Room version"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Encryption" Label="Encryption algorithm"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.JoinRules" Label="Join rules"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.GuestAccess" Label="Guest access"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.HistoryVisibility" Label="History visibility"/></span>
+ <span class="tile tile280"><StringFilterComponent Filter="@Filter.Topic" Label="Topic"/></span>
<u style="display: block;">Optional checks</u>
- <span class="tile tile150">
- <InputCheckbox @bind-Value="@Filter.CheckFederation"></InputCheckbox> Is federated:
- @if (Filter.CheckFederation) {
- <InputCheckbox @bind-Value="@Filter.Federatable"></InputCheckbox>
- }
- </span>
- <span class="tile tile150">
- <InputCheckbox @bind-Value="@Filter.CheckPublic"></InputCheckbox> Is public:
- @if (Filter.CheckPublic) {
- <InputCheckbox @bind-Value="@Filter.Public"></InputCheckbox>
- }
- </span>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Federation" Label="Is federated"/></span>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Public" Label="Is public"/></span>
+ <span class="tile tile150"><BooleanFilterComponent Filter="@Filter.Tombstone" Label="Is tombstoned"/></span>
<u style="display: block;">Ranges</u>
<span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"/>
- <span class="range-sep">state events</span>
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"/>
+ <InputCheckbox @bind-Value="@Filter.StateEvents.Enabled"/>
+ @if (!Filter.StateEvents.Enabled) {
+ <span>State events</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.StateEvents.CheckGreaterThan"/>
+ <span> </span>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.GreaterThan"/>
+ <span class="range-sep">state events</span>
+ <InputCheckbox @bind-Value="@Filter.StateEvents.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEvents.LessThan"/>
+ }
</span>
<span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersGreaterThan"></InputNumber><span class="range-sep">members</span><InputNumber
- max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembersLessThan"></InputNumber>
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.Enabled"/>
+ @if (!Filter.JoinedMembers.Enabled) {
+ <span>Joined members</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckGreaterThan"/>
+ <span> </span>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.GreaterThan"/>
+ <span class="range-sep">members</span>
+ <InputCheckbox @bind-Value="@Filter.JoinedMembers.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedMembers.LessThan"/>
+ }
</span>
<span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembersGreaterThan"></InputNumber><span
- class="range-sep">local members</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int"
- @bind-Value="@Filter.JoinedLocalMembersLessThan"></InputNumber>
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.Enabled"/>
+ <span> </span>
+ @if (!Filter.JoinedLocalMembers.Enabled) {
+ <span>Joined local members</span>
+ }
+ else {
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckGreaterThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.GreaterThan"/>
+ <span class="range-sep">local members</span>
+ <InputCheckbox @bind-Value="@Filter.JoinedLocalMembers.CheckLessThan"/>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.JoinedLocalMembers.LessThan"/>
+ }
</span>
</div>
+@* @{ *@
+@* 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) {
<span>Block room: </span>
<InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/>
<br/>
@@ -34,6 +38,12 @@
<br/>
<span>Delete <b>ALL</b> local user media: </span>
<InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox>
+ <br/>
+ <span>Follow tombstone (if any): </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.FollowTombstone"/>
+ @if (!EditorOnly) {
+ <LinkButton InlineText="true" OnClickAsync="@FollowTombstoneAsync">Exec</LinkButton>
+ }
</details>
<details>
@@ -50,7 +60,19 @@
<br/>
</details>
- <LinkButton OnClickAsync="@DeleteRoom">Execute</LinkButton>
+ @if (!EditorOnly) {
+ <LinkButton OnClickAsync="@DeleteRoom">Execute</LinkButton>
+ }
+}
+else {
+ <pre>
+ @(_status?.ToJson() ?? "Loading status...")
+ </pre>
+ <br/>
+ <LinkButton InlineText="true" OnClickAsync="@OnComplete">[Stop tracking]</LinkButton>
+ if (_status?.Status == SynapseAdminRoomDeleteStatus.Failed) {
+ <LinkButton InlineText="true" OnClickAsync="@ForceDelete">[Force delete]</LinkButton>
+ }
}
@code {
@@ -61,14 +83,52 @@
[Parameter]
public required AuthenticatedHomeserverSynapse Homeserver { get; set; }
+ [Parameter]
+ public bool EditorOnly { get; set; }
+
private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!;
+ private SynapseAdminRoomDeleteStatus? _status = null;
+ private bool _isTracking = false;
protected override async Task OnInitializedAsync() {
+ if (EditorOnly) return;
TaskMap = new NamedCache<RoomShutdownContext>(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<RoomTombstoneEventContent>();
+ 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<string, int>(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<StateEventResponse?> 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<RoomQuery> Logger
+@inject BlazorSaveFileService BlazorSaveFileService
<h3>Homeserver Administration - Room Query</h3>
@@ -21,15 +24,42 @@
<option value="@item.Key">@item.Value</option>
}
</select><br/>
-<label>Ascending: </label>
-<InputCheckbox @bind-Value="Ascending"/><br/>
+<InputCheckbox @bind-Value="Ascending"/>
+<label> Ascending</label><br/>
+<InputCheckbox @bind-Value="FetchV12PlusCreatorServer"/>
+<label> Fetch v12+ room creation homeserver</label>
+<LinkButton InlineText="true" OnClickAsync="FetchV12PlusCreatorServersAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="FetchTombstones"/>
+<label> Check for tombstone events</label>
+<LinkButton InlineText="true" OnClickAsync="FetchTombstoneEventsAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="SummarizeLocalMembers"/>
+<label> Fetch local member list for small rooms</label>
+<LinkButton InlineText="true" OnClickAsync="FetchLocalMemberEventsAsync"> (Execute manually)</LinkButton><br/>
+<InputCheckbox @bind-Value="ShowFullResultData"/>
+<label> Show full result data (JSON)</label><br/>
+<InputCheckbox @bind-Value="EnableMultiPurge"/>
+<label> Enable multi-purge mode</label>
+@if (EnableMultiPurge) {
+ <span> </span>
+ <LinkButton InlineText="true" OnClick="@MultiPurgeInvertSelection">[Invert selection]</LinkButton>
+ <span> </span>
+ <details style="display: inline-block;">
+ <summary>Edit purge options</summary>
+ <SynapseRoomShutdownWindowContent Context="@DefaultShutdownContext" Homeserver="Homeserver" EditorOnly="true"/>
+ </details>
+}
+else {
+ <br/>
+}
<details>
- <summary>
- <span>Local filtering (slow)</span>
- </summary>
+ <summary>Local filtering (slow)</summary>
<SynapseRoomQueryFilter Filter="@Filter"/>
</details>
-<button class="btn btn-primary" @onclick="Search">Search</button>
+<LinkButton OnClickAsync="@Search">Search</LinkButton>
+
+@if (EnableMultiPurge) {
+ <LinkButton Color="#FF8800" OnClick="@PurgeSelection">Purge selected rooms</LinkButton>
+}
<br/>
@if (Results.Count > 0) {
@@ -40,6 +70,10 @@
<div class="room-list-item">
@* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
<p>
+ @if (EnableMultiPurge) {
+ <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/>
+ <span> </span>
+ }
@if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) {
<span>@room.CanonicalAlias - </span>
}
@@ -57,6 +91,8 @@
<p>
<LinkButton OnClickAsync="@(() => DeleteRoom(room))">Delete room</LinkButton>
<LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={room.RoomId}&via={room.OriginHomeserver}")">Resync state</LinkButton>
+ <LinkButton OnClickAsync="@(() => ExportState(room))">@(room.JoinedLocalMembers == 0 ? "Try to export state" : "Export state")</LinkButton>
+ <LinkButton OnClickAsync="@(() => ForceJoin(room))">Force Join</LinkButton>
</p>
@{
@@ -119,11 +155,23 @@
memberSummary += $": {string.Join(", ", room.LocalMembers)}";
}
}
- <span>@memberSummary</span>
- <details>
- <summary>Full result data</summary>
- <pre>@room.ToJson(ignoreNull: true)</pre>
- </details>
+ <span>@memberSummary</span><br/>
+ @if (!string.IsNullOrWhiteSpace(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)) {
+ <details>
+ <summary>Room topic</summary>
+ <pre>@(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic)</pre>
+ </details>
+ }
+ @foreach (var ex in room.Exceptions) {
+ <span style="color: red;">@ex</span>
+ <br/>
+ }
+ @if (ShowFullResultData) {
+ <details>
+ <summary>Full result data</summary>
+ <pre>@room.ToJson(ignoreNull: true)</pre>
+ </details>
+ }
</div>
}
@* *@
@@ -148,10 +196,6 @@
</ModalWindow>
}
-<style>
-
-</style>
-
@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<RoomInfo> Results { get; set; } = new();
private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!;
@@ -178,6 +234,21 @@
private NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext> 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<SynapseRoomShutdownWindowContent.RoomShutdownContext>(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<string> 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<string, string> validOrderBy = new() {
{ "name", "Room name" },
@@ -415,11 +476,143 @@
private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom {
public List<string>? 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<string> 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();
}
}
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index 360548d..7199934 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -26,7 +26,7 @@
<InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox>
Create sub-spaces per user
</p>
-
+
<br/>
<LinkButton OnClickAsync="@Disband" Color="#FF0000">Disband</LinkButton>
<LinkButton OnClickAsync="@Execute">Next</LinkButton>
@@ -78,7 +78,7 @@ else {
userRooms.Add(room);
}
- var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncEnumerable();
+ var roomChecks = userRooms.Select(GetFeasibleSpaces).ToAsyncResultEnumerable();
await foreach (var room in roomChecks)
if (room.HasValue)
spaces.TryAdd(room.Value.id, room.Value.roomInfo);
@@ -109,8 +109,8 @@ else {
public async Task<(string id, RoomInfo roomInfo)?> GetFeasibleSpaces(GenericRoom room) {
try {
var ri = new RoomInfo(room);
-
- await foreach(var evt in room.GetFullStateAsync())
+
+ await foreach (var evt in room.GetFullStateAsync())
ri.StateEvents.Add(evt);
var powerLevels = (await ri.GetStateEvent(RoomPowerLevelEventContent.EventId)).TypedContent as RoomPowerLevelEventContent;
@@ -118,7 +118,7 @@ else {
Console.WriteLine($"No permission to send m.space.child in {room.RoomId}...");
return null;
}
-
+
Status = $"Found viable space: {ri.RoomName}";
if (!string.IsNullOrWhiteSpace(SetupData.DmSpaceConfiguration!.DMSpaceId)) {
if (await room.GetStateOrNullAsync<DMSpaceInfo>(DMSpaceInfo.EventId) is { } dsi) {
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
index 25d1629..ed65e94 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -38,7 +38,9 @@ else {
}
@foreach (var (room, usersList) in duplicateDmRooms) {
<ModalWindow Title="Duplicate room found" X="_offset += 30" Y="_offset">
- <p>Found room assigned to multiple users: <RoomListItem RoomInfo="@room"></RoomListItem></p>
+ <p>Found room assigned to multiple users:
+ <RoomListItem RoomInfo="@room"></RoomListItem>
+ </p>
<p>Users:</p>
@foreach (var userProfileResponse in usersList) {
<LinkButton OnClickAsync="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))">
@@ -141,12 +143,12 @@ else {
}
var roomList = new List<RoomInfo>();
- var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable();
await foreach (var result in tasks)
roomList.Add(result);
return (userProfile, roomList);
// StateHasChanged();
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var res in results) {
SetupData.DMRooms.Add(res.userProfile, res.roomList);
// Status = $"Listed {dmRooms.Count} users";
@@ -181,8 +183,8 @@ else {
await roomInfo.FetchAllStateAsync();
roomMembers[roomInfo] = new();
// roomInfo.CreationEventContent = await room.GetCreateEventAsync();
-
- if(roomInfo.RoomName == room.RoomId)
+
+ if (roomInfo.RoomName == room.RoomId)
try {
roomInfo.RoomName = await room.GetNameOrFallbackAsync();
}
@@ -192,7 +194,7 @@ else {
await foreach (var member in membersEnum)
if (member.TypedContent is RoomMemberEventContent memberEvent)
roomMembers[roomInfo].Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey });
-
+
try {
string? roomIcon = (await room.GetAvatarUrlAsync())?.Url;
if (room is not null)
diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
index 2bed22e..686894c 100644
--- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
+++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -115,11 +115,11 @@ else {
// };
// }
// var roomList = new List<RoomInfo>();
- // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncEnumerable();
+ // var tasks = rooms.Select(x => GetRoomInfo(hs.GetRoom(x))).ToAsyncResultEnumerable();
// await foreach (var result in tasks)
// roomList.Add(result);
// return (userProfile, roomList);
- // }).ToAsyncEnumerable();
+ // }).ToAsyncResultEnumerable();
// await foreach (var res in results) {
// dmRooms.Add(new RoomInfo() {
// Room = dmSpaceRoom,
diff --git a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
index e30adf6..17dd554 100644
--- a/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
+++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -19,6 +19,7 @@ else {
else if (checkedRooms.Count > 1) {
<p>Done!</p>
}
+
@foreach (var (state, rooms) in matchingStates) {
<u>@state</u>
<br/>
@@ -71,13 +72,14 @@ else {
_semaphoreSlim.Release();
return; //abort if changed
}
+
matchingStates.Clear();
foreach (var homeserver in hss) {
currentHs = homeserver;
var rooms = await homeserver.GetJoinedRooms();
rooms.RemoveAll(x => checkedRooms.Contains(x.RoomId));
checkedRooms.AddRange(rooms.Select(x => x.RoomId));
- var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncEnumerable();
+ var tasks = rooms.Select(x => GetMembershipAsync(x, mxid)).ToAsyncResultEnumerable();
await foreach (var (room, state) in tasks) {
if (state is null) continue;
if (!matchingStates.ContainsKey(state.Membership))
@@ -97,8 +99,10 @@ else {
return; //abort if changed
}
}
+
StateHasChanged();
}
+
currentHs = null;
StateHasChanged();
_semaphoreSlim.Release();
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index 5876861..92c6ca5 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -381,7 +381,7 @@ else {
.SelectMany(x => x.ActivePolicies.Values)
.ToArray();
- await foreach (var modifiedPolicyInfos in tasks.ToAsyncEnumerable()) {
+ await foreach (var modifiedPolicyInfos in tasks.ToAsyncResultEnumerable()) {
if (modifiedPolicyInfos.Count == 0) continue;
var applySw = Stopwatch.StartNew();
// Console.WriteLine($"Main: got {modifiedPolicyInfos.Count} modified policies from worker, time: {scanSw.Elapsed}");
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
index 6df56ba..52c5f30 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -12,9 +12,9 @@
<h3>
<span>Policy lists </span>
<LinkButton OnClickAsync="@(() => {
- ShowPolicyListCreationWindow = true;
- return Task.CompletedTask;
- })">
+ ShowPolicyListCreationWindow = true;
+ return Task.CompletedTask;
+ })">
<span class="oi oi-plus" aria-hidden="true"> Create</span>
</LinkButton>
</h3>
@@ -137,7 +137,7 @@
if (policies.Count == 0) return null;
Status2 = $"Found legacy list {room.RoomId}...";
return await RoomInfo.FromRoom(room, state, true);
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var room in rooms) {
if (room is not null) {
@@ -145,7 +145,7 @@
StateHasChanged();
}
}
-
+
isLoading = false;
Status = "";
Status2 = "";
diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 8b9735e..7740596 100644
--- a/MatrixUtils.Web/Pages/StreamTest.razor
+++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -89,7 +89,7 @@
// var res2 = Homeserver.ClientHttpClient.GetAsync(url);
// var tasks = Enumerable.Range(1, 128)
// .Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url+$"?width={x*128}&height={x*128}"))
- // .ToAsyncEnumerable();
+ // .ToAsyncResultEnumerable();
await foreach (var result in GetStreamsDelayed(url)) {
Streams.Add(result);
// await Task.Delay(100);
@@ -107,7 +107,7 @@
for (int i = 0; i < 32; i++) {
var tasks = Enumerable.Range(1, 4)
.Select(x => Homeserver.ClientHttpClient.GetStreamAsync(url + $"?width={x * 128}&height={x * 128}&r={Random.Shared.Next(100000)}"))
- .ToAsyncEnumerable();
+ .ToAsyncResultEnumerable();
await foreach (var result in tasks) {
yield return result;
}
diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index b0f7dbf..067036e 100644
--- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -53,8 +53,8 @@
var oldRoom = hs.GetRoom(roomId);
var newRoom = hs.GetRoom(newRoomId);
var members = await oldRoom.GetMembersListAsync();
- var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncEnumerable();
- // var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable();
+ var tasks = members.Select(x => ExecuteInvite(hs, newRoom, x.StateKey)).ToAsyncResultEnumerable();
+ // var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
diff --git a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
index acad827..8ba160a 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
@@ -41,13 +41,13 @@
var ss = new SemaphoreSlim(32, 32);
var rooms = await hs.GetJoinedRooms();
RoomCount = rooms.Count;
- var fetchTasks = rooms.Select(roomId => workerService.TaskPool.Invoke(() => InternalGetMembersByHomeserver(hs.WellKnownUris.Client, hs.AccessToken, roomId.RoomId))).ToList().ToAsyncEnumerable();
+ var fetchTasks = rooms.Select(roomId => workerService.TaskPool.Invoke(() => InternalGetMembersByHomeserver(hs.WellKnownUris.Client, hs.AccessToken, roomId.RoomId))).ToList().ToAsyncResultEnumerable();
// var fetchTasks = rooms.Select(async x => {
- // await ss.WaitAsync();
- // var res = await x.GetMembersByHomeserverAsync();
- // ss.Release();
- // return res;
- // }).ToAsyncEnumerable();
+ // await ss.WaitAsync();
+ // var res = await x.GetMembersByHomeserverAsync();
+ // ss.Release();
+ // return res;
+ // }).ToAsyncResultEnumerable();
await foreach (var result in fetchTasks) {
foreach (var (resHomeserver, resMembers) in result) {
if (!homeservers.TryAdd(resHomeserver, resMembers)) {
diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index f8d1d31..bfd5fd3 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -83,7 +83,7 @@ else
foreach (var message in response.Chunk) {
if (!message.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue;
//OriginServerTs to datetime
- var dt = DateTimeOffset.FromUnixTimeMilliseconds((long)message.OriginServerTs!.Value).DateTime;
+ var dt = DateTimeOffset.FromUnixTimeMilliseconds(message.OriginServerTs!.Value).DateTime;
var date = new DateOnly(dt.Year, dt.Month, dt.Day);
if (!RoomData[roomName].ContainsKey(date.Year)) {
RoomData[roomName][date.Year] = new();
@@ -100,9 +100,8 @@ else
else rgb.B++;
RoomData[roomName][date.Year][date] = rgb;
}
-
-
}
+
var max = RoomData.SelectMany(x => x.Value.Values).Aggregate(new ActivityGraph.RGB(), (current, next) => new() {
R = Math.Max(current.R, next.Average(x => x.Value.R)),
G = Math.Max(current.G, next.Average(x => x.Value.G)),
diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index fcdb3d0..dc5333b 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
@@ -11,7 +11,8 @@
<p>Users: </p>
<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
<br/>
-<InputText @bind-Value="@ImportFromRoomId"></InputText><LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton>
+<InputText @bind-Value="@ImportFromRoomId"></InputText>
+<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -44,9 +45,7 @@
@foreach (var (userId, events) in matches) {
<p>
<span>@userId.PadRight(col1Width)</span>
- @foreach (var @event in events) {
-
-}
+ @foreach (var @event in events) { }
</p>
}
</pre>
@@ -97,7 +96,7 @@
rooms = new ObservableCollection<GenericRoom>(distinctRooms);
rooms.CollectionChanged += (sender, args) => StateHasChanged();
- var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync())).ToAsyncEnumerable();
+ var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync())).ToAsyncResultEnumerable();
await foreach (var (room, state) in stateTasks) {
roomMembers.Add(room, state);
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
index 1fd0ff6..5ad9de4 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -161,7 +161,7 @@
}
return null;
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index f39a2eb..2261cb8 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -26,10 +26,10 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) {
+ @foreach (var (userId, events) in matches.OrderBy(x => x.Key)) {
<h4>@userId</h4>
<table>
- @foreach (var match in events.OrderBy(x=>x.RoomName)) {
+ @foreach (var match in events.OrderBy(x => x.RoomName)) {
<tr>
<td>@match.RoomName (<span>@match.Room.RoomId</span>)</td>
<td>
@@ -161,7 +161,7 @@
}
return null;
- }).ToAsyncEnumerable();
+ }).ToAsyncResultEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
@@ -186,10 +186,9 @@
_ => $"Unknown membership {membership.Membership}, sent at {time} by {state.Sender} for {membership.Reason}"
};
}
-
+
private async Task ExportJson() {
var json = matches.ToJson();
-
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index b893970..acc86a2 100644
--- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -42,7 +42,7 @@
private async Task Execute() {
foreach (var hs in hss) {
var rooms = await hs.GetJoinedRooms();
- var tasks = rooms.Select(x=>ApplyPowerlevelsInRoom(hs, x)).ToAsyncEnumerable();
+ var tasks = rooms.Select(x => ApplyPowerlevelsInRoom(hs, x)).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
@@ -62,12 +62,11 @@
log.Add("I am same PL in " + room.RoomId);
continue;
}
-
+
pls.SetUserPowerLevel(ahs.WhoAmI.UserId, pls.GetUserPowerLevel(hs.WhoAmI.UserId));
await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, pls);
log.Add($"Updated powerlevel of {room.RoomId} to {pls.GetUserPowerLevel(ahs.WhoAmI.UserId)}");
}
-
}
catch (MatrixException e) {
return $"Failed to update PLs in {room.RoomId}: {e.Message}";
@@ -75,6 +74,7 @@
catch (Exception e) {
return $"Failed to update PLs in {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
diff --git a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
index 748f2fb..ee17f1d 100644
--- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -42,23 +42,24 @@
}
private async Task Execute() {
- // foreach (var hs in hss) {
- // var rooms = await hs.GetJoinedRooms();
- var tasks = hss.Select(ExecuteInvite).ToAsyncEnumerable();
+ // foreach (var hs in hss) {
+ // var rooms = await hs.GetJoinedRooms();
+ var tasks = hss.Select(ExecuteInvite).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
StateHasChanged();
}
}
- tasks = hss.Select(ExecuteJoin).ToAsyncEnumerable();
+
+ tasks = hss.Select(ExecuteJoin).ToAsyncResultEnumerable();
await foreach (var a in tasks) {
if (!string.IsNullOrWhiteSpace(a)) {
log.Add(a);
StateHasChanged();
}
}
- // }
+ // }
}
private async Task<string> ExecuteInvite(AuthenticatedHomeserverGeneric hs) {
@@ -69,6 +70,7 @@
if (joinRule.JoinRule == RoomJoinRulesEventContent.JoinRules.Public) return "Room is public, no invite needed";
}
catch { }
+
var pls = await room.GetPowerLevelsAsync();
if (pls.GetUserPowerLevel(hs.WhoAmI.UserId) < pls.Invite) return "I do not have permission to send invite in " + room.RoomId;
await room.InviteUsersAsync(hss.Select(x => x.WhoAmI.UserId).ToList());
@@ -80,6 +82,7 @@
catch (Exception e) {
return $"Failed to invite in {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
@@ -92,6 +95,7 @@
if (mse?.Membership == "join") return $"User {hs.WhoAmI.UserId} already in room";
}
catch { }
+
await room.JoinAsync();
}
catch (MatrixException e) {
@@ -100,6 +104,7 @@
catch (Exception e) {
return $"Failed to join {hs.WhoAmI.UserId} to {room.RoomId}: {e.Message}";
}
+
StateHasChanged();
return "";
}
diff --git a/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
index 984130f..0e838c7 100644
--- a/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
@@ -70,7 +70,7 @@
return room.RoomId;
})
.ToList();
- await foreach (var roomScanResult in roomScanTasks.ToAsyncEnumerable()) {
+ await foreach (var roomScanResult in roomScanTasks.ToAsyncResultEnumerable()) {
_observableProgressState.Label.Value = roomScanResult;
}
}
diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index 290b92a..2b7b6cf 100644
--- a/MatrixUtils.Web/Pages/User/Profile.razor
+++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -12,9 +12,13 @@
<div>
<MxcAvatar Homeserver="@Homeserver" MxcUri="@NewProfile.AvatarUrl" Circular="true" Size="96"/>
<div style="display: inline-block; vertical-align: middle;">
- <span>Display name: </span><FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox><br/>
- <span>Avatar URL: </span><FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
- <InputFile OnChange="@AvatarChanged"></InputFile><br/>
+ <span>Display name: </span>
+ <FancyTextBox @bind-Value="@NewProfile.DisplayName"></FancyTextBox>
+ <br/>
+ <span>Avatar URL: </span>
+ <FancyTextBox @bind-Value="@NewProfile.AvatarUrl"></FancyTextBox>
+ <InputFile OnChange="@AvatarChanged"></InputFile>
+ <br/>
<LinkButton OnClickAsync="@(() => UpdateProfile())">Update profile</LinkButton>
<LinkButton OnClickAsync="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton>
</div>
@@ -26,7 +30,9 @@
<br/>
@* <details> *@
- <h4>Room profiles<hr></h4>
+ <h4>Room profiles
+ <hr>
+ </h4>
@foreach (var room in Rooms) {
<details class="details-compact">
@@ -41,9 +47,15 @@
@* <img src="@Homeserver.ResolveMediaUri(room.OwnMembership.AvatarUrl)" style="width: 96px; height: 96px; border-radius: 50%; object-fit: cover;"/> *@
<MxcAvatar Homeserver="@Homeserver" MxcUri="@room.OwnMembership.AvatarUrl" Circular="true" Size="96"/>
<div style="display: inline-block; vertical-align: middle;">
- <span>Display name: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.DisplayName"></FancyTextBox><br/>
- <span>Avatar URL: </span><FancyTextBox BackgroundColor="@(room.OwnMembership.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")" @bind-Value="@room.OwnMembership.AvatarUrl"></FancyTextBox>
- <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile><br/>
+ <span>Display name: </span>
+ <FancyTextBox BackgroundColor="@(room.OwnMembership.DisplayName == OldProfile.DisplayName ? "" : "#ffff0033")"
+ @bind-Value="@room.OwnMembership.DisplayName"></FancyTextBox>
+ <br/>
+ <span>Avatar URL: </span>
+ <FancyTextBox BackgroundColor="@(room.OwnMembership.AvatarUrl == OldProfile.AvatarUrl ? "" : "#ffff0033")"
+ @bind-Value="@room.OwnMembership.AvatarUrl"></FancyTextBox>
+ <InputFile OnChange="@(ifcea => RoomAvatarChanged(ifcea, room.Room.RoomId))"></InputFile>
+ <br/>
<LinkButton OnClickAsync="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton>
</div>
<br/>
@@ -117,7 +129,7 @@
});
roomInfoTasks.Add(task);
}
-
+
await Task.WhenAll(roomInfoTasks);
StateHasChanged();
@@ -126,7 +138,7 @@
// var roomNameTasks = RoomProfiles.Keys.Select(x => Homeserver.GetRoom(x)).Select(async x => {
// var name = await x.GetNameOrFallbackAsync();
// return new KeyValuePair<string, string?>(x.RoomId, name);
- // }).ToAsyncEnumerable();
+ // }).ToAsyncResultEnumerable();
// await foreach (var (roomId, roomName) in roomNameTasks) {
// Status = $"Got room name for {roomId}: {roomName}";
|