diff --git a/MatrixUtils.Web/MatrixUtils.Web.csproj b/MatrixUtils.Web/MatrixUtils.Web.csproj
index acb4054..aa9f37f 100644
--- a/MatrixUtils.Web/MatrixUtils.Web.csproj
+++ b/MatrixUtils.Web/MatrixUtils.Web.csproj
@@ -41,6 +41,7 @@
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.1" />
<PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.5.39" />
</ItemGroup>
diff --git a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index 36fa247..9dc9556 100644
--- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
+++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -1,6 +1,7 @@
@page "/Dev/WellKnownRes"
@using ArcaneLibs.Extensions
-@using LibMatrix.Services.WellKnownResolvers
+@using LibMatrix.Services.WellKnownResolver
+@using LibMatrix.Services.WellKnownResolver.WellKnownResolvers
@inject HomeserverResolverService legacyResolver
@inject WellKnownResolverService rewriteResolver
@inject ClientWellKnownResolver rewriteClientResolver
@@ -9,18 +10,39 @@
<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span>
+<span>Stats:</span><br/>
+<span>Server count: @entries.Count</span><br/>
+<span>Client server resolution rate (N/O/T): @entries.Count(x => x.HasClientWellKnown)/@entries.Count(x => !string.IsNullOrWhiteSpace(x.LegacyResolutionResult?.Client))/@entries.Count</span>
+<br/>
+<span>Server server resolution rate (N/T): @entries.Count(x => x.HasServerWellKnown)/@entries.Count</span><br/>
+<span>Support resolution rate (N/T): @entries.Count(x => x.HasSupportWellKnown)/@entries.Count</span><br/>
<table class="table-bordered">
<thead>
<td>Homeserver</td>
- <td>Legacy client URL</td>
- <td>Rewrite client URL</td>
+ <td>Client API</td>
+ <td>Server API</td>
+ <td>Has support record</td>
</thead>
@foreach (var entry in entries) {
- <tr style="background-color: @GetColor(entry)">
+ <tr>
<td>@entry.Homeserver</td>
- <td>@(entry.LegacyClientUrl ?? "null")</td>
- <td>@(entry.RewriteClientUrl ?? "null")</td>
+ <td style="background-color: @GetClientColor(entry)">
+ <span>L: @entry.LegacyResolutionResult?.Client</span><br/>
+ <span>R: @entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver.BaseUrl</span>
+ </td>
+ <td style="background-color: @GetServerColor(entry)">
+ <span>L: @entry.LegacyResolutionResult?.Server</span><br/>
+ <span>R: @entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver</span>
+ </td>
+ <td>@(entry.HasSupportWellKnown ? "Y" : "X")</td>
+ </tr>
+ <tr>
+ <td colspan="6">
+ <details>
+ <pre>@(entry.WellKnownResolutionResult?.ToJson() ?? "null")</pre>
+ </details>
+ </td>
</tr>
}
</table>
@@ -44,8 +66,12 @@
private class TableEntry {
public required string Homeserver { get; set; }
- public string? LegacyClientUrl { get; set; }
- public string? RewriteClientUrl { get; set; }
+ public HomeserverResolverService.WellKnownUris? LegacyResolutionResult { get; set; }
+ public WellKnownResolverService.WellKnownRecords? WellKnownResolutionResult { get; set; }
+
+ public bool HasClientWellKnown => WellKnownResolutionResult?.ClientWellKnown is { Content.Homeserver.BaseUrl: { Length: > 0 } };
+ public bool HasServerWellKnown => WellKnownResolutionResult?.ServerWellKnown is { Content.Homeserver.Length: > 0 };
+ public bool HasSupportWellKnown => WellKnownResolutionResult?.SupportWellKnown?.Content is not null and not { SupportPage: null, Contacts: null or { Count: 0 } };
}
private async Task Execute() {
@@ -55,7 +81,7 @@
foreach (var homeserver in homeservers) {
var e = new TableEntry() { Homeserver = homeserver };
_ = TryResolveLegacy(e);
- _ = TryResolveRewrite(e);
+ _ = TryFullResolveRewrite(e);
entries.Add(e);
}
@@ -65,23 +91,32 @@
private async Task TryResolveLegacy(TableEntry entry) {
try {
- entry.LegacyClientUrl = (await legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false)).Client;
+ var cTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableServer: false);
+ var sTask = legacyResolver.ResolveHomeserverFromWellKnown(entry.Homeserver, enableClient: false);
+ entry.LegacyResolutionResult = (await cTask);
+ entry.LegacyResolutionResult.Server = (await sTask).Server;
StateHasChanged();
}
catch { }
}
- private async Task TryResolveRewrite(TableEntry entry) {
+ private async Task TryFullResolveRewrite(TableEntry entry) {
try {
- entry.RewriteClientUrl = (await rewriteClientResolver.TryResolveClientWellKnown(entry.Homeserver)).WellKnown.Homeserver.BaseUrl;
+ entry.WellKnownResolutionResult = await rewriteResolver.TryResolveWellKnownRecords(entry.Homeserver);
StateHasChanged();
}
catch { }
}
-
- private string GetColor(TableEntry entry) {
- if (entry.LegacyClientUrl == entry.RewriteClientUrl && entry.RewriteClientUrl == null) return "#333333";
- if (entry.LegacyClientUrl == entry.RewriteClientUrl?.TrimEnd('/')) return "#008800";
+
+ private string GetClientColor(TableEntry entry) {
+ if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl && entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl == null) return "#333333";
+ if (entry.LegacyResolutionResult?.Client == entry.WellKnownResolutionResult?.ClientWellKnown?.Content?.Homeserver?.BaseUrl?.TrimEnd('/')) return "#008800";
+ return "#ff0000";
+ }
+
+ private string GetServerColor(TableEntry entry) {
+ if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver && entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver == null) return "#333333";
+ if (entry.LegacyResolutionResult?.Server == entry.WellKnownResolutionResult?.ServerWellKnown?.Content?.Homeserver.TrimEnd('/')) return "#008800";
return "#ff0000";
}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index 09cc2cd..6ccdce4 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -10,7 +10,8 @@ else {
@if (Homeserver is AuthenticatedHomeserverSynapse) {
<h4>Synapse tools</h4>
<hr/>
- <a href="/HSAdmin/RoomQuery">Query rooms</a>
+ <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/>
+ <a href="/HSAdmin/Synapse/BlockMedia">Block media</a>
}
else if (Homeserver is AuthenticatedHomeserverHSE) {
<h4>Rory&::LibMatrix.HomeserverEmulator tools</h4>
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
new file mode 100644
index 0000000..02bf88f
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BackgroundJobs.razor
@@ -0,0 +1,29 @@
+@page "/HSAdmin/Synapse/BackgroundJobs"
+@using ArcaneLibs.Extensions
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+
+<h3>Homeserver Administration - Background jobs</h3>
+<pre>@BackgroundJobStatus?.ToJson(ignoreNull: true)</pre>
+
+@code {
+ private AuthenticatedHomeserverSynapse? Homeserver { get; set; }
+ private SynapseAdminBackgroundUpdateStatusResponse? BackgroundJobStatus { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await RmuStorage.GetCurrentSessionOrNavigate() as AuthenticatedHomeserverSynapse;
+ if (hs is null) return;
+ Homeserver = hs;
+
+ while (true) {
+ try {
+ BackgroundJobStatus = await hs.Admin.GetBackgroundUpdatesStatusAsync();
+ StateHasChanged();
+ await Task.Delay(1000);
+ }
+ catch (Exception ex) {
+ Console.WriteLine(ex);
+ }
+ }
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
new file mode 100644
index 0000000..6de4e4d
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -0,0 +1,191 @@
+@page "/HSAdmin/Synapse/BlockMedia"
+@using System.Text.Json
+@using ArcaneLibs.Extensions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec
+
+<h3>Homeserver Administration - Block media</h3>
+@if (Homeserver is not null) {
+ <label>Event URL: </label>
+ <FancyTextBox @bind-Value="EventUrl"/>
+ <br/>
+ <label>Event JSON: </label>
+ <details>
+ <summary>@(string.IsNullOrEmpty(EventJson) ? "" : "{ ... }")</summary>
+ <FancyTextBox Multiline="true" @bind-Value="EventJson"/>
+ </details>
+ <br/>
+ <label>MXC URI: </label>
+ <FancyTextBox @bind-Value="MxcUrl"/>
+ <br/>
+ <label>Room ID: </label>
+ <FancyTextBox @bind-Value="RoomId"/>
+ <br/>
+ <pre>@MxcUri?.ToJson(ignoreNull: true)</pre>
+
+ @if (Event is not null) {
+ <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton>
+ }
+
+ @if (Event?.Sender?.Split(':', 2)[1] == Homeserver?.ServerName) {
+ <p>User is a local user!</p>
+ <LinkButton OnClick="@DeactivateUser">Deactivate User</LinkButton>
+ <LinkButton OnClick="@QuarantineMediaByUser">Quarantine all media</LinkButton>
+ }
+}
+
+<style>
+ .int-input {
+ width: 128px;
+ }
+
+ .tile {
+ display: inline-block;
+ padding: 4px;
+ border: 1px solid #ffffff22;
+ }
+
+ .tile280 {
+ min-width: 280px;
+ }
+
+ .tile150 {
+ min-width: 150px;
+ }
+
+ .range-sep {
+ display: inline-block;
+ padding: 4px;
+ width: 150px;
+ }
+
+ .range-sep::before {
+ content: "@("<") ";
+ }
+
+ .range-sep::after {
+ content: " @("<")";
+ }
+
+ .center-children {
+ text-align: center;
+ }
+</style>
+
+@code {
+
+ private AuthenticatedHomeserverSynapse? Homeserver { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await RmuStorage.GetCurrentSessionOrNavigate() as AuthenticatedHomeserverSynapse;
+ if (hs is null) return;
+ Homeserver = hs;
+
+ if (!string.IsNullOrWhiteSpace(EventUrl)) {
+ _ = ExpandEventUrl();
+ }
+ }
+
+ [SupplyParameterFromQuery]
+ public string? EventUrl {
+ get;
+ set {
+ field = value?.Split('?')[0];
+ _ = ExpandEventUrl();
+ }
+ }
+
+ private StateEventResponse? Event { get; set; }
+
+ private string? EventJson {
+ get;
+ set {
+ field = value;
+ _ = ExpandEventJson();
+ }
+ }
+
+ private string? MxcUrl {
+ get;
+ set {
+ field = value;
+ _ = ExpandMxcUri();
+ }
+ }
+
+ private MxcUri? MxcUri { get; set; }
+
+ private string? RoomId {
+ get => Event?.RoomId ?? field;
+ set;
+ }
+
+ private async Task ExpandEventUrl() {
+ Console.WriteLine("Expanding event URL...");
+ if (!string.IsNullOrWhiteSpace(EventUrl)) {
+ Console.WriteLine("Parsing event URL...");
+ var data = ParseEventUrl(EventUrl);
+ Console.WriteLine($"Room: {data.room}, Event: {data.eventId}");
+ RoomId = data.room;
+ var room = Homeserver.GetRoom(data.room);
+ var eventResponse = await room.GetEventAsync(data.eventId);
+ eventResponse.RoomId ??= data.room;
+ EventJson = eventResponse?.ToJson() ?? "null";
+ }
+
+ StateHasChanged();
+ }
+
+ private async Task ExpandEventJson() {
+ Console.WriteLine("Expanding event JSON...");
+ if (!string.IsNullOrWhiteSpace(EventJson)) {
+ Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson);
+ MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url;
+ Console.WriteLine($"MXC URL: {MxcUrl}");
+
+ var possiblyRelated = await Homeserver.Admin.GetRoomMediaAsync(Event!.RoomId!);
+ }
+
+ StateHasChanged();
+ }
+
+ private async Task ExpandMxcUri() {
+ Console.WriteLine("Expanding MXC URI...");
+ if (!string.IsNullOrWhiteSpace(MxcUrl)) {
+ MxcUri = MxcUrl;
+ }
+
+ StateHasChanged();
+ }
+
+ private (string room, string eventId) ParseEventUrl(string url) {
+ var parts = url.Split('/');
+ Console.WriteLine($"Parts: {string.Join(", ", parts)}");
+ return (parts[4].UrlDecode(), parts[5].Split('?')[0].UrlDecode());
+ }
+
+#region Local user
+
+ private async Task DeactivateUser() {
+ await Homeserver.Admin.DeactivateUserAsync(Event.Sender, true);
+ }
+
+ private async Task QuarantineMediaByUser() {
+ if (Event is null) return;
+ var media = Homeserver.Admin.GetUserMediaEnumerableAsync(Event?.Sender!);
+ await foreach (var m in media) {
+ if (m is not null) {
+ // await Homeserver.Admin.QuarantineMedia(m);
+ // await Homeserver.Admin.DeleteMedia(m);
+ }
+ }
+ }
+
+#endregion
+
+ private async Task RedactAllEvents() {
+ if (Event is null) return;
+ await Homeserver!.Admin.DeleteAllMessages(Event.Sender!);
+ }
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor.css
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
new file mode 100644
index 0000000..d598994
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindow.razor
@@ -0,0 +1,5 @@
+<h3>SynapseRoomShutdownWindow</h3>
+
+@code {
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
new file mode 100644
index 0000000..d5daf75
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -0,0 +1,113 @@
+@using LibMatrix.Homeservers.Extensions.NamedCaches
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+
+@if (string.IsNullOrWhiteSpace(Context.DeleteId)) {
+ <b>Media options</b>
+ <br/>
+ <hr/>
+ <span>Quarantine local media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalMedia"/>
+ <br/>
+ <span>Quarantine remote media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineRemoteMedia"/>
+ <br/>
+ <span>Delete remote media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteRemoteMedia"/>
+ <br/>
+
+ <b>User options</b>
+ <br/>
+ <hr/>
+ <span>Suspend local users: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.SuspendLocalUsers"></InputCheckbox>
+ <br/>
+ <span>Quarantine <b>ALL</b> local user media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.QuarantineLocalUserMedia"></InputCheckbox>
+ <br/>
+ <span>Delete <b>ALL</b> local user media: </span>
+ <InputCheckbox @bind-Value="@Context.ExtraOptions.DeleteLocalUserMedia"></InputCheckbox>
+ <br/>
+
+ <b>Room deletion options</b>
+ <br/>
+ <hr/>
+ <span>Block room: </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/>
+ <br/>
+ <span>Purge room: </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.Purge"/>
+ <br/>
+ <span>Force purge room (unsafe): </span>
+ <InputCheckbox @bind-Value="@Context.DeleteRequest.ForcePurge"></InputCheckbox>
+ <br/>
+ <span>Warning room User ID (optional): </span>
+ <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/>
+ <br/>
+ @if (!string.IsNullOrWhiteSpace(Context.DeleteRequest.NewRoomUserId)) {
+ <span>Warning room name: </span>
+ <FancyTextBox @bind-Value="@Context.DeleteRequest.RoomName"/>
+ <br/>
+ <span>Warning room message (plaintext): </span>
+ <FancyTextBox Multiline="true" @bind-Value="@Context.DeleteRequest.Message"/>
+ <br/>
+ }
+
+ <LinkButton OnClick="@DeleteRoom">Execute</LinkButton>
+}
+
+@code {
+
+ [Parameter]
+ public required RoomShutdownContext Context { get; set; }
+
+ [Parameter]
+ public required AuthenticatedHomeserverSynapse Homeserver { get; set; }
+
+ private NamedCache<RoomShutdownContext> TaskMap { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync() {
+ TaskMap = new NamedCache<RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks");
+ }
+
+ public class RoomShutdownContext {
+ public required string RoomId { get; set; }
+ public string? DeleteId { get; set; }
+ public ExtraDeleteOptions ExtraOptions { get; set; } = new();
+
+ public SynapseAdminRoomDeleteRequest DeleteRequest { get; set; } = new() {
+ Block = true,
+ Purge = true,
+ ForcePurge = false
+ };
+
+ public class ExtraDeleteOptions {
+ // room options
+ public bool QuarantineLocalMedia { get; set; }
+ public bool QuarantineRemoteMedia { get; set; }
+
+ public bool DeleteRemoteMedia { get; set; }
+
+ // user options
+ public bool SuspendLocalUsers { get; set; }
+ public bool QuarantineLocalUserMedia { get; set; }
+ public bool DeleteLocalUserMedia { get; set; }
+ }
+ }
+
+ public async Task OnComplete() {
+ await OnCompleteLock.WaitAsync();
+ try {
+ await TaskMap.RemoveValueAsync(Context.DeleteId!);
+ }
+ finally {
+ OnCompleteLock.Release();
+ }
+ }
+
+ public async Task DeleteRoom() {
+ await TaskMap.SetValueAsync(Context.RoomId, Context);
+ }
+
+ private static readonly SemaphoreSlim OnCompleteLock = new(1, 1);
+
+}
\ 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 07af1dc..0a93df8 100644
--- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,7 +1,10 @@
@page "/HSAdmin/Synapse/RoomQuery"
-@using LibMatrix.Responses.Admin
-@using LibMatrix.Filters
+@using Microsoft.AspNetCore.WebUtilities
@using ArcaneLibs.Extensions
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests
+@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses
+@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components
<h3>Homeserver Administration - Room Query</h3>
@@ -48,13 +51,17 @@
<u style="display: block;">Ranges</u>
<span class="tile center-children">
- <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber>
+ <InputNumber max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsGreaterThan"></InputNumber><span class="range-sep">state events</span><InputNumber
+ max="@int.MaxValue" class="int-input" TValue="int" @bind-Value="@Filter.StateEventsLessThan"></InputNumber>
</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>
+ <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>
</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>
+ <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>
</span>
</div>
</details>
@@ -98,41 +105,84 @@
<br/>
}
</p>
+ <p>
+ <LinkButton OnClick="@(() => {
+ DeleteRequests.Add(res.RoomId, new() {
+ RoomId = res.RoomId,
+ DeleteRequest = new() {
+ Block = true,
+ Purge = true,
+ ForcePurge = false
+ }
+ });
+
+ return Task.CompletedTask;
+ })">Delete room
+ </LinkButton>
+ </p>
<span>@res.StateEvents state events</span><br/>
- <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
+ @if (res.LocalMembers is null) {
+ <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
+ }
+ else {
+ <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</span>
+ }
<details>
<summary>Full result data</summary>
<pre>@res.ToJson(ignoreNull: true)</pre>
</details>
</div>
}
+@* *@
+@* @if (DeleteRequest.HasValue) { *@
+@* <ModalWindow MinWidth="600" Title="@("Delete " + DeleteRequest.Value.RoomId)" OnCloseClicked="@(() => { DeleteRequest = null; })"> *@
+@* *@
+@* </ModalWindow> *@
+@* } *@
+
+@* @foreach (var (roomId, status) in DeleteStatuses) { *@
+@* <ModalWindow Title="@("Delete status for " + roomId)" MinWidth="600"> *@
+@* <pre>@status.ToJson()</pre> *@
+@* </ModalWindow> *@
+@* } *@
+
+@foreach(var (roomId, deleteRequest) in DeleteRequests) {
+ <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/>
+}
<style>
.int-input {
width: 128px;
}
+
.tile {
display: inline-block;
padding: 4px;
border: 1px solid #ffffff22;
}
+
.tile280 {
min-width: 280px;
}
+
.tile150 {
min-width: 150px;
}
+
.range-sep {
display: inline-block;
padding: 4px;
width: 150px;
}
+
.range-sep::before {
content: "@("<") ";
}
+
.range-sep::after {
content: " @("<")";
}
+
.center-children {
text-align: center;
}
@@ -152,16 +202,92 @@
[SupplyParameterFromQuery(Name = "ascending")]
public bool Ascending { get; set; }
- public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new();
+ private List<RoomInfo> Results { get; set; } = new();
+
+ private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!;
private string Status { get; set; }
- public LocalRoomQueryFilter Filter { get; set; } = new();
+ public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new();
+
+ private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = [];
+
+ // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new();
protected override Task OnParametersSetAsync() {
if (Ascending == null)
Ascending = true;
OrderBy ??= "name";
+
+ var execute = false;
+
+ foreach (var (key, value) in QueryHelpers.ParseQuery(new Uri(NavigationManager.Uri).Query)) {
+ switch (key) {
+ case "RoomIdContains":
+ Filter.RoomIdContains = value[0]!;
+ break;
+ case "NameContains":
+ Filter.NameContains = value[0]!;
+ break;
+ case "CanonicalAliasContains":
+ Filter.CanonicalAliasContains = value[0]!;
+ break;
+ case "VersionContains":
+ Filter.VersionContains = value[0]!;
+ break;
+ case "CreatorContains":
+ Filter.CreatorContains = value[0]!;
+ break;
+ case "EncryptionContains":
+ Filter.EncryptionContains = value[0]!;
+ break;
+ case "JoinRulesContains":
+ Filter.JoinRulesContains = value[0]!;
+ break;
+ case "GuestAccessContains":
+ Filter.GuestAccessContains = value[0]!;
+ break;
+ case "HistoryVisibilityContains":
+ Filter.HistoryVisibilityContains = value[0]!;
+ break;
+ case "Federatable":
+ Filter.Federatable = bool.Parse(value[0]!);
+ Filter.CheckFederation = true;
+ break;
+ case "Public":
+ Filter.Public = value[0] == "true";
+ Filter.CheckPublic = true;
+ break;
+ case "JoinedMembersGreaterThan":
+ Filter.JoinedMembersGreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedMembersLessThan":
+ Filter.JoinedMembersLessThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersGreaterThan":
+ Filter.JoinedLocalMembersGreaterThan = int.Parse(value[0]!);
+ break;
+ case "JoinedLocalMembersLessThan":
+ Filter.JoinedLocalMembersLessThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsGreaterThan":
+ Filter.StateEventsGreaterThan = int.Parse(value[0]!);
+ break;
+ case "StateEventsLessThan":
+ Filter.StateEventsLessThan = int.Parse(value[0]!);
+ break;
+ case "Execute":
+ execute = true;
+ break;
+ default:
+ Console.WriteLine($"Unknown query parameter: {key}");
+ break;
+ }
+ }
+
+ if (execute)
+ _ = Search();
+
return Task.CompletedTask;
}
@@ -169,18 +295,107 @@
Results.Clear();
var hs = await RmuStorage.GetCurrentSessionOrNavigate();
if (hs is AuthenticatedHomeserverSynapse synapse) {
+ Homeserver = synapse;
var searchRooms = synapse.Admin.SearchRoomsAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", searchTerm: SearchTerm, localFilter: Filter).GetAsyncEnumerator();
while (await searchRooms.MoveNextAsync()) {
var room = searchRooms.Current;
- Console.WriteLine("Hit: " + room.ToJson(false));
- Results.Add(room);
- if (Results.Count % 10 == 0)
+
+ if (Results.Count < 100)
+ Console.WriteLine("Hit: " + room.ToJson(false));
+
+ var roomInfo = new RoomInfo {
+ RoomId = room.RoomId,
+ Name = room.Name,
+ CanonicalAlias = room.CanonicalAlias,
+ Creator = room.Creator,
+ Version = room.Version,
+ Encryption = room.Encryption,
+ Federatable = room.Federatable,
+ Public = room.Public,
+ JoinRules = room.JoinRules,
+ GuestAccess = room.GuestAccess,
+ HistoryVisibility = room.HistoryVisibility,
+ StateEvents = room.StateEvents,
+ JoinedMembers = room.JoinedMembers,
+ JoinedLocalMembers = room.JoinedLocalMembers
+ };
+
+ Results.Add(roomInfo);
+
+ if (room.JoinedLocalMembers is > 0 and < 100)
+ roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList();
+
+ if (Results.Count < 200 || Results.Count % 1000 == 0) {
StateHasChanged();
+ await Task.Yield();
+ }
}
}
StateHasChanged();
}
+ //
+ // 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 readonly Dictionary<string, string> validOrderBy = new() {
{ "name", "Room name" },
@@ -198,4 +413,8 @@
{ "state_events", "Number of state events" }
};
+ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom {
+ public List<string>? LocalMembers { get; set; }
+ }
+
}
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 2549c12..d9503ee 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -2,6 +2,7 @@
@using System.Collections.Frozen
@using System.Collections.ObjectModel
@using System.Diagnostics
+@using System.Text.Json
@using ArcaneLibs.Extensions
@using LibMatrix
@using LibMatrix.EventTypes.Spec.State.RoomInfo
@@ -422,7 +423,15 @@
var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] };
var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false));
await foreach (var resp in events) {
- var all = resp.State.Concat(resp.Chunk);
+ var all = resp.State.Concat(resp.Chunk)
+ // ugly hack, because some users fuck around too much
+ .Select(x => {
+ if (x.RawContent?["displayname"]?.GetValueKind() != JsonValueKind.String)
+ x.RawContent?.Remove("displayname");
+ if (x.RawContent?["avatar_url"]?.GetValueKind() is not JsonValueKind.String)
+ x.RawContent?.Remove("avatar_url");
+ return x;
+ });
Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId));
Log.Add($"Got {resp.State.Count} state and {resp.Chunk.Count} timeline events.");
diff --git a/MatrixUtils.Web/Program.cs b/MatrixUtils.Web/Program.cs
index 8bc2c8f..8cab0fb 100644
--- a/MatrixUtils.Web/Program.cs
+++ b/MatrixUtils.Web/Program.cs
@@ -28,7 +28,7 @@ builder.Services.AddWebWorkerService(webWorkerService =>
webWorkerService.TaskPool.MaxPoolSize = 2;
// Below is telling the WebWorkerService TaskPool to set the initial size to 2 if running in a Window scope and 0 otherwise
// This starts up 2 WebWorkers to handle TaskPool tasks as needed
- webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 2 : 0;
+ webWorkerService.TaskPool.PoolSize = webWorkerService.GlobalScope == GlobalScope.Window ? 0 : 0;
});
try {
|