diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
new file mode 100644
index 0000000..07af1dc
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -0,0 +1,201 @@
+@page "/HSAdmin/Synapse/RoomQuery"
+@using LibMatrix.Responses.Admin
+@using LibMatrix.Filters
+@using ArcaneLibs.Extensions
+
+<h3>Homeserver Administration - Room Query</h3>
+
+<label>Search name: </label>
+<InputText @bind-Value="SearchTerm"/><br/>
+<label>Order by: </label>
+<select @bind="OrderBy">
+ @foreach (var item in validOrderBy) {
+ <option value="@item.Key">@item.Value</option>
+ }
+</select><br/>
+<label>Ascending: </label>
+<InputCheckbox @bind-Value="Ascending"/><br/>
+<details>
+ <summary>
+ <span>Local filtering (slow)</span>
+
+ </summary>
+ <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>
+
+ <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>
+
+ <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>
+ </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>
+ </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>
+ </span>
+ </div>
+</details>
+<button class="btn btn-primary" @onclick="Search">Search</button>
+<br/>
+
+@if (Results.Count > 0) {
+ <p>Found @Results.Count rooms</p>
+ <details>
+ <summary>TSV data (copy/paste)</summary>
+ <pre style="font-size: 0.6em;">
+ <table>
+ @foreach (var res in Results) {
+ <tr>
+ <td style="padding: 8px;">@res.RoomId@("\t")</td>
+ <td style="padding: 8px;">@res.CanonicalAlias@("\t")</td>
+ <td style="padding: 8px;">@res.Creator@("\t")</td>
+ <td style="padding: 8px;">@res.Name</td>
+ </tr>
+ }
+ </table>
+ </pre>
+ </details>
+}
+
+@foreach (var res in Results) {
+ <div style="background-color: #ffffff11; border-radius: 0.5em; display: block; margin-top: 4px; padding: 4px;">
+ @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@
+ <p>
+ @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) {
+ <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span>
+ <br/>
+ }
+ else {
+ <span>@res.RoomId (@res.Name)</span>
+ <br/>
+ }
+ @if (!string.IsNullOrWhiteSpace(res.Creator)) {
+ @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@
+ <span>Created by @res.Creator</span>
+ <br/>
+ }
+ </p>
+ <span>@res.StateEvents state events</span><br/>
+ <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span>
+ <details>
+ <summary>Full result data</summary>
+ <pre>@res.ToJson(ignoreNull: true)</pre>
+ </details>
+ </div>
+}
+
+<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 {
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "order_by")]
+ public string? OrderBy { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "name_search")]
+ public string SearchTerm { get; set; }
+
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "ascending")]
+ public bool Ascending { get; set; }
+
+ public List<AdminRoomListingResult.AdminRoomListingResultRoom> Results { get; set; } = new();
+
+ private string Status { get; set; }
+
+ public LocalRoomQueryFilter Filter { get; set; } = new();
+
+ protected override Task OnParametersSetAsync() {
+ if (Ascending == null)
+ Ascending = true;
+ OrderBy ??= "name";
+ return Task.CompletedTask;
+ }
+
+ private async Task Search() {
+ Results.Clear();
+ var hs = await RmuStorage.GetCurrentSessionOrNavigate();
+ if (hs is AuthenticatedHomeserverSynapse 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)
+ StateHasChanged();
+ }
+ }
+
+ StateHasChanged();
+ }
+
+ private readonly Dictionary<string, string> validOrderBy = new() {
+ { "name", "Room name" },
+ { "canonical_alias", "Main alias address" },
+ { "joined_members", "Number of members (reversed)" },
+ { "joined_local_members", "Number of local members (reversed)" },
+ { "version", "Room version" },
+ { "creator", "Creator of the room" },
+ { "encryption", "End-to-end encryption algorithm" },
+ { "federatable", "Is room federated" },
+ { "public", "Visibility in room list" },
+ { "join_rules", "Join rules" },
+ { "guest_access", "Guest access" },
+ { "history_visibility", "Visibility of history" },
+ { "state_events", "Number of state events" }
+ };
+
+}
diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
|