about summary refs log tree commit diff
path: root/MatrixUtils.Web/Pages
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixUtils.Web/Pages')
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevOptions.razor16
-rw-r--r--MatrixUtils.Web/Pages/Dev/DevUtilities.razor31
-rw-r--r--MatrixUtils.Web/Pages/Dev/WellKnownRes.razor3
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor2
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor11
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor74
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css35
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor5
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor235
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor714
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor211
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor243
-rw-r--r--MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css7
-rw-r--r--MatrixUtils.Web/Pages/Index.razor22
-rw-r--r--MatrixUtils.Web/Pages/InvalidSession.razor6
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor2
-rw-r--r--MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs10
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor14
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor22
-rw-r--r--MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor8
-rw-r--r--MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor1
-rw-r--r--MatrixUtils.Web/Pages/LoginPage.razor8
-rw-r--r--MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor6
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create.razor39
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Create2.razor147
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Index.razor43
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor877
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs142
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css9
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList2.razor48
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor74
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor88
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor218
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyLists.razor160
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css6
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor52
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor92
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor83
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor60
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor19
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor123
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor70
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor65
-rw-r--r--MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor51
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Space.razor82
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateEditor.razor16
-rw-r--r--MatrixUtils.Web/Pages/Rooms/StateViewer.razor53
-rw-r--r--MatrixUtils.Web/Pages/Rooms/Timeline.razor11
-rw-r--r--MatrixUtils.Web/Pages/ServerInfo.razor1
-rw-r--r--MatrixUtils.Web/Pages/StreamTest.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor70
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor6
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor12
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor13
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor15
-rw-r--r--MatrixUtils.Web/Pages/Tools/InviteCounter.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/MassCMEBan.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor10
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor195
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor18
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor17
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor51
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor204
-rw-r--r--MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor8
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor17
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/StickerManager.razor80
-rw-r--r--MatrixUtils.Web/Pages/User/Profile.razor36
77 files changed, 4084 insertions, 1029 deletions
diff --git a/MatrixUtils.Web/Pages/Dev/DevOptions.razor b/MatrixUtils.Web/Pages/Dev/DevOptions.razor

index 33e577f..281cf07 100644 --- a/MatrixUtils.Web/Pages/Dev/DevOptions.razor +++ b/MatrixUtils.Web/Pages/Dev/DevOptions.razor
@@ -21,7 +21,7 @@ </p> <details> <summary>Manage local sessions</summary> - + </details> @if (userSettings is not null) { @@ -40,10 +40,14 @@ @code { private RmuSessionStore.Settings? userSettings { get; set; } + protected override async Task OnInitializedAsync() { - // userSettings = await TieredStorage.DataStorageProvider.LoadObjectAsync<RmuSessionStore.Settings>("rmu.settings"); - - await base.OnInitializedAsync(); + await (Task)typeof(RmuSessionStore).GetMethod("LoadStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(sessionStore, [true])!; + await foreach (var _ in sessionStore.TryGetAllHomeservers()) { } + + await (Task)typeof(RmuSessionStore).GetMethod("SaveStorage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(sessionStore, [true])!; } private async Task LogStuff() { @@ -58,8 +62,9 @@ foreach (var key in keys) { data.Add(key, await TieredStorage.DataStorageProvider.LoadObjectAsync<object>(key)); } + var dataUri = "data:application/json;base64,"; - dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); + dataUri += Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data))); await JSRuntime.InvokeVoidAsync("window.open", dataUri, "_blank"); } @@ -69,6 +74,7 @@ foreach (var (key, value) in data) { await TieredStorage.DataStorageProvider.SaveObjectAsync(key, value); } + NavigationManager.NavigateTo(NavigationManager.Uri, true, true); } diff --git a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
index 3b2d533..f6392a4 100644 --- a/MatrixUtils.Web/Pages/Dev/DevUtilities.razor +++ b/MatrixUtils.Web/Pages/Dev/DevUtilities.razor
@@ -1,5 +1,8 @@ @page "/Dev/Utilities" @using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.Ephemeral +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Helpers @using MatrixUtils.Abstractions <h3>Debug Tools</h3> @@ -14,7 +17,7 @@ else { <summary>Room List</summary> @foreach (var roomId in Rooms) { <a style="color: unset; text-decoration: unset;" href="/RoomStateViewer/@roomId.Replace('.', '~')"> - <RoomListItem RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> + <RoomListItem Homeserver="hs" RoomInfo="@(new RoomInfo(hs.GetRoom(roomId)))" LoadData="true"></RoomListItem> </a> } </details> @@ -61,6 +64,7 @@ else { StateHasChanged(); return; } + if (res.Content.Headers.ContentType.MediaType == "application/json") GetRequestResult = $"Error: {res.StatusCode}\n" + (await res.Content.ReadFromJsonAsync<object>()).ToJson(); else @@ -69,7 +73,32 @@ else { catch (Exception e) { GetRequestResult = $"Error: {e}"; } + StateHasChanged(); } + private async Task TestRoomBuilder() { + var rb = new RoomBuilder() { + HistoryVisibility = new RoomHistoryVisibilityEventContent() { HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared }, + ImportantState = [ + new() { + RawContent = new() { + ["type"] = "m.room.name", + ["name"] = "Test Room" + } + }, + new() { + Type = "test", + TypedContent = new PresenceEventContent() { + Presence = "online", + LastActiveAgo = 0, + } + }, + + ] + }; + + await rb.Create(hs); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
index 1906dd8..722f9b3 100644 --- a/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor +++ b/MatrixUtils.Web/Pages/Dev/WellKnownRes.razor
@@ -8,7 +8,7 @@ <h3>Known Homeserver List</h3> <hr/> -<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClick="@Execute">Execute</LinkButton></span> +<span>Room ID: <FancyTextBox @bind-Value="@RoomId"/><LinkButton OnClickAsync="@Execute">Execute</LinkButton></span> <span>Stats:</span><br/> <span>Server count: @entries.Count</span><br/> @@ -72,6 +72,7 @@ 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 } }; + public bool HasPolicyServerWellKnown => WellKnownResolutionResult?.PolicyServerWellKnown?.Content is not null and not { PublicKey: null or "" }; } private async Task Execute() { diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
index e1b46e2..21b0972 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSAdmin.razor
@@ -1,5 +1,6 @@ @page "/HSAdmin" @using ArcaneLibs.Extensions +@using LibMatrix.Responses.Federation <h3>Homeserver Admininistration</h3> <hr/> @@ -11,7 +12,9 @@ else { <h4>Synapse tools</h4> <hr/> <a href="/HSAdmin/Synapse/RoomQuery">Query rooms</a><br/> - <a href="/HSAdmin/Synapse/BlockMedia">Block media</a> + <a href="/HSAdmin/Synapse/UserQuery">Query users</a><br/> + <a href="/HSAdmin/Synapse/BlockMedia">Block media</a><br/> + <a href="/HSAdmin/Synapse/BackgroundJobs">View running background jobs</a><br/> } else if (Homeserver is AuthenticatedHomeserverHSE) { <h4>Rory&amp;::LibMatrix.HomeserverEmulator tools</h4> diff --git a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
index 87600c6..ec2ec54 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/HSE/ManageExternalProfiles.razor
@@ -3,7 +3,7 @@ @using LibMatrix.Responses <h3>Manage external profiles</h3> -<LinkButton OnClick="AddAllLocalProfiles">Add local sessions</LinkButton> +<LinkButton OnClickAsync="AddAllLocalProfiles">Add local sessions</LinkButton> @foreach(var p in ExternalProfiles) { diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
index d07ff08..5ccaca9 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/BlockMedia.razor
@@ -3,6 +3,7 @@ @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes.Spec +@using LibMatrix.StructuredData <h3>Homeserver Administration - Block media</h3> @if (Homeserver is not null) { @@ -24,13 +25,13 @@ <pre>@MxcUri?.ToJson(ignoreNull: true)</pre> @if (Event is not null) { - <LinkButton OnClick="@RedactAllEvents">Redact all messages</LinkButton> + <LinkButton OnClickAsync="@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> + <LinkButton OnClickAsync="@DeactivateUser">Deactivate User</LinkButton> + <LinkButton OnClickAsync="@QuarantineMediaByUser">Quarantine all media</LinkButton> } } @@ -95,7 +96,7 @@ } } - private StateEventResponse? Event { get; set; } + private MatrixEventResponse? Event { get; set; } private string? EventJson { get; @@ -139,7 +140,7 @@ private async Task ExpandEventJson() { Console.WriteLine("Expanding event JSON..."); if (!string.IsNullOrWhiteSpace(EventJson)) { - Event = JsonSerializer.Deserialize<StateEventResponse>(EventJson); + Event = JsonSerializer.Deserialize<MatrixEventResponse>(EventJson); MxcUrl = Event?.ContentAs<RoomMessageEventContent>()?.Url; Console.WriteLine($"MXC URL: {MxcUrl}"); diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor new file mode 100644
index 0000000..f1c5907 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor
@@ -0,0 +1,74 @@ +@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"><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"><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"> + <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"> + <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"> + <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 { + + [Parameter] + public required SynapseAdminLocalRoomQueryFilter Filter { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css new file mode 100644
index 0000000..83ce426 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryFilter.razor.css
@@ -0,0 +1,35 @@ +.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; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor new file mode 100644
index 0000000..5591072 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/RoomQuery/SynapseRoomQueryResult.razor
@@ -0,0 +1,5 @@ +<h3>SynapseRoomQueryResult</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
index d5daf75..b0e6a89 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/Components/SynapseRoomShutdownWindowContent.razor
@@ -1,36 +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)) { - <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/> +@if (string.IsNullOrWhiteSpace(Context.DeleteId) || EditorOnly) { <span>Block room: </span> <InputCheckbox @bind-Value="@Context.DeleteRequest.Block"/> <br/> @@ -40,19 +16,63 @@ <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)) { + <details> + <summary>Media</summary> + <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"/> + </details> + + <details> + <summary>Local users</summary> + <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/> + <span>Follow tombstone (if any): </span> + <InputCheckbox @bind-Value="@Context.ExtraOptions.FollowTombstone"/> + @if (!EditorOnly) { + <LinkButton InlineText="true" OnClickAsync="@FollowTombstoneAsync">Exec</LinkButton> + } + </details> + + <details> + <summary>Issue warning to local members (optional)</summary> + <b>All fields are required if used!</b><br/> + <span>Warning room User ID: </span> + <FancyTextBox @bind-Value="@Context.DeleteRequest.NewRoomUserId"/> + <br/> <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/> - } + </details> - <LinkButton OnClick="@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 { @@ -63,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(); @@ -80,11 +138,14 @@ ForcePurge = false }; + 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,9 +171,96 @@ } 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); } - + 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<MatrixEventResponse?> 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 79e7357..05899c8 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor
@@ -1,10 +1,18 @@ @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 +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches @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 +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery +@inject ILogger<RoomQuery> Logger +@inject BlazorSaveFileService BlazorSaveFileService <h3>Homeserver Administration - Room Query</h3> @@ -16,121 +24,154 @@ <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> - <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> + <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) { <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;"> +@foreach (var room in Results) { + <div class="room-list-item"> @* <RoomListItem RoomName="@res.Name" RoomId="@res.RoomId"></RoomListItem> *@ <p> - @if (!string.IsNullOrWhiteSpace(res.CanonicalAlias)) { - <span>@res.CanonicalAlias - @res.RoomId (@res.Name)</span> - <br/> + @if (EnableMultiPurge) { + <InputCheckbox @bind-Value="@room.MultiPurgeSelected"/> + <span> </span> } - else { - <span>@res.RoomId (@res.Name)</span> - <br/> + @if (!string.IsNullOrWhiteSpace(room.CanonicalAlias)) { + <span>@room.CanonicalAlias - </span> } - @if (!string.IsNullOrWhiteSpace(res.Creator)) { - @* <span>Created by <InlineUserItem UserId="@res.Creator"></InlineUserItem></span> *@ - <span>Created by @res.Creator</span> + <span>@room.RoomId</span> + @if (!string.IsNullOrWhiteSpace(room.Name)) { + <span> (@room.Name)</span> + } + <br/> + + @if (!string.IsNullOrWhiteSpace(room.Creator)) { + <span>Created by @room.Creator</span> <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> + <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> - <span>@res.StateEvents state events</span><br/> - @if (res.LocalMembers is null) { - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server</span> + + @{ + List<string?> flags = []; + if (room.JoinedLocalMembers > 0) { + flags.Add(room.JoinRules switch { + "public" => "Public", + "invite" => "Invite only", + "knock" => "Knock", + "restricted" => "Restricted", + "knock_restricted" => "Knock + restricted", + // TODO: default? + null => null, + "" => null, + _ => "unknown join rule: " + room.JoinRules + }); + + if (!string.IsNullOrWhiteSpace(room.Encryption)) flags.Add("encrypted"); + if (!room.Federatable) flags.Add("unfederated"); + + flags.Add(room.HistoryVisibility switch { + "world_readable" => "world readable history", + "shared" => "shared history", + "invited" => "history since invite", + "joined" => "history since join", + // TODO: default? + null => null, + "" => null, + _ => "unknown history setting: " + room.HistoryVisibility + }); + + flags.Add(room.GuestAccess switch { + "can_join" => "guests allowed", + "forbidden" => null, + // TODO: default? + null => null, + "" => null, + _ => "unknown guest access: " + room.GuestAccess, + }); + + flags = flags.Where(x => x != null).ToList(); + } + } + <span>@string.Join(", ", flags)</span> + @if (room.JoinedLocalMembers == 0 && flags.Count > 0) { + <span> at the time of leaving</span> } - else { - <span>@res.JoinedMembers members, of which @res.JoinedLocalMembers are on this server: @(string.Join(", ", res.LocalMembers))</span> + <br/> + + <span>@room.StateEvents state events, room version @(room.Version ?? "1")</span><br/> + @if (room.TombstoneEvent is not null) { + var tombstoneContent = room.TombstoneEvent.ContentAs<RoomTombstoneEventContent>()!; + <span>Room is tombstoned! Target room: @tombstoneContent.ReplacementRoom, message: @tombstoneContent.Body</span> + <br/> + } + + @{ + var memberSummary = room.MemberSummary; + if (room.LocalMembers is not null) { + memberSummary += $": {string.Join(", ", room.LocalMembers)}"; + } + } + <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> } - <details> - <summary>Full result data</summary> - <pre>@res.ToJson(ignoreNull: true)</pre> - </details> </div> } @* *@ @@ -146,48 +187,15 @@ @* </ModalWindow> *@ @* } *@ -@foreach(var (roomId, deleteRequest) in DeleteRequests) { - <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> +@foreach (var (roomId, deleteRequest) in DeleteRequests) { + <ModalWindow Title="@($"Delete room {roomId}")" OnCloseClicked="@(() => { + DeleteRequests.Remove(roomId); + StateHasChanged(); + })"> + <SynapseRoomShutdownWindowContent Context="deleteRequest" Homeserver="Homeserver"/> + </ModalWindow> } -<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] @@ -196,27 +204,65 @@ [Parameter] [SupplyParameterFromQuery(Name = "name_search")] - public string SearchTerm { get; set; } + public string? SearchTerm { get; set; } [Parameter] [SupplyParameterFromQuery(Name = "ascending")] - public bool Ascending { get; set; } + 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!; - private string Status { get; set; } - - public SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); + private SynapseAdminLocalRoomQueryFilter Filter { get; set; } = new(); private Dictionary<string, SynapseRoomShutdownWindowContent.RoomShutdownContext> DeleteRequests { get; set; } = []; // private Dictionary<string, SynapseAdminRoomDeleteStatus> DeleteStatuses { get; set; } = new(); + 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) { + NavigationManager.NavigateTo("/"); + return; + } + + Homeserver = synapse; + TaskMap = new NamedCache<SynapseRoomShutdownWindowContent.RoomShutdownContext>(Homeserver, "gay.rory.matrixutils.synapse_room_shutdown_tasks"); + DeleteRequests = (await TaskMap.ReadCacheMapAsync()).Where(x => x.Value.DeleteId is not null).ToDictionary(); + StateHasChanged(); + } + protected override Task OnParametersSetAsync() { - if (Ascending == null) - Ascending = true; OrderBy ??= "name"; var execute = false; @@ -224,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(); @@ -293,109 +367,96 @@ private async Task Search() { Results.Clear(); - var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); - 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("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; - 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, + OriginHomeserver = + Homeserver.GetRoom(room.RoomId).IsV12PlusRoomId + ? room.RoomId.Split(':', 2).Skip(1).FirstOrDefault(string.Empty) + : string.Empty + }; - 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 (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)}"); + } + } - if (room.JoinedLocalMembers is > 0 and < 100) - roomInfo.LocalMembers = (await synapse.Admin.GetRoomMembersAsync(room.RoomId)).Members.Where(x => x.EndsWith(":" + synapse.ServerName)).ToList(); + Results.Add(roomInfo); - if (Results.Count < 200 || Results.Count % 1000 == 0) { - StateHasChanged(); - await Task.Yield(); - } + if ((Results.Count <= 200 && Results.Count % 10 == 0 && FetchV12PlusCreatorServer) || Results.Count % 1000 == 0) { + StateHasChanged(); + await Task.Yield(); + await Task.Delay(1); } } StateHasChanged(); + + if (FetchV12PlusCreatorServer) await FetchV12PlusCreatorServersAsync(false); + if (SummarizeLocalMembers) await FetchLocalMemberEventsAsync(false); + // if (CheckTombstone) await FetchTombstoneEventsAsync(false); + + StateHasChanged(); + } + + 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 void PurgeSelection() { + foreach (var room in Results.Where(x => x.MultiPurgeSelected)) { + DeleteRoom(room, true); + } } - // - // 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" }, @@ -415,6 +476,143 @@ private class RoomInfo : SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom { public List<string>? LocalMembers { 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/HSAdmin/Synapse/RoomQuery.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
index e69de29..62941e5 100644 --- a/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/RoomQuery.razor.css
@@ -0,0 +1,7 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor new file mode 100644
index 0000000..3cc5a6a --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/SubTools/SynapseRoomStateResync.razor
@@ -0,0 +1,211 @@ +@page "/HSAdmin/Synapse/ResyncState" +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests + +<h3>Resync room state with other server</h3> +<hr/> + +@if (!Executing) { + <p>WARNING: Will likely not work on invite-only/knock rooms! May also mess with history visibility!</p> + <p>If the room is using mjolnir/draupnir, it's probably recommended to set the "via" to the server it's hosted on.</p> + <span>Room ID: </span> + <InputText @bind-Value="@RoomId"></InputText> + <br/> + <span>Via: </span> + <InputText @bind-Value="@Via"></InputText> + <br/> + <LinkButton OnClickAsync="@Execute">Execute</LinkButton> +} + +@if (Executing) { + <p>Execution in progress. DO NOT CLOSE THIS PAGE!</p> +} +@* stage 1 *@ +@if (Stage >= 1) { + @if (Members is null) { + <p>Loading members...</p> + } + else { + <p>Got @Members.Count local members</p> + } +} + +@* stage 2 *@ +@if (Stage == 2) { + <p>Purging room, please wait...</p> + <pre>@DeleteStatus.ToJson(ignoreNull: true)</pre> +} + +@* stage 3 *@ +@if (Stage == 3) { + <p>Rejoining room, please wait...</p> + <p>Members left to restore: </p> + string members = ""; + foreach (var member in Members) { + members += $"{member.StateKey} ({member.ContentAs<RoomMemberEventContent>()?.ToJson(indent: false, ignoreNull: true)})\n"; + } + + <pre> + @members + </pre> +} + +@if (Stage == 4) { + <p>Execution finished. You may now close the page :)</p> +} + +@if (Error is not null) { + <p style="color: red">Error: @Error.Message</p> + <pre> + @Error.ToString() + </pre> +} + +@code { + + [Parameter] + [SupplyParameterFromQuery] + public string? RoomId { get; set; } + + [Parameter] + [SupplyParameterFromQuery(Name = "via")] + public string? Via { get; set; } + + private AuthenticatedHomeserverSynapse? Homeserver { get; set; } + + // Execution flow + private int Stage { get; set; } + private bool Executing { get; set; } + private Exception? Error { get; set; } + + // Stage 1 + private List<MatrixEventResponse>? Members { get; set; } + + // Stage 2 + private SynapseAdminRoomDeleteStatus? DeleteStatus { get; set; } + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverSynapse hs) return; + Homeserver = hs; + + StateHasChanged(); + } + + private Task Execute() => Execute(0); + + private async Task Execute(int startStage) { + if (string.IsNullOrWhiteSpace(RoomId)) return; + if (string.IsNullOrWhiteSpace(Via)) return; + Executing = true; + StateHasChanged(); + + await ExecuteStages(startStage); + + StateHasChanged(); + } + + private async Task ExecuteStages(int startStage) { + if (startStage <= 1) + if (!await TryGetRoomMembers()) + return; + if (startStage <= 2) + if (!await TryPurgeRoom()) + return; + if (startStage <= 3) + if (!await TryRestoreRoom()) + return; + + Stage = 4; + Executing = false; + StateHasChanged(); + } + + private async Task<bool> TryGetRoomMembers() { + Stage = 1; + try { + Members = (await Homeserver.Admin.GetRoomStateAsync(RoomId, type: RoomMemberEventContent.EventId)) + .Events.Where(m => (m.StateKey?.EndsWith(':' + Homeserver.ServerName) ?? false) && m.ContentAs<RoomMemberEventContent>()!.Membership == "join") + .ToList(); + Console.WriteLine(Members.ToJson(ignoreNull: true)); + StateHasChanged(); + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + + private async Task<bool> TryPurgeRoom() { + Stage = 2; + + try { + var resp = await Homeserver.Admin.DeleteRoom(RoomId, new SynapseAdminRoomDeleteRequest { + Block = true, + Purge = true, + // ForcePurge = true // This causes synapse to early return and not actually purge stuff... + }, waitForCompletion: false); + + while (true) { + // we dont want API failure to break this step + try { + DeleteStatus = await Homeserver.Admin.GetRoomDeleteStatus(resp.DeleteId); + StateHasChanged(); + if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Complete) { + return true; + } + + if (DeleteStatus.Status == SynapseAdminRoomDeleteStatus.Failed) { + Error = new Exception("Failed to delete room: " + DeleteStatus.ToJson()); + return Executing = false; + } + + await Task.Delay(1000); + } + catch { } + } + + StateHasChanged(); + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + + private async Task<bool> TryRestoreRoom() { + Stage = 3; + try { + await Homeserver.Admin.BlockRoom(RoomId, block: false); + Members = Random.Shared.GetItems(Members.ToArray(), Members.Count).ToList(); + StateHasChanged(); + foreach (var member in Members) { + while (true) { + try { + var hs = member.StateKey == Homeserver.WhoAmI.UserId + ? Homeserver + : await Homeserver.Admin.GetHomeserverForUserAsync(member.StateKey!, TimeSpan.FromMinutes(120)); + await hs.GetRoom(RoomId).JoinAsync([Via], reason: "Reconciling state with " + Via, false); + await hs.GetRoom(RoomId).SendStateEventAsync(RoomMemberEventContent.EventId, member.StateKey, member.RawContent); + Members = Members.Skip(1).ToList(); + StateHasChanged(); + break; + } + catch (Exception e) { + Error = new Exception($"{DateTime.Now:u} Failed to join room: {member.StateKey}, retrying\n", e); + } + } + } + + return true; + } + catch (Exception e) { + Error = e; + return Executing = false; + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor new file mode 100644
index 0000000..54ac800 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor
@@ -0,0 +1,243 @@ +@page "/HSAdmin/Synapse/UserQuery" +@using Microsoft.AspNetCore.WebUtilities +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Homeservers.Extensions.NamedCaches +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters +@using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components +@using MatrixUtils.Web.Pages.HSAdmin.Synapse.Components.RoomQuery +@inject ILogger<RoomQuery> Logger + +<h3>Homeserver Administration - User 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> + @* <SynapseRoomQueryFilter Filter="@Filter"/> *@ +</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 user in Results) { + <div class="room-list-item"> + <p> + <span>@user.Name</span> + @if (!string.IsNullOrWhiteSpace(user.DisplayName)) { + <span> (@user.DisplayName)</span> + } + <br/> + </p> + <p> + <LinkButton OnClickAsync="@(() => Login(user))">Log in</LinkButton> + @* <LinkButton OnClickAsync="@(() => DeleteRoom(user))">Delete room</LinkButton> *@ + @* <LinkButton target="_blank" href="@($"/HSAdmin/Synapse/ResyncState?roomId={user.RoomId}&via={user.RoomId.Split(':', 2)[1]}")">Resync state</LinkButton> *@ + + </p> + + @{ + List<string?> flags = []; + if (user.IsGuest == true) flags.Add("guest"); + if (user.Admin == true) flags.Add("admin"); + if (user.Deactivated == true) flags.Add("deactivated"); + if (user.Erased == true) flags.Add("erased"); + if (user.ShadowBanned == true) flags.Add("shadow banned"); + if (user.Locked == true) flags.Add("locked"); + if (user.Approved == true) flags.Add("approved"); + + if (!string.IsNullOrWhiteSpace(user.UserType)) flags.Add($"type=\"{user.UserType}\""); + + flags = flags.Where(x => x != null).ToList(); + } + <span>@string.Join(", ", flags)</span> + <br/> + + <details> + <summary>Full result data</summary> + <pre>@user.ToJson(ignoreNull: true)</pre> + </details> + </div> +} + +@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; } = true; + + private List<SynapseAdminUserListResult.SynapseAdminUserListResultUser> Results { get; set; } = new(); + + private AuthenticatedHomeserverSynapse Homeserver { get; set; } = null!; + + private SynapseAdminLocalUserQueryFilter Filter { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is not AuthenticatedHomeserverSynapse synapse) { + NavigationManager.NavigateTo("/"); + return; + } + + Homeserver = synapse; + StateHasChanged(); + } + + protected override Task OnParametersSetAsync() { + 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; + } + + private async Task Search() { + Results.Clear(); + var searchRooms = Homeserver.Admin.SearchUsersAsync(orderBy: OrderBy!, dir: Ascending ? "f" : "b", localFilter: Filter).GetAsyncEnumerator(); + while (await searchRooms.MoveNextAsync()) { + var room = searchRooms.Current; + + Results.Add(room); + + if ((Results.Count <= 200 && Results.Count % 10 == 0) || Results.Count % 1000 == 0) { + StateHasChanged(); + await Task.Yield(); + await Task.Delay(1); + } + } + + StateHasChanged(); + + StateHasChanged(); + } + + private readonly Dictionary<string, string> validOrderBy = new() { + { "name", "User name" }, + { "is_guest", "Guest status" }, + { "admin", "Admin status" }, + { "user_type", "User type" }, + { "deactivated", "Deactivation status" }, + { "shadow_banned", "Shadow banned status" }, + { "displayname", "Display name" }, + { "avatar_url", "Avatar URL" }, + { "creation_ts", "Creation time" }, + { "last_seen_ts", "Last activity" }, + }; + + private async Task Login(SynapseAdminUserListResult.SynapseAdminUserListResultUser user) { + var loginResult = await Homeserver.Admin.LoginUserAsync(user.Name, TimeSpan.FromDays(1)); + await sessionStore.AddSession(new() { + AccessToken = loginResult.AccessToken, + DeviceId = loginResult.DeviceId, + UserId = loginResult.UserId, + Homeserver = Homeserver.ServerName, + Proxy = Homeserver.Proxy + }); + + } + +} diff --git a/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css new file mode 100644
index 0000000..62941e5 --- /dev/null +++ b/MatrixUtils.Web/Pages/HSAdmin/Synapse/UserList.razor.css
@@ -0,0 +1,7 @@ +.room-list-item { + background-color: #ffffff11; + border-radius: 0.5em; + display: block; + margin-top: 4px; + padding: 4px; +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index 7de4613..82ee0f2 100644 --- a/MatrixUtils.Web/Pages/Index.razor +++ b/MatrixUtils.Web/Pages/Index.razor
@@ -4,6 +4,7 @@ @using LibMatrix @using ArcaneLibs @using System.Diagnostics +@using LibMatrix.Responses.Federation <PageTitle>Index</PageTitle> @@ -19,7 +20,7 @@ Small collection of tools to do not-so-everyday things. </span> } <hr/> -<form> +<form aria-busy="@Busy"> <table> @foreach (var session in _sessions.OrderByDescending(x => x.UserInfo.RoomCount)) { var auth = session.Auth; @@ -58,9 +59,9 @@ Small collection of tools to do not-so-everyday things. </td> <td> <p> - <LinkButton OnClick="@(() => ManageUser(session.SessionId))">Manage</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId, true))">Log out</LinkButton> + <LinkButton OnClickAsync="@(() => ManageUser(session.SessionId))">Manage</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId, true))">Log out</LinkButton> </p> </td> </tr> @@ -89,7 +90,7 @@ Small collection of tools to do not-so-everyday things. </p> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -118,10 +119,10 @@ Small collection of tools to do not-so-everyday things. </p> </td> <td> - <LinkButton OnClick="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton> + <LinkButton OnClickAsync="@(() => Task.Run(() => NavigationManager.NavigateTo($"/InvalidSession?ctx={session.SessionId}")))">Re-login</LinkButton> </td> <td> - <LinkButton OnClick="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => RemoveUser(session.SessionId))">Remove</LinkButton> </td> </tr> } @@ -137,6 +138,8 @@ Small collection of tools to do not-so-everyday things. private const bool _debug = false; #endif + private bool Busy { get; set; } = true; + private class HomepageSessionInfo : RmuSessionStore.SessionInfo { public UserInfo? UserInfo { get; set; } public ServerVersionResponse? ServerVersion { get; set; } @@ -153,7 +156,6 @@ Small collection of tools to do not-so-everyday things. protected override async Task OnInitializedAsync() { Console.WriteLine("Index.OnInitializedAsync"); logger.LogDebug("Initialising index page"); - await sessionStore.RunMigrations(); _currentSession = await sessionStore.GetCurrentSession(); _sessions.Clear(); @@ -242,7 +244,9 @@ Small collection of tools to do not-so-everyday things. await Task.WhenAll(tasks); scannedSessions = totalSessions; - await base.OnInitializedAsync(); + Busy = false; + StateHasChanged(); + Console.WriteLine("Index.OnInitializedAsync finished"); } private class UserInfo { diff --git a/MatrixUtils.Web/Pages/InvalidSession.razor b/MatrixUtils.Web/Pages/InvalidSession.razor
index 1ec99f6..f86d112 100644 --- a/MatrixUtils.Web/Pages/InvalidSession.razor +++ b/MatrixUtils.Web/Pages/InvalidSession.razor
@@ -8,14 +8,14 @@ @if (_auth is not null) { <p>It appears that the affected user is @_auth.UserId (@_auth.DeviceId) on @_auth.Homeserver!</p> - <LinkButton OnClick="@(OpenRefreshDialog)">Refresh token</LinkButton> - <LinkButton OnClick="@(RemoveUser)">Remove</LinkButton> + <LinkButton OnClickAsync="@(OpenRefreshDialog)">Refresh token</LinkButton> + <LinkButton OnClickAsync="@(RemoveUser)">Remove</LinkButton> @if (_showRefreshDialog) { <ModalWindow MinWidth="300" X="275" Y="300" Title="@($"Password for {_auth.UserId}")"> <FancyTextBox IsPassword="true" @bind-Value="@_password"></FancyTextBox> <br/> - <LinkButton OnClick="TryLogin">Log in</LinkButton> + <LinkButton OnClickAsync="TryLogin">Log in</LinkButton> @if (_loginException is not null) { <pre style="color: red;">@_loginException.RawContent</pre> } diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
index 8831dd1..56c8cfe 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientRoomList.razor
@@ -1,7 +1,7 @@ @using ClientContext = MatrixUtils.Web.Pages.Labs.Client.Index.ClientContext @* user header and room list *@ @foreach (var room in Data.SyncWrapper.Rooms) { - <LinkButton OnClick="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")"> + <LinkButton OnClickAsync="@(async () => Data.SelectedRoom = room)" Color="@(Data.SelectedRoom == room ? "#FF00FF" : "")"> @room.RoomName </LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
index 16051b8..c58114e 100644 --- a/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs +++ b/MatrixUtils.Web/Pages/Labs/Client/ClientComponents/ClientSyncWrapper.cs
@@ -13,9 +13,10 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti MinimumDelay = TimeSpan.FromMilliseconds(2000), IsInitialSync = false }; + private string _status = "Loading..."; - public ObservableCollection<StateEvent> AccountData { get; set; } = new(); + public ObservableCollection<MatrixEvent> AccountData { get; set; } = new(); public ObservableCollection<RoomInfo> Rooms { get; set; } = new(); public string Status { @@ -29,13 +30,12 @@ public class ClientSyncWrapper(AuthenticatedHomeserverGeneric homeserver) : Noti Status = $"[{DateTime.Now:s}] Syncing..."; await foreach (var response in resp) { Task.Yield(); - Status = $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates"; + Status = + $"[{DateTime.Now:s}] {response.Rooms?.Join?.Count ?? 0 + response.Rooms?.Invite?.Count ?? 0 + response.Rooms?.Leave?.Count ?? 0} rooms, {response.AccountData?.Events?.Count ?? 0} account data, {response.ToDevice?.Events?.Count ?? 0} to-device, {response.DeviceLists?.Changed?.Count ?? 0} device lists, {response.Presence?.Events?.Count ?? 0} presence updates"; await HandleSyncResponse(response); await Task.Yield(); } } - private async Task HandleSyncResponse(SyncResponse resp) { - - } + private async Task HandleSyncResponse(SyncResponse resp) { } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
index a974a8f..7199934 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage1.razor
@@ -26,10 +26,10 @@ <InputCheckbox @bind-Value="SetupData.DmSpaceInfo.LayerByUser"></InputCheckbox> Create sub-spaces per user </p> - + <br/> - <LinkButton OnClick="@Disband" Color="#FF0000">Disband</LinkButton> - <LinkButton OnClick="@Execute">Next</LinkButton> + <LinkButton OnClickAsync="@Disband" Color="#FF0000">Disband</LinkButton> + <LinkButton OnClickAsync="@Execute">Next</LinkButton> } else { <p>Discovering spaces, please wait...</p> @@ -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 b8eb257..ed65e94 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage2.razor
@@ -31,17 +31,19 @@ else { } <br/> -<LinkButton OnClick="@Execute">Next</LinkButton> +<LinkButton OnClickAsync="@Execute">Next</LinkButton> @{ var _offset = 0; } @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 OnClick="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> + <LinkButton OnClickAsync="@(() => SetRoomAssignment(room.Room.RoomId, userProfileResponse.Id))"> <span>Assign to </span> <InlineUserItem User="userProfileResponse"></InlineUserItem> </LinkButton> @@ -54,7 +56,7 @@ else { <ModalWindow Title="Re-assign DM" OnCloseClicked="@(() => DmToReassign = null)"> <RoomListItem RoomInfo="@DmToReassign"></RoomListItem> @foreach (var userProfileResponse in roomMembers[DmToReassign]) { - <LinkButton OnClick="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> + <LinkButton OnClickAsync="@(() => SetRoomAssignment(DmToReassign.Room.RoomId, userProfileResponse.Id))"> <span>Assign to </span> <InlineUserItem User="userProfileResponse"></InlineUserItem> </LinkButton> @@ -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,18 +183,18 @@ 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(); } catch { } - var membersEnum = room.GetMembersEnumerableAsync(true); + var membersEnum = room.GetMembersEnumerableAsync("join"); 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 dac9c49..686894c 100644 --- a/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor +++ b/MatrixUtils.Web/Pages/Labs/DMSpace/DMSpaceStages/DMSpaceStage3.razor
@@ -59,7 +59,7 @@ else { } <br/> -<LinkButton OnClick="@Execute">Next</LinkButton> +<LinkButton OnClickAsync="@Execute">Next</LinkButton> @code { @@ -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, @@ -150,7 +150,7 @@ else { } catch { } - var membersEnum = room.GetMembersEnumerableAsync(true); + var membersEnum = room.GetMembersEnumerableAsync("join"); await foreach (var member in membersEnum) if (member.TypedContent is RoomMemberEventContent memberEvent) roomMembers.Add(new() { DisplayName = memberEvent.DisplayName, AvatarUrl = memberEvent.AvatarUrl, Id = member.StateKey }); diff --git a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
index 79f931b..dd217e9 100644 --- a/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor +++ b/MatrixUtils.Web/Pages/Labs/Rooms2/Index2Components/RoomsIndex2ByRoomTypeTab.razor
@@ -36,7 +36,6 @@ } //debounce StateHasChanged, we dont want to reredner on every key stroke - private CancellationTokenSource _debounceCts = new CancellationTokenSource(); private async Task DebouncedStateHasChanged() { diff --git a/MatrixUtils.Web/Pages/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 88577a2..38ede74 100644 --- a/MatrixUtils.Web/Pages/LoginPage.razor +++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -22,8 +22,8 @@ <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox> </span> <br/> -<LinkButton OnClick="@AddRecord">Add account to queue</LinkButton> -<LinkButton OnClick="@(() => Login(newRecordInput))">Log in</LinkButton> +<LinkButton OnClickAsync="@AddRecord">Add account to queue</LinkButton> +<LinkButton OnClickAsync="@(() => Login(newRecordInput))">Log in</LinkButton> <br/> <br/> @@ -44,7 +44,7 @@ <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox> </span> <br/> -<LinkButton OnClick="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton> +<LinkButton OnClickAsync="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton> <br/> <br/> @@ -101,7 +101,7 @@ } </table> <br/> -<LinkButton OnClick="@LoginAll">Log in</LinkButton> +<LinkButton OnClickAsync="@LoginAll">Log in</LinkButton> @code { 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/Create.razor b/MatrixUtils.Web/Pages/Rooms/Create.razor
index 021ad18..051d5af 100644 --- a/MatrixUtils.Web/Pages/Rooms/Create.razor +++ b/MatrixUtils.Web/Pages/Rooms/Create.razor
@@ -12,10 +12,10 @@ @* <pre Contenteditable="true" @onkeypress="@JsonChanged" content="JsonString">@JsonString</pre> *@ <style> - table.table-top-first-tr tr td:first-child { - vertical-align: top; - } - </style> + table.table-top-first-tr tr td:first-child { + vertical-align: top; + } +</style> <table class="table-top-first-tr"> <tr style="padding-bottom: 16px;"> <td>Preset:</td> @@ -41,7 +41,10 @@ } else { <FancyTextBox @bind-Value="@creationEvent.Name"></FancyTextBox> - <p>(#<FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox>:@Homeserver.WhoAmI.UserId.Split(':').Last())</p> + <p>(# + <FancyTextBox @bind-Value="@creationEvent.RoomAliasName"></FancyTextBox> + :@Homeserver.WhoAmI.UserId.Split(':').Last()) + </p> } </td> </tr> @@ -89,7 +92,8 @@ <td> @* <img src="@Homeserver.ResolveMediaUri(roomAvatarEvent.Url)" style="width: 128px; height: 128px; border-radius: 50%;"/> *@ <div style="display: inline-block; vertical-align: middle;"> - <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox><br/> + <FancyTextBox @bind-Value="@roomAvatarEvent.Url"></FancyTextBox> + <br/> <InputFile OnChange="RoomIconFilePicked"></InputFile> </div> </td> @@ -105,19 +109,27 @@ <FancyTextBox Formatter="@GetPermissionFriendlyName" Value="@_event" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Events.ChangeKey(_event, val); }"> - </FancyTextBox>: + </FancyTextBox> + : </td> <td> - <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Events[_event]" + @oninput="val => { creationEvent.PowerLevelContentOverride.Events[_event] = int.Parse(val.Value.ToString()); }" + @onfocusout="() => { creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"/> </td> </tr> } @foreach (var user in creationEvent.PowerLevelContentOverride.Users.Keys) { var _user = user; <tr> - <td><FancyTextBox Value="@_user" ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox>:</td> <td> - <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> + <FancyTextBox Value="@_user" + ValueChanged="val => { creationEvent.PowerLevelContentOverride.Users.ChangeKey(_user, val); creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); }"></FancyTextBox> + : + </td> + <td> + <input type="number" value="@creationEvent.PowerLevelContentOverride.Users[_user]" + @oninput="val => { creationEvent.PowerLevelContentOverride.Users[_user] = int.Parse(val.Value.ToString()); }"/> </td> </tr> } @@ -266,6 +278,7 @@ var instance = (IRoomCreationTemplate)Activator.CreateInstance(x); Presets[instance.Name] = instance.CreateRoomRequest; } + Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); if (!Presets.ContainsKey("Default")) { @@ -299,7 +312,7 @@ private void InviteMember(string mxid) { if (!creationEvent.InitialState.Any(x => x.Type == "m.room.member" && x.StateKey == mxid) && Homeserver.UserId != mxid) - creationEvent.InitialState.Add(new StateEvent { + creationEvent.InitialState.Add(new MatrixEvent { Type = "m.room.member", StateKey = mxid, TypedContent = new RoomMemberEventContent { @@ -316,7 +329,7 @@ "m.room.server_acl" => "Server ACL", "m.room.avatar" => "Avatar", _ => key - }; + }; private string GetPermissionFriendlyName(string key) => key switch { "m.reaction" => "Send reaction", @@ -331,6 +344,6 @@ "m.room.pinned_events" => "Pin events", "m.room.server_acl" => "Change server ACLs", _ => key - }; + }; } diff --git a/MatrixUtils.Web/Pages/Rooms/Create2.razor b/MatrixUtils.Web/Pages/Rooms/Create2.razor new file mode 100644
index 0000000..4a29847 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/Create2.razor
@@ -0,0 +1,147 @@ +@page "/Rooms/Create2" +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.Helpers +@using LibMatrix.Responses +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Pages.Rooms.RoomCreateComponents +@inject ILogger<Create2> logger +@* @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not */ *@ + +<h3>Room Manager - Create Room</h3> + +@if (Ready) { + <style> + table.table-top-first-tr tr td:first-child { + vertical-align: top; + } + </style> + <table class="table-top-first-tr"> + @if (roomBuilder is RoomUpgradeBuilder roomUpgrade) { + <RoomCreateUpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@StateHasChanged" OldRoom="@PreviousRoom" /> + } + else { + @* <tr style="padding-bottom: 16px;"> *@ + @* <td>Preset:</td> *@ + @* <td> *@ + @* @if (Presets is null) { *@ + @* <p style="color: red;">Presets is null!</p> *@ + @* } *@ + @* else { *@ + @* <p style="color: red;">Support for presets is currently disabled!</p> *@ + @* $1$ <InputSelect @bind-Value="@RoomPreset"> #1# *@ + @* $1$ @foreach (var createRoomRequest in Presets) { #1# *@ + @* $1$ <option value="@createRoomRequest.Key">@createRoomRequest.Key</option> #1# *@ + @* $1$ } #1# *@ + @* $1$ </InputSelect> #1# *@ + @* } *@ + @* </td> *@ + @* </tr> *@ + } + <RoomCreateBasicRoomInfoOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreateCreateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreatePrivacyOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreatePermissionsOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + <RoomCreateMembershipOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + @* Initial states, should remain at bottom *@ + <RoomCreateInitialStateOptions roomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged" Homeserver="@Homeserver"/> + </table> + <LinkButton OnClickAsync="@CreateRoom">Create room</LinkButton> +} + +<RoomCreateStateDisplay @bind-RoomBuilder="@roomBuilder" PageStateHasChanged="@StateHasChanged"/> + +@if (_matrixException is not null) { + <ModalWindow Title="@("Matrix exception: " + _matrixException.ErrorCode)"> + <pre> + @_matrixException.Message + </pre> + </ModalWindow> +} + +@code { + +#region State + + [Parameter, SupplyParameterFromQuery(Name = "previousRoomId")] + public string? PreviousRoomId { get; set; } + + public GenericRoom? PreviousRoom { get; set; } + + private bool Ready { get; set; } + + private RoomBuilder roomBuilder { get; set; } = new(); + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + + private MatrixException? _matrixException { get; set; } + +#endregion + +#region Presets + + private Dictionary<string, CreateRoomRequest>? Presets { get; set; } = new(); + // private string RoomPreset { + // get => Presets.ContainsValue(roomBuilder) ? Presets.First(x => x.Value == roomBuilder).Key : "Not a preset"; + // set { + // roomBuilder = Presets[value]; + // JsonChanged(); + // StateHasChanged(); + // } + // } + +#endregion + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (Homeserver is null) return; + if (!string.IsNullOrWhiteSpace(PreviousRoomId)) { + roomBuilder = new RoomUpgradeBuilder(); + PreviousRoom = Homeserver.GetRoom(PreviousRoomId); + } + + roomBuilder.ServerAcls.Allow = ["*"]; + roomBuilder.ServerAcls.Deny = []; + + // foreach (var x in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Contains(typeof(IRoomCreationTemplate))).ToList()) { + // Console.WriteLine($"Found room creation template in class: {x.FullName}"); + // var instance = (IRoomCreationTemplate)Activator.CreateInstance(x); + // Presets[instance.Name] = instance.CreateRoomRequest; + // } + // + // Presets = Presets.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); + + // if (!Presets.ContainsKey("Default")) { + // Console.WriteLine($"No default room found in {Presets.Count} presets: {string.Join(", ", Presets.Keys)}"); + // } + // else RoomPreset = "Default"; + + Ready = true; + StateHasChanged(); + if (roomBuilder is RoomUpgradeBuilder roomUpgrade) { + // await roomUpgrade.ImportAsync().ConfigureAwait(false); + StateHasChanged(); + } + } + + protected override bool ShouldRender() { + if (roomBuilder.Type == "") + roomBuilder.Type = null; // Reset to null if empty, so it doesn't get sent as an empty string + var result = base.ShouldRender(); + logger.LogInformation("ShouldRender: " + result); + return result; + } + + private async Task CreateRoom() { + Console.WriteLine("Create room"); + Console.WriteLine(roomBuilder.ToJson()); + roomBuilder.AdditionalCreationContent["gay.rory.created_using"] = "Rory&::MatrixUtils (https://mru.rory.gay)"; + try { + var newRoom = await roomBuilder.Create(Homeserver); + } + catch (MatrixException e) { + _matrixException = e; + } + } + +} diff --git a/MatrixUtils.Web/Pages/Rooms/Index.razor b/MatrixUtils.Web/Pages/Rooms/Index.razor
index 0373a46..115c903 100644 --- a/MatrixUtils.Web/Pages/Rooms/Index.razor +++ b/MatrixUtils.Web/Pages/Rooms/Index.razor
@@ -73,7 +73,7 @@ GlobalProfile = await Homeserver.GetProfileAsync(Homeserver.WhoAmI.UserId); var filter = await Homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetBasicRoomInfo); - var filterData = await Homeserver.GetFilterAsync(filter); + // var filterData = await Homeserver.GetFilterAsync(filter); // Rooms = new ObservableCollection<RoomInfo>(rooms.Select(room => new RoomInfo(room))); // foreach (var stateType in filterData.Room?.State?.Types ?? []) { @@ -97,7 +97,8 @@ syncHelper = new SyncHelper(Homeserver, logger) { Timeout = 30000, FilterId = filter, - MinimumDelay = TimeSpan.FromMilliseconds(5000) + MinimumDelay = TimeSpan.FromMilliseconds(5000), + UseMsc4222StateAfter = true }; // profileSyncHelper = new SyncHelper(Homeserver, logger) { // Timeout = 10000, @@ -106,9 +107,9 @@ // }; // profileUpdateFilter.Room.State.Senders.Add(Homeserver.WhoAmI.UserId); - RunSyncLoop(syncHelper); + _ = RunSyncLoop(syncHelper); // RunSyncLoop(profileSyncHelper); - RunQueueProcessor(); + _ = RunQueueProcessor(); await base.OnInitializedAsync(); } @@ -138,7 +139,7 @@ } else { // Console.WriteLine($"QueueWorker: encountered new room {roomId}!"); - room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.State?.Events); + room = new RoomInfo(Homeserver.GetRoom(roomId), roomData.StateAfter?.Events); Rooms.Add(room); } @@ -147,11 +148,10 @@ throw new InvalidDataException("Somehow this is null???"); } - if (roomData.State?.Events is { Count: > 0 }) - room.StateEvents.MergeStateEventLists(roomData.State.Events); - else { + if (roomData is { StateAfter.Events.Count: > 0 }) + room.StateEvents!.MergeStateEventLists(roomData.StateAfter.Events); + else Console.WriteLine($"QueueWorker: could not merge state for {room.Room.RoomId} as new data contains no state events!"); - } if (maxUpdates % 100 == 0) { Console.WriteLine($"QueueWorker: {queue.Count} entries left in queue, {maxUpdates} maxUpdates left, RenderContents: {RenderContents}"); @@ -181,7 +181,8 @@ get => _status; set { _status = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -191,7 +192,8 @@ get => _status2; set { _status2 = value; - StateHasChanged(); + // StateHasChanged(); + Console.WriteLine(value); } } @@ -203,26 +205,24 @@ var syncs = syncHelper.EnumerateSyncAsync(); await foreach (var sync in syncs) { - Console.WriteLine("trying sync"); - if (sync is null) continue; - var filter = await Homeserver.GetFilterAsync(syncHelper.FilterId); Status = $"Got sync with {sync.Rooms?.Join?.Count ?? 0} room updates, next batch: {sync.NextBatch}!"; - if (sync?.Rooms?.Join != null) + if (sync.Rooms?.Join != null) foreach (var joinedRoom in sync.Rooms.Join) - if ( /*joinedRoom.Value.AccountData?.Events?.Count > 0 ||*/ joinedRoom.Value.State?.Events?.Count > 0) { - joinedRoom.Value.State.Events.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI?.UserId); + if (joinedRoom.Value.StateAfter?.Events?.Count > 0) { + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => x.Type == "m.room.member" && x.StateKey != Homeserver.WhoAmI.UserId); // We can't trust servers to give us what we ask for, and this ruins performance // Thanks, Conduit. - joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false); - if (filter.Room?.State?.NotSenders?.Any() ?? false) - joinedRoom.Value.State.Events.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender) ?? false); + if (filter is { Room.State.Types.Count: > 0 }) + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.Types?.Contains(x.Type) == false); + if (filter is { Room.State.NotSenders.Count: > 0 }) + joinedRoom.Value.StateAfter?.Events?.RemoveAll(x => filter.Room?.State?.NotSenders?.Contains(x.Sender!) ?? false); queue.Enqueue(joinedRoom); } - if (sync.Rooms.Leave is { Count: > 0 }) + if (sync.Rooms?.Leave is { Count: > 0 }) foreach (var leftRoom in sync.Rooms.Leave) if (Rooms.Any(x => x.Room.RoomId == leftRoom.Key)) Rooms.Remove(Rooms.First(x => x.Room.RoomId == leftRoom.Key)); @@ -231,6 +231,7 @@ $"{sync.Rooms?.Join?.Count ?? 0} new updates!"; Status2 = $"Next batch: {sync?.NextBatch}"; + StateHasChanged(); await Task.Yield(); } } diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index 9c35673..f2ab186 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -5,159 +5,148 @@ @using System.Diagnostics @using LibMatrix.RoomTypes @using System.Collections.Frozen +@using System.Collections.Immutable @using System.Reflection +@using System.Text.Json @using ArcaneLibs.Attributes +@using ArcaneLibs.Blazor.Components.Services @using LibMatrix.EventTypes -@using LibMatrix.EventTypes.Common @using LibMatrix.EventTypes.Interop.Draupnir @using LibMatrix.EventTypes.Spec.State.RoomInfo - -@using MatrixUtils.Web.Shared.PolicyEditorComponents @using SpawnDev.BlazorJS.WebWorkers +@using MatrixUtils.Web.Pages.Rooms.PolicyListComponents +@using SpawnDev.BlazorJS @inject WebWorkerService WebWorkerService +@inject ILogger<PolicyList> logger +@inject BlazorJSRuntime JsRuntime -<h3>Policy list editor - Editing @(RoomName ?? RoomId)</h3> -@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) { - <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span> -} -@if (!string.IsNullOrWhiteSpace(RoomAlias)) { - <span>Alias: @RoomAlias</span> -} -<hr/> -@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ -<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> -<LinkButton OnClick="@(() => { MassCreatePolicies = true; return Task.CompletedTask; })">Create many new policies</LinkButton> - -@if (Loading) { - <p>Loading...</p> -} -else if (PolicyEventsByType is not { Count: > 0 }) { - <p>No policies yet</p> +@if (!IsInitialised) { + <p>Connecting to homeserver...</p> } else { - var renderSw = Stopwatch.StartNew(); - var renderTotalSw = Stopwatch.StartNew(); - @foreach (var (type, value) in PolicyEventsByType) { - <p> - @(GetValidPolicyEventsByType(type).Count) active, - @(GetInvalidPolicyEventsByType(type).Count) invalid - (@value.Count total) - @(GetPolicyTypeName(type).ToLower()) - </p> + <PolicyListEditorHeader Room="@Room" @bind-RenderEventInfo="@RenderEventInfo" ReloadStateAsync="@(() => LoadStateAsync(true))"></PolicyListEditorHeader> + @if (Loading) { + <p>Loading...</p> } + // else if (PolicyEventsByType is not { Count: > 0 }) { + @* <p>No policies yet</p> *@ + // } + else { + var renderSw = Stopwatch.StartNew(); + var renderTotalSw = Stopwatch.StartNew(); + @foreach (var value in PolicyCollections.Values.OrderByDescending(x => x.TotalCount)) { + <p> + @value.ActivePolicies.Count active, + @value.RemovedPolicies.Count removed + (@value.TotalCount total) + @value.Name.ToLower() + </p> + } - Console.WriteLine($"Rendered hearder in {renderSw.GetElapsedAndRestart()}"); + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @DuplicateBans.Value.ActivePolicies.Count duplicate bans + </p> + } - @foreach (var type in KnownPolicyTypes.OrderByDescending(t => GetPolicyEventsByType(t).Count)) { - <details> - <summary> - <span> - @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") - </span> - <hr style="margin: revert;"/> - </summary> - <table class="table table-striped table-hover"> - @{ - var policies = GetValidPolicyEventsByType(type); - var invalidPolicies = GetInvalidPolicyEventsByType(type); - // enumerate all properties with friendly name - var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) - .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) - .ToFrozenSet(); - var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); + @if (RedundantBans?.ActivePolicies.Count > 0) { + <p style="color: orange;"> + Found @RedundantBans.Value.ActivePolicies.Count redundant bans + </p> + } - var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => props.Any(y => y.Name == x.Name)) - .ToFrozenSet(); - Console.WriteLine($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); - } - <thead> - <tr> - @foreach (var name in propNames) { - <th>@name</th> - } - <th>Actions</th> - </tr> - </thead> - <tbody style="border-width: 1px;"> - @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { - <tr> - @{ - var typedContent = policy.TypedContent!; - } - @foreach (var prop in proxySafeProps ?? Enumerable.Empty<PropertyInfo>()) { - <td>@prop.GetGetMethod()?.Invoke(typedContent, null)</td> - } - <td> - <div style="display: ruby;"> - @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton> - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> - @if (policy.IsLegacyType) { - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> - } + // logger.LogInformation($"Rendered header in {renderSw.GetElapsedAndRestart()}"); - @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.Type)) { - <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent</LinkButton> - @if (CurrentUserIsDraupnir) { - <LinkButton Color="@(ActiveKicks.ContainsKey(policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(policy))">Kick users @(ActiveKicks.ContainsKey(policy) ? $"({ActiveKicks[policy]})" : null)</LinkButton> - } - } - } - else { - <p>No permission to modify</p> - } - </div> - </td> - </tr> - } - </tbody> - </table> - <details> - <summary> - <u> - @("Invalid " + GetPolicyTypeName(type).ToLower()) - </u> - </summary> - <table class="table table-striped table-hover"> - <thead> - <tr> - <th>State key</th> - <th>Json contents</th> - </tr> - </thead> - <tbody> - @foreach (var policy in invalidPolicies) { - <tr> - <td>@policy.StateKey</td> - <td> - <pre>@policy.RawContent.ToJson(true, false)</pre> - </td> - </tr> - } - </tbody> - </table> - </details> - </details> - } + // var renderSw2 = Stopwatch.StartNew(); + // IOrderedEnumerable<Type> policiesByType = KnownPolicyTypes.Where(t => GetPolicyEventsByType(t).Count > 0).OrderByDescending(t => GetPolicyEventsByType(t).Count); + // logger.LogInformation($"Ordered policy types by count in {renderSw2.GetElapsedAndRestart()}"); - Console.WriteLine($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); - Console.WriteLine($"Rendered in {renderTotalSw.Elapsed}"); -} + @if (DuplicateBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@DuplicateBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } -@if (CurrentlyEditingEvent is not null) { - <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSave="@(e => UpdatePolicyAsync(e))"></PolicyEditorModal> -} + @if (RedundantBans?.ActivePolicies.Count > 0) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@RedundantBans.Value" + Room="@Room"></PolicyListCategoryComponent> + } -@if (ServerPolicyToMakePermanent is not null) { - <ModalWindow Title="Make policy permanent"> + foreach (var collection in PolicyCollections.Values.OrderByDescending(x => x.ActivePolicies.Count)) { + <PolicyListCategoryComponent RenderInvalidSection="false" RenderEventInfo="@RenderEventInfo" PolicyCollection="@collection" Room="@Room"></PolicyListCategoryComponent> + } - </ModalWindow> -} + // foreach (var type in policiesByType) { + @* foreach (var type in (List<Type>) []) { *@ + @* <details> *@ + @* <summary> *@ + @* <span> *@ + @* @($"{GetPolicyTypeName(type)}: {GetPolicyEventsByType(type).Count} policies") *@ + @* </span> *@ + @* <hr style="margin: revert;"/> *@ + @* </summary> *@ + @* <table class="table table-striped table-hover table-bordered align-middle"> *@ + @* @{ *@ + @* var renderSw3 = Stopwatch.StartNew(); *@ + @* var policies = GetValidPolicyEventsByType(type); *@ + @* var invalidPolicies = GetInvalidPolicyEventsByType(type); *@ + @* // enumerate all properties with friendly name *@ + @* var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@ + @* .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) *@ + @* .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) *@ + @* .ToFrozenSet(); *@ + @* var propNames = props.Select(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName()!).ToFrozenSet(); *@ + @* *@ + @* var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) *@ + @* .Where(x => props.Any(y => y.Name == x.Name)) *@ + @* .ToFrozenSet(); *@ + @* logger.LogInformation($"{proxySafeProps?.Count} proxy safe props found in {policies.FirstOrDefault()?.TypedContent?.GetType()}"); *@ + @* logger.LogInformation($"Filtered policies and got properties in {renderSw3.GetElapsedAndRestart()}"); *@ + @* } *@ + @* <thead> *@ + @* <tr> *@ + @* @foreach (var name in propNames) { *@ + @* <th>@name</th> *@ + @* } *@ + @* <th>Actions</th> *@ + @* </tr> *@ + @* </thead> *@ + @* <tbody> *@ + @* @foreach (var policy in policies.OrderBy(x => x.RawContent?["entity"]?.GetValue<string>())) { *@ + @* <PolicyListRowComponent PolicyInfo="@policy" Room="@Room"></PolicyListRowComponent> *@ + @* } *@ + @* </tbody> *@ + @* </table> *@ + @* <details> *@ + @* <summary> *@ + @* <u> *@ + @* @("Invalid " + GetPolicyTypeName(type).ToLower()) *@ + @* </u> *@ + @* </summary> *@ + @* <table class="table table-striped table-hover"> *@ + @* <thead> *@ + @* <tr> *@ + @* <th>State key</th> *@ + @* <th>Json contents</th> *@ + @* </tr> *@ + @* </thead> *@ + @* <tbody> *@ + @* @foreach (var policy in invalidPolicies) { *@ + @* <tr> *@ + @* <td>@policy.StateKey</td> *@ + @* <td> *@ + @* <pre>@policy.RawContent.ToJson(true, false)</pre> *@ + @* </td> *@ + @* </tr> *@ + @* } *@ + @* </tbody> *@ + @* </table> *@ + @* </details> *@ + @* </details> *@ + // } -@if (MassCreatePolicies) { - <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { MassCreatePolicies = false; LoadStatesAsync(); })"></MassPolicyEditorModal> + // logger.LogInformation($"Rendered policies in {renderSw.GetElapsedAndRestart()}"); + logger.LogInformation("Rendered in {TimeSpan}", renderTotalSw.Elapsed); + } } @code { @@ -168,50 +157,50 @@ else { private const bool Debug = false; #endif + private bool IsInitialised { get; set; } = false; private bool Loading { get; set; } = true; [Parameter] - public string RoomId { get; set; } + public required string RoomId { get; set; } - private bool _enableAvatars; - private StateEventResponse? _currentlyEditingEvent; - private bool _massCreatePolicies; - private StateEventResponse? _serverPolicyToMakePermanent; - - private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); - - private StateEventResponse? CurrentlyEditingEvent { - get => _currentlyEditingEvent; + [Parameter, SupplyParameterFromQuery] + public bool RenderEventInfo { + get; set { - _currentlyEditingEvent = value; + field = value; StateHasChanged(); } } - public StateEventResponse? ServerPolicyToMakePermanent { - get => _serverPolicyToMakePermanent; + private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new(); + + public MatrixEventResponse? ServerPolicyToMakePermanent { + get; set { - _serverPolicyToMakePermanent = value; + field = value; StateHasChanged(); } } - private AuthenticatedHomeserverGeneric Homeserver { get; set; } - private GenericRoom Room { get; set; } - private RoomPowerLevelEventContent PowerLevels { get; set; } + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private GenericRoom Room { get; set; } = null!; + private RoomPowerLevelEventContent PowerLevels { get; set; } = null!; public bool CurrentUserIsDraupnir { get; set; } - public string? RoomName { get; set; } - public string? RoomAlias { get; set; } - public string? DraupnirShortcode { get; set; } - public Dictionary<StateEventResponse, int> ActiveKicks { get; set; } = []; - public bool MassCreatePolicies { - get => _massCreatePolicies; - set { - _massCreatePolicies = value; - StateHasChanged(); - } - } + public Dictionary<MatrixEventResponse, int> ActiveKicks { get; set; } = []; + + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + + // event types, unnamed + // private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes + // .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + // + // private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes + // .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); + + Dictionary<Type, PolicyCollection> PolicyCollections { get; set; } = new(); + PolicyCollection? DuplicateBans { get; set; } + PolicyCollection? RedundantBans { get; set; } protected override async Task OnInitializedAsync() { var sw = Stopwatch.StartNew(); @@ -219,202 +208,456 @@ else { Homeserver = (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true))!; if (Homeserver is null) return; Room = Homeserver.GetRoom(RoomId!); - await Task.WhenAll([ + IsInitialised = true; + StateHasChanged(); + await Task.WhenAll( Task.Run(async () => { PowerLevels = (await Room.GetPowerLevelsAsync())!; }), - Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }), - Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), - Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }), - Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>("org.matrix.mjolnir.protected_rooms")) is not null; }), - ]); - await LoadStatesAsync(); - Console.WriteLine($"Policy list editor initialized in {sw.Elapsed}!"); + Task.Run(async () => { CurrentUserIsDraupnir = (await Homeserver.GetAccountDataOrNullAsync<object>(DraupnirProtectedRoomsData.EventId)) is not null; }) + ); + StateHasChanged(); + await LoadStateAsync(firstLoad: true); + Loading = false; + logger.LogInformation("Policy list editor initialized in {SwElapsed}!", sw.Elapsed); } - private async Task LoadStatesAsync() { - Loading = true; - var states = Room.GetFullStateAsync(); - PolicyEventsByType.Clear(); - await foreach (var state in states) { - if (state is null) continue; - if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; - if (!PolicyEventsByType.ContainsKey(state.MappedType)) PolicyEventsByType.Add(state.MappedType, new()); - PolicyEventsByType[state.MappedType].Add(state); + private async Task LoadStateAsync(bool firstLoad = false) { + // preload workers in task pool + // await Task.WhenAll(Enumerable.Range(0, WebWorkerService.MaxWorkerCount).Select(async _ => (await WebWorkerService.TaskPool.GetWorkerAsync()).WhenReady).ToList()); + var taskPoolReadyTask = WebWorkerService.TaskPool.SetWorkerCount(WebWorkerService.MaxWorkerCount); + var sw = Stopwatch.StartNew(); + // Loading = true; + // var states = Room.GetFullStateAsync(); + var states = await Room.GetFullStateAsListAsync(); + // PolicyEventsByType.Clear(); + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); + + foreach (var type in KnownPolicyTypes) { + if (!PolicyCollections.ContainsKey(type)) { + var filterPropSw = Stopwatch.StartNew(); + // enumerate all properties with friendly name + var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => (x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyNameOrNull()) is not null) + .Where(x => x.GetCustomAttribute<TableHideAttribute>() is null) + .ToFrozenSet(); + + var proxySafeProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => props.Any(y => y.Name == x.Name)) + .ToFrozenDictionary(x => x.GetFriendlyNameOrNull() ?? x.GetJsonPropertyName(), x => x); + logger.LogInformation("{Count} proxy safe props found in {TypeFullName} ({TimeSpan})", proxySafeProps?.Count, type.FullName, filterPropSw.Elapsed); + PolicyCollections.Add(type, new() { + Name = type.GetFriendlyNamePluralOrNull() ?? type.FullName ?? type.Name, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = proxySafeProps + }); + } } + var count = 0; + var parseSw = Stopwatch.StartNew(); + foreach (var evt in states) { + var mappedType = evt.MappedType; + if (count % 100 == 0) + logger.LogInformation("Processing state #{Count:000000} {EvtType} @ {SwElapsed} (took {ParseSwElapsed:c} so far to process)", count, evt.Type, sw.Elapsed, parseSw.Elapsed); + count++; + + if (!mappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + + var collection = PolicyCollections[mappedType]; + + var key = (evt.Type, evt.StateKey!); + var policyInfo = new PolicyCollection.PolicyInfo { + Policy = evt, + MadeRedundantBy = [], + DuplicatedBy = [] + }; + if (evt.RawContent is null or { Count: 0 } || string.IsNullOrWhiteSpace(evt.RawContent?["recommendation"]?.GetValue<string>())) { + collection.ActivePolicies.Remove(key); + if (!collection.RemovedPolicies.TryAdd(key, policyInfo)) { + if (MatrixEvent.Equals(collection.RemovedPolicies[key].Policy, evt)) continue; + collection.RemovedPolicies[key] = policyInfo; + } + } + else { + collection.RemovedPolicies.Remove(key); + if (!collection.ActivePolicies.TryAdd(key, policyInfo)) { + if (MatrixEvent.Equals(collection.ActivePolicies[key].Policy, evt)) continue; + collection.ActivePolicies[key] = policyInfo; + } + } + } + + logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed); + foreach (var collection in PolicyCollections) { + logger.LogInformation("Policy collection {KeyFullName} has {ActivePoliciesCount} active and {RemovedPoliciesCount} removed policies.", collection.Key.FullName, collection.Value.ActivePolicies.Count, collection.Value.RemovedPolicies.Count); + } + + await Task.Delay(1); + Loading = false; StateHasChanged(); - } + await Task.Delay(100); - private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + // return; + logger.LogInformation("LoadStatesAsync: Scanning for redundant policies..."); - private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + var scanSw = Stopwatch.StartNew(); + // var allPolicyInfos = PolicyCollections.Values + // .SelectMany(x => x.ActivePolicies.Values) + // .ToArray(); + // var allPolicies = allPolicyInfos + // .Select<PolicyCollection.PolicyInfo, (PolicyCollection.PolicyInfo PolicyInfo, PolicyRuleEventContent TypedContent)>(x => (x, (x.Policy.TypedContent as PolicyRuleEventContent)!)) + // .ToList(); + // var hashPolicies = allPolicies + // .Where(x => x.TypedContent.IsHashedRule()) + // .ToList(); + // var wildcardPolicies = allPolicies + // .Except(hashPolicies) // hashed policies cannot be wildcards + // .Where(x => x.TypedContent.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent) + // .ToList(); + // var nonWildcardPolicies = allPolicies + // // .Except(wildcardPolicies) + // .Where(x => !x.TypedContent!.IsGlobRule() || x.TypedContent is ServerPolicyRuleEventContent) + // .ToList(); + // Console.WriteLine($"Got {allPolicies.Count} total policies, {wildcardPolicies.Count} wildcard policies. Time spent: {scanSw.Elapsed}"); + // int i = 0; + // int hits = 0; + // int redundant = 0; + // int duplicates = 0; - private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) - .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // foreach (var (policyInfo, policyContent) in allPolicies) { + // foreach (var (otherPolicyInfo, otherPolicyContent) in allPolicies) { + // if (policyInfo.Policy == otherPolicyInfo.Policy) continue; // same event + // if (MatrixEvent.TypeKeyPairMatches(policyInfo.Policy, otherPolicyInfo.Policy)) { + // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson()); + // continue; // same type and state key + // } + // // if(!policyContent.IsHashedRule()) + // } + // + // if (++i % 100 == 0) { + // Console.WriteLine($"Processed {i} policies in {scanSw.Elapsed}"); + // await Task.Delay(1); + // } + // } - private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() - ?? type.GetCustomAttributes<MatrixEventAttribute>() - .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + int scanningPolicyCount = 0; + var aggregatedPolicies = PolicyCollections.Values + .Aggregate(new List<MatrixEventResponse>(), (acc, val) => { + acc.AddRange(val.ActivePolicies.Select(x => x.Value.Policy)); + return acc; + }); + Console.WriteLine($"Scanning for redundant policies in {aggregatedPolicies.Count} total policies... ({scanSw.Elapsed})"); + List<Task<List<PolicyCollection.PolicyInfo>>> tasks = []; + // try to save some load... + var policiesJson = JsonSerializer.Serialize(aggregatedPolicies); + var policiesJsonMarshalled = JsRuntime.ReturnMe<SpawnDev.BlazorJS.JSObjects.String>(policiesJson); + var ranges = Enumerable.Range(0, aggregatedPolicies.Count).DistributeSequentially(WebWorkerService.MaxWorkerCount); + await taskPoolReadyTask; + tasks.AddRange(ranges.Select(range => WebWorkerService.TaskPool.Invoke(CheckDuplicatePoliciesAsync, policiesJsonMarshalled, range.First(), range.Last()))); - private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + Console.WriteLine($"Main: started {tasks.Count} workers in {scanSw.Elapsed}"); + // tasks.Add(CheckDuplicatePoliciesAsync(allPolicyInfos, range.First() .. range.Last())); + + // var allPolicyEvents = aggregatedPolicies.Select(x => x.Policy).ToList(); + + DuplicateBans = new() { + Name = "Duplicate bans", + ViewType = PolicyCollection.SpecialViewType.Duplicates, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + RedundantBans = new() { + Name = "Redundant bans", + ViewType = PolicyCollection.SpecialViewType.Redundant, + ActivePolicies = [], + RemovedPolicies = [], + PropertiesToDisplay = PolicyCollections.SelectMany(x => x.Value.PropertiesToDisplay).DistinctBy(x => x.Key).ToFrozenDictionary() + }; + + var allPolicyInfos = PolicyCollections.Values + .SelectMany(x => x.ActivePolicies.Values) + .ToArray(); + + 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}"); + foreach (var modifiedPolicyInfo in modifiedPolicyInfos) { + var original = allPolicyInfos.First(p => p.Policy.EventId == modifiedPolicyInfo.Policy.EventId); + original.DuplicatedBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.DuplicatedBy.Any(y => MatrixEvent.Equals(x, y))).ToList(); + original.MadeRedundantBy = aggregatedPolicies.Where(x => modifiedPolicyInfo.MadeRedundantBy.Any(y => MatrixEvent.Equals(x, y))).ToList(); + modifiedPolicyInfo.DuplicatedBy = modifiedPolicyInfo.MadeRedundantBy = []; // Early dereference + if (original.DuplicatedBy.Count > 0) { + if (!DuplicateBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + DuplicateBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } - private async Task RemovePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { }); - PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); - await LoadStatesAsync(); + if (original.MadeRedundantBy.Count > 0) { + if (!RedundantBans.Value.ActivePolicies.ContainsKey((original.Policy.Type, original.Policy.StateKey!))) + RedundantBans.Value.ActivePolicies.Add((original.Policy.Type, original.Policy.StateKey!), original); + } + // Console.WriteLine($"Memory usage: {Util.BytesToString(GC.GetTotalMemory(false))}"); + } + + Console.WriteLine($"Main: Processed {modifiedPolicyInfos.Count} modified policies in {scanSw.Elapsed} (applied in {applySw.Elapsed})"); + } + + Console.WriteLine($"Processed {allPolicyInfos.Length} policies in {scanSw.Elapsed}"); + + // // scan for wildcard matches + // foreach (var policy in allPolicies) { + // var matchingPolicies = wildcardPolicies + // .Where(x => + // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy) + // && x.Item2.EntityMatches(policy.TypedContent.Entity!) + // ) + // .ToList(); + // + // if (matchingPolicies.Count > 0) { + // logger.LogInformation($"{i} Got {matchingPolicies.Count} hits for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}"); + // foreach (var match in matchingPolicies) { + // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy); + // } + // + // hits++; + // redundant += matchingPolicies.Count; + // + // if (hits % 5 == 0) + // StateHasChanged(); + // } + // else { + // //logger.LogInformation("Sleeping..."); + // await Task.Delay(1); + // } + // + // i++; + // } + // + // i = 0; + // // scan for exact duplicates + // foreach (var policy in allPolicies) { + // var matchingPolicies = allPolicies + // .Where(x => + // !StateEvent.TypeKeyPairMatches(policy.PolicyInfo.Policy, x.PolicyInfo.Policy) + // && ( + // x.Item2.IsHashedRule() + // ? x.Item2.EntityMatches(policy.Item2.Entity) + // : x.Item2!.Entity == policy.Item2.Entity! + // ) + // ) + // .ToList(); + // + // if (matchingPolicies.Count > 0) { + // logger.LogInformation($"{i} Got {matchingPolicies.Count} duplicates for {policy.PolicyInfo.Policy.RawContent.ToJson()}: {matchingPolicies.Select(x => x.PolicyInfo.Policy.RawContent).ToJson()}"); + // foreach (var match in matchingPolicies) { + // policy.PolicyInfo.MadeRedundantBy.Add(match.PolicyInfo.Policy); + // } + // + // hits++; + // duplicates += matchingPolicies.Count; + // + // if (hits % 5 == 0) + // StateHasChanged(); + // } + // else { + // //logger.LogInformation("Sleeping..."); + // await Task.Delay(1); + // } + // + // i++; + // } + // + // logger.LogInformation($"LoadStatesAsync: Found {hits} ({redundant} redundant, {duplicates} duplicates) redundant policies in {sw.Elapsed}"); + // StateHasChanged(); } - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { - await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent); - CurrentlyEditingEvent = null; - await LoadStatesAsync(); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(SpawnDev.BlazorJS.JSObjects.String policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(policiesJson.ValueOf()); + Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.ValueOf().Length} bytes of JSON ({policies!.Count} policies)"); + return await CheckDuplicatePoliciesAsync(policies!, start .. end); } - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { - policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; - await LoadStatesAsync(); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(string policiesJson, int start, int end) { + var policies = JsonSerializer.Deserialize<List<MatrixEventResponse>>(policiesJson); + Console.WriteLine($"Got request to check duplicate policies in range {start} to {end} (length: {end - start}), {policiesJson.Length} bytes of JSON ({policies!.Count} policies)"); + return await CheckDuplicatePoliciesAsync(policies!, start .. end); } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + [return: WorkerTransfer] + private static Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, int start, int end) + => CheckDuplicatePoliciesAsync(policies, start .. end); - // event types, unnamed - private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes - .ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x); + [return: WorkerTransfer] + private static async Task<List<PolicyCollection.PolicyInfo>> CheckDuplicatePoliciesAsync(List<MatrixEventResponse> policies, Range range) { + var sw = Stopwatch.StartNew(); + var jsConsole = App.Host.Services.GetService<JsConsoleService>()!; + Console.WriteLine($"Processing policies in range {range} ({range.GetOffsetAndLength(policies.Count).Length}) with {policies.Count} total policies"); + var allPolicies = policies + .Select(x => (Event: x, TypedContent: (x.TypedContent as PolicyRuleEventContent)!)) + .ToList(); + var toCheck = allPolicies[range]; + var modifiedPolicies = new List<PolicyCollection.PolicyInfo>(); - private static Dictionary<Type, string[]> PolicyTypeIds = KnownPolicyTypes - .ToDictionary(x => x, x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName).ToArray()); + foreach (var (policyEvent, policyContent) in toCheck) { + List<MatrixEventResponse> duplicatedBy = []; + List<MatrixEventResponse> madeRedundantBy = []; -#region Draupnir interop + foreach (var (otherPolicyEvent, otherPolicyContent) in allPolicies) { + if (policyEvent == otherPolicyEvent) continue; // same event + if (MatrixEvent.TypeKeyPairMatches(policyEvent, otherPolicyEvent)) { + // logger.LogWarning("Sanity check failed: Found same type and state key for two different policies: {Policy1} and {Policy2}", policyInfo.Policy.RawContent.ToJson(), otherPolicyInfo.Policy.RawContent.ToJson()); + Console.WriteLine($"Sanity check failed: Found same type and state key for two different policies: {policyEvent.RawContent.ToJson()} and {otherPolicyEvent.RawContent.ToJson()}"); + continue; // same type and state key + } - private SemaphoreSlim ss = new(16, 16); + // if(!policyContent.IsHashedRule()) + if (!string.IsNullOrWhiteSpace(policyContent.Entity) && policyContent.Entity == otherPolicyContent.Entity) { + // Console.WriteLine($"Found duplicate policy: {policyEvent.EventId} is duplicated by {otherPolicyEvent.EventId}"); + duplicatedBy.Add(otherPolicyEvent); + } + } - private async Task DraupnirKickMatching(StateEventResponse policy) { - try { - var content = policy.TypedContent! as PolicyRuleEventContent; - if (content is null) return; - if (string.IsNullOrWhiteSpace(content.Entity)) return; + if (duplicatedBy.Count > 0 || madeRedundantBy.Count > 0) { + var summary = $"Policy {policyEvent.EventId} is:"; + if (duplicatedBy.Count > 0) + summary += $"\n- Duplicated by {duplicatedBy.Count} policies: {string.Join(", ", duplicatedBy.Select(x => x.EventId))}"; + if (madeRedundantBy.Count > 0) + summary += $"\n- Made redundant by {madeRedundantBy.Count} policies: {string.Join(", ", madeRedundantBy.Select(x => x.EventId))}"; + // Console.WriteLine(summary); + await jsConsole.Info(summary); + await Task.Delay(1); + modifiedPolicies.Add(new() { + Policy = policyEvent, + DuplicatedBy = duplicatedBy, + MadeRedundantBy = madeRedundantBy + }); + } - var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId); - var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList(); + // await Task.Delay(1); + } - ActiveKicks.Add(policy, rooms.Count); - StateHasChanged(); - await Task.Delay(500); + await jsConsole.Info($"Worker: Found {modifiedPolicies.Count} modified policies in range {range} (length: {range.GetOffsetAndLength(policies.Count).Length}) in {sw.Elapsed}"); - // for (int i = 0; i < 12; i++) { - // _ = WebWorkerService.TaskPool.Invoke(WasteCpu); - // } + return modifiedPolicies; + } + + // the old one: + private async Task LoadStatesAsync(bool firstLoad = false) { + await LoadStateAsync(firstLoad); + return; + var sw = Stopwatch.StartNew(); + Loading = true; + // var states = Room.GetFullStateAsync(); + var states = await Room.GetFullStateAsListAsync(); + // PolicyEventsByType.Clear(); - // static async Task runKicks(string roomId, PolicyRuleEventContent content) { - // Console.WriteLine($"Checking {roomId}..."); - // // Console.WriteLine($"Checking {room.RoomId}..."); - // // - // // try { - // // var members = await room.GetMembersListAsync(); - // // foreach (var member in members) { - // // var membership = member.ContentAs<RoomMemberEventContent>(); - // // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; - // // if (membership?.Membership is "leave" or "ban") continue; - // // - // // if (content.EntityMatches(member.StateKey!)) - // // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); - // // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); - // // } - // // } - // // finally { - // // Console.WriteLine($"Finished checking {room.RoomId}..."); - // // } - // } - // - // try { - // var tasks = rooms.Select(room => WebWorkerService.TaskPool.Invoke(runKicks, room.RoomId, content)).ToList(); - // - // await Task.WhenAll(tasks); - // } - // catch (Exception e) { - // Console.WriteLine(e); - // } + logger.LogInformation("LoadStatesAsync: Loaded state in {SwElapsed}", sw.Elapsed); - await NastyInternalsPleaseIgnore.ExecuteKickWithWasmWorkers(WebWorkerService, Homeserver, policy, data.Rooms); - // await Task.Run(async () => { - // foreach (var room in rooms) { - // try { - // Console.WriteLine($"Checking {room.RoomId}..."); - // var members = await room.GetMembersListAsync(); - // foreach (var member in members) { - // var membership = member.ContentAs<RoomMemberEventContent>(); - // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; - // if (membership?.Membership is "leave" or "ban") continue; - // - // if (content.EntityMatches(member.StateKey!)) - // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); - // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); - // } - // ActiveKicks[policy]--; - // StateHasChanged(); - // } - // finally { - // Console.WriteLine($"Finished checking {room.RoomId}..."); - // } - // } - // }); - } - finally { - ActiveKicks.Remove(policy); - StateHasChanged(); - await Task.Delay(500); + foreach (var type in KnownPolicyTypes) { + if (!PolicyEventsByType.ContainsKey(type)) + PolicyEventsByType.Add(type, new List + <MatrixEventResponse>(16000)); } - } -#region Nasty, nasty internals, please ignore! + int count = 0; + + foreach (var state in states) { + var _spsw = Stopwatch.StartNew(); + TimeSpan e1, e2, e3, e4, e5, e6, t; + if (!state.MappedType.IsAssignableTo(typeof(PolicyRuleEventContent))) continue; + e1 = _spsw.Elapsed; + var targetPolicies = PolicyEventsByType[state.MappedType]; + e2 = _spsw.Elapsed; + if (!firstLoad && targetPolicies.FirstOrDefault(x => MatrixEvent.TypeKeyPairMatches(x, state)) is { } evt) { + e3 = _spsw.Elapsed; + if (MatrixEvent.Equals(evt, state)) { + if (count % 100 == 0) { + await Task.Delay(10); + await Task.Yield(); + } - private static class NastyInternalsPleaseIgnore { - public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, StateEventResponse evt, List<string> roomIds) { - try { - // var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal, hs.WellKnownUris.Client, hs.AccessToken, roomId, content.Entity)).ToList(); - var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal2, hs.WellKnownUris, hs.AccessToken, roomId, evt)).ToList(); - // workerService.TaskPool.Invoke(ExecuteKickInternal, hs.BaseUrl, hs.AccessToken, roomIds, content.Entity); - await Task.WhenAll(tasks); + e4 = _spsw.Elapsed; + logger.LogInformation("[E] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={Zero:c},t={SpswElapsed:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, TimeSpan.Zero, _spsw.Elapsed); + continue; + } + + e4 = _spsw.Elapsed; + targetPolicies.Remove(evt); + e5 = _spsw.Elapsed; + targetPolicies.Add(state); + e6 = _spsw.Elapsed; + t = _spsw.Elapsed; + logger.LogInformation("[M] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={E3:c}, e4={E4:c}, e5={E5:c}, e6={E6:c},t={TimeSpan1:c})", count++, state.Type, sw.Elapsed, e1, e2, e3, e4, e5, e6, t); } - catch (Exception e) { - Console.WriteLine(e); + else { + targetPolicies.Add(state); + t = _spsw.Elapsed; + logger.LogInformation("[N] LoadStatesAsync: Processed state #{I:000000} {StateType} @ {SwElapsed} (e1={TimeSpan:c}, e2={E2:c}, e3={Zero:c}, e4={TimeSpan1:c}, e5={Zero1:c}, e6={TimeSpan2:c}, t={TimeSpan3:c})", count++, state.Type, sw.Elapsed, e1, e2, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero, t); } + + // await Task.Delay(10); + // await Task.Yield(); } - private static async Task ExecuteKickInternal(string homeserverBaseUrl, string accessToken, string roomId, string entity) { - try { - Console.WriteLine("args: " + string.Join(", ", homeserverBaseUrl, accessToken, roomId, entity)); - Console.WriteLine($"Checking {roomId}..."); - var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken); - Console.WriteLine($"Got HS..."); - var room = hs.GetRoom(roomId); - Console.WriteLine($"Got room..."); - var members = await room.GetMembersListAsync(); - Console.WriteLine($"Got members..."); - // foreach (var member in members) { - // var membership = member.ContentAs<RoomMemberEventContent>(); - // if (member.StateKey == hs.WhoAmI.UserId) continue; - // if (membership?.Membership is "leave" or "ban") continue; - // - // if (entity == member.StateKey) - // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); - // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); - // } - } - catch (Exception e) { - Console.WriteLine(e); - } + logger.LogInformation("LoadStatesAsync: Processed state in {SwElapsed}", sw.Elapsed); + + Loading = false; + StateHasChanged(); + await Task.Delay(10); + await Task.Yield(); + logger.LogInformation("LoadStatesAsync: yield finished in {SwElapsed}", sw.Elapsed); + } + + private List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + + // private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is { Count: > 0 } && string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); + // + // private List<MatrixEventResponse> GetRemovedPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + // .Where(x => x.RawContent is null or { Count: 0 }).ToList(); + + private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() + ?? type.GetCustomAttributes<MatrixEventAttribute>() + .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.EventName))?.EventName; + + private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; + + public struct PolicyCollection { + public required string Name { get; init; } + public SpecialViewType ViewType { get; init; } + public int TotalCount => ActivePolicies.Count + RemovedPolicies.Count; + + public required Dictionary<(string Type, string StateKey), PolicyInfo> ActivePolicies { get; set; } + + // public Dictionary<(string Type, string StateKey), MatrixEventResponse> InvalidPolicies { get; set; } + public required Dictionary<(string Type, string StateKey), PolicyInfo> RemovedPolicies { get; set; } + public required FrozenDictionary<string, PropertyInfo> PropertiesToDisplay { get; set; } + + public class PolicyInfo { + public required MatrixEventResponse Policy { get; init; } + public required List<MatrixEventResponse> MadeRedundantBy { get; set; } + public required List<MatrixEventResponse> DuplicatedBy { get; set; } } - private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, StateEventResponse policy) { - Console.WriteLine($"Checking {roomId}..."); - Console.WriteLine(policy.EventId); + public enum SpecialViewType { + None, + Duplicates, + Redundant, } } -#endregion - -#endregion + // private struct PolicyStats { + // public int Active { get; set; } + // public int Invalid { get; set; } + // public int Removed { get; set; } + // } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs new file mode 100644
index 0000000..6f45041 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.cs
@@ -0,0 +1,142 @@ +using LibMatrix; +using LibMatrix.EventTypes.Interop.Draupnir; +using LibMatrix.EventTypes.Spec.State.Policy; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using SpawnDev.BlazorJS.WebWorkers; + +namespace MatrixUtils.Web.Pages.Rooms; + +public partial class PolicyList { +#region Draupnir interop + + private SemaphoreSlim ss = new(16, 16); + + private async Task DraupnirKickMatching(MatrixEventResponse policy) { + try { + var content = policy.TypedContent! as PolicyRuleEventContent; + if (content is null) return; + if (string.IsNullOrWhiteSpace(content.Entity)) return; + + var data = await Homeserver.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId); + var rooms = data.Rooms.Select(Homeserver.GetRoom).ToList(); + + ActiveKicks.Add(policy, rooms.Count); + StateHasChanged(); + await Task.Delay(500); + + // for (int i = 0; i < 12; i++) { + // _ = WebWorkerService.TaskPool.Invoke(WasteCpu); + // } + + // static async Task runKicks(string roomId, PolicyRuleEventContent content) { + // Console.WriteLine($"Checking {roomId}..."); + // // Console.WriteLine($"Checking {room.RoomId}..."); + // // + // // try { + // // var members = await room.GetMembersListAsync(); + // // foreach (var member in members) { + // // var membership = member.ContentAs<RoomMemberEventContent>(); + // // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; + // // if (membership?.Membership is "leave" or "ban") continue; + // // + // // if (content.EntityMatches(member.StateKey!)) + // // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // // } + // // } + // // finally { + // // Console.WriteLine($"Finished checking {room.RoomId}..."); + // // } + // } + // + // try { + // var tasks = rooms.Select(room => WebWorkerService.TaskPool.Invoke(runKicks, room.RoomId, content)).ToList(); + // + // await Task.WhenAll(tasks); + // } + // catch (Exception e) { + // Console.WriteLine(e); + // } + + await NastyInternalsPleaseIgnore.ExecuteKickWithWasmWorkers(WebWorkerService, Homeserver, policy, data.Rooms); + // await Task.Run(async () => { + // foreach (var room in rooms) { + // try { + // Console.WriteLine($"Checking {room.RoomId}..."); + // var members = await room.GetMembersListAsync(); + // foreach (var member in members) { + // var membership = member.ContentAs<RoomMemberEventContent>(); + // if (member.StateKey == room.Homeserver.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (content.EntityMatches(member.StateKey!)) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + // ActiveKicks[policy]--; + // StateHasChanged(); + // } + // finally { + // Console.WriteLine($"Finished checking {room.RoomId}..."); + // } + // } + // }); + } + finally { + ActiveKicks.Remove(policy); + StateHasChanged(); + await Task.Delay(500); + } + } + +#region Nasty, nasty internals, please ignore! + + private static class NastyInternalsPleaseIgnore { + public static async Task ExecuteKickWithWasmWorkers(WebWorkerService workerService, AuthenticatedHomeserverGeneric hs, MatrixEventResponse evt, List<string> roomIds) { + try { + // var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal, hs.WellKnownUris.Client, hs.AccessToken, roomId, content.Entity)).ToList(); + var tasks = roomIds.Select(roomId => workerService.TaskPool.Invoke(ExecuteKickInternal2, hs.WellKnownUris, hs.AccessToken, roomId, evt)).ToList(); + // workerService.TaskPool.Invoke(ExecuteKickInternal, hs.BaseUrl, hs.AccessToken, roomIds, content.Entity); + await Task.WhenAll(tasks); + } + catch (Exception e) { + Console.WriteLine(e); + } + } + + private static async Task ExecuteKickInternal(string homeserverBaseUrl, string accessToken, string roomId, string entity) { + try { + Console.WriteLine("args: " + string.Join(", ", homeserverBaseUrl, accessToken, roomId, entity)); + Console.WriteLine($"Checking {roomId}..."); + var hs = new AuthenticatedHomeserverGeneric(homeserverBaseUrl, new() { Client = homeserverBaseUrl }, null, accessToken); + Console.WriteLine($"Got HS..."); + var room = hs.GetRoom(roomId); + Console.WriteLine($"Got room..."); + var members = await room.GetMembersListAsync(); + Console.WriteLine($"Got members..."); + // foreach (var member in members) { + // var membership = member.ContentAs<RoomMemberEventContent>(); + // if (member.StateKey == hs.WhoAmI.UserId) continue; + // if (membership?.Membership is "leave" or "ban") continue; + // + // if (entity == member.StateKey) + // // await room.KickAsync(member.StateKey, content.Reason ?? "No reason given"); + // Console.WriteLine($"Would kick {member.StateKey} from {room.RoomId} (EntityMatches)"); + // } + } + catch (Exception e) { + Console.WriteLine(e); + } + } + + private async static Task ExecuteKickInternal2(HomeserverResolverService.WellKnownUris wellKnownUris, string accessToken, string roomId, MatrixEventResponse policy) { + Console.WriteLine($"Checking {roomId}..."); + Console.WriteLine(policy.EventId); + } + } + +#endregion + +#endregion +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css deleted file mode 100644
index afe9fb0..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor.css +++ /dev/null
@@ -1,9 +0,0 @@ -th { - border-width: 1px; -} - -table { - width: fit-content; - border-width: 1px; - vertical-align: middle; -} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
index 982fc5a..ac918a8 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyList2.razor
@@ -15,7 +15,11 @@ <h3>Policy list editor - Editing @RoomId</h3> <hr/> @* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ -<LinkButton OnClick="@(() => { CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; return Task.CompletedTask; })">Create new policy</LinkButton> +<LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy +</LinkButton> @if (Loading) { <p>Loading...</p> @@ -71,16 +75,24 @@ else { } <div style="display: ruby;"> @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, policy.Type)) { - <LinkButton OnClick="@(() => { CurrentlyEditingEvent = policy; return Task.CompletedTask; })">Edit</LinkButton> - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> + <LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = policy; + return Task.CompletedTask; + })">Edit + </LinkButton> + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Remove</LinkButton> @if (policy.IsLegacyType) { - <LinkButton OnClick="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> + <LinkButton OnClickAsync="@(() => RemovePolicyAsync(policy))">Update policy type</LinkButton> } @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(policy.EventId)) { - <LinkButton OnClick="@(() => { ServerPolicyToMakePermanent = policy; return Task.CompletedTask; })">Make permanent (wildcard)</LinkButton> + <LinkButton OnClickAsync="@(() => { + ServerPolicyToMakePermanent = policy; + return Task.CompletedTask; + })">Make permanent (wildcard) + </LinkButton> @if (CurrentUserIsDraupnir) { - <LinkButton OnClick="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> + <LinkButton OnClickAsync="@(() => UpgradePolicyAsync(policy))">Kick matching users</LinkButton> } } else { @@ -144,12 +156,12 @@ else { public string RoomId { get; set; } private bool _enableAvatars; - private StateEventResponse? _currentlyEditingEvent; - private StateEventResponse? _serverPolicyToMakePermanent; + private MatrixEventResponse? _currentlyEditingEvent; + private MatrixEventResponse? _serverPolicyToMakePermanent; - private Dictionary<Type, List<StateEventResponse>> PolicyEventsByType { get; set; } = new(); + private Dictionary<Type, List<MatrixEventResponse>> PolicyEventsByType { get; set; } = new(); - private StateEventResponse? CurrentlyEditingEvent { + private MatrixEventResponse? CurrentlyEditingEvent { get => _currentlyEditingEvent; set { _currentlyEditingEvent = value; @@ -157,7 +169,7 @@ else { } } - private StateEventResponse? ServerPolicyToMakePermanent { + private MatrixEventResponse? ServerPolicyToMakePermanent { get => _serverPolicyToMakePermanent; set { _serverPolicyToMakePermanent = value; @@ -197,12 +209,12 @@ else { StateHasChanged(); } - private List<StateEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; + private List<MatrixEventResponse> GetPolicyEventsByType(Type type) => PolicyEventsByType.ContainsKey(type) ? PolicyEventsByType[type] : []; - private List<StateEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + private List<MatrixEventResponse> GetValidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) .Where(x => !string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); - private List<StateEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) + private List<MatrixEventResponse> GetInvalidPolicyEventsByType(Type type) => GetPolicyEventsByType(type) .Where(x => string.IsNullOrWhiteSpace(x.RawContent?["recommendation"]?.GetValue<string>())).ToList(); private string? GetPolicyTypeNameOrNull(Type type) => type.GetFriendlyNamePluralOrNull() @@ -211,24 +223,24 @@ else { private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name; - private async Task RemovePolicyAsync(StateEventResponse policyEvent) { + private async Task RemovePolicyAsync(MatrixEventResponse policyEvent) { await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { }); PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); await LoadStatesAsync(); } - private async Task UpdatePolicyAsync(StateEventResponse policyEvent) { + private async Task UpdatePolicyAsync(MatrixEventResponse policyEvent) { await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent); CurrentlyEditingEvent = null; await LoadStatesAsync(); } - private async Task UpgradePolicyAsync(StateEventResponse policyEvent) { + private async Task UpgradePolicyAsync(MatrixEventResponse policyEvent) { policyEvent.RawContent["upgraded_from_type"] = policyEvent.Type; await LoadStatesAsync(); } - private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); + private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet(); // event types, unnamed private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor new file mode 100644
index 0000000..932e0fe --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListCategoryComponent.razor
@@ -0,0 +1,74 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.RoomTypes +<details> + <summary> + <span> + @($"{PolicyCollection.Name}: {PolicyCollection.TotalCount} policies") + </span> + <hr style="margin: revert;"/> + </summary> + <table class="table table-striped table-hover table-bordered align-middle"> + <thead> + <tr> + <th>Actions</th> + @foreach (var name in PolicyCollection.PropertiesToDisplay!.Keys) { + <th>@name</th> + } + </tr> + </thead> + <tbody> + @foreach (var policy in PolicyCollection.ActivePolicies.Values.OrderBy(x => x.Policy.RawContent?["entity"]?.GetValue<string>())) { + <PolicyListRowComponent PolicyCollectionStateHasChanged="@StateHasChanged" RenderEventInfo="RenderEventInfo" PolicyInfo="@policy" PolicyCollection="@PolicyCollection" Room="@Room"></PolicyListRowComponent> + } + </tbody> + </table> + @if (RenderInvalidSection) { + <details> + <summary> + <u> + @("Invalid " + PolicyCollection.Name.ToLower()) + </u> + </summary> + <table class="table table-striped table-hover table-bordered align-middle"> + <thead> + <tr> + <th>State key</th> + <th>Json contents</th> + </tr> + </thead> + <tbody> + @foreach (var policy in PolicyCollection.RemovedPolicies.Values) { + <tr> + <td>@policy.Policy.StateKey</td> + <td> + <pre>@policy.Policy.RawContent.ToJson(true, false)</pre> + </td> + </tr> + } + </tbody> + </table> + </details> + } +</details> + +@code { + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + [Parameter] + public required GenericRoom Room { get; set; } + + [Parameter] + public bool RenderEventInfo { get; set; } + + [Parameter] + public bool RenderInvalidSection { get; set; } = true; + + protected override bool ShouldRender() { + // if (PolicyCollection is null) return false; + + return true; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor new file mode 100644
index 0000000..b57beae --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListEditorHeader.razor
@@ -0,0 +1,88 @@ +@using LibMatrix +@using LibMatrix.EventTypes.Common +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents +<h3>Policy list editor - Editing @(RoomName ?? Room.RoomId)</h3> +@if (!string.IsNullOrWhiteSpace(DraupnirShortcode)) { + <span style="margin-right: 2em;">Shortcode: @DraupnirShortcode</span> +} +@if (!string.IsNullOrWhiteSpace(RoomAlias)) { + <span>Alias: @RoomAlias</span> +} +<hr/> +@* <InputCheckbox @bind-Value="EnableAvatars"></InputCheckbox><label>Enable avatars (WILL EXPOSE YOUR IP TO TARGET HOMESERVERS!)</label> *@ +<LinkButton OnClickAsync="@(() => { + CurrentlyEditingEvent = new() { Type = "", RawContent = new() }; + return Task.CompletedTask; + })">Create new policy +</LinkButton> +<LinkButton OnClickAsync="@(() => { + MassCreatePolicies = true; + return Task.CompletedTask; + })">Create many new policies +</LinkButton> +<LinkButton OnClickAsync="@(() => ReloadStateAsync())">Refresh</LinkButton> + +@if (CurrentlyEditingEvent is not null) { + <PolicyEditorModal PolicyEvent="@CurrentlyEditingEvent" OnClose="@(() => CurrentlyEditingEvent = null)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal> +} + +@if (MassCreatePolicies) { + <MassPolicyEditorModal Room="@Room" OnClose="@(() => MassCreatePolicies = false)" OnSaved="@(() => { + MassCreatePolicies = false; + // _ = LoadStatesAsync(); + })"></MassPolicyEditorModal> +} +<br/> +<InputCheckbox Value="@RenderEventInfo" ValueChanged="@RenderEventInfoChanged" ValueExpression="@(() => RenderEventInfo)"/> +<span> Render event info</span> + +@code { + + [Parameter] + public required GenericRoom Room { get; set; } + + [Parameter] + public required Func<Task> ReloadStateAsync { get; set; } + + [Parameter] + public required bool RenderEventInfo { get; set; } + + [Parameter] + public required EventCallback<bool> RenderEventInfoChanged { get; set; } + + private string? RoomName { get; set; } + private string? RoomAlias { get; set; } + private string? DraupnirShortcode { get; set; } + + private MatrixEventResponse? CurrentlyEditingEvent { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool MassCreatePolicies { + get; + set { + field = value; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() { + await Task.WhenAll( + Task.Run(async () => { DraupnirShortcode = (await Room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode; }), + Task.Run(async () => { RoomAlias = (await Room.GetCanonicalAliasAsync())?.Alias; }), + Task.Run(async () => { RoomName = await Room.GetNameOrFallbackAsync(); }) + ); + + StateHasChanged(); + } + + private async Task UpdatePolicyAsync(MatrixEventResponse evt) { + Console.WriteLine("UpdatePolicyAsync in PolicyListEditorHeader not yet implemented!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor new file mode 100644
index 0000000..3ded78f --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/PolicyListComponents/PolicyListRowComponent.razor
@@ -0,0 +1,218 @@ +@using System.Reflection +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Shared.PolicyEditorComponents + +@if (_isInitialized && IsVisible) { + <tr id="@PolicyInfo.Policy.EventId"> + <td> + <div style="display: flex; flex-direction: row; gap: 0.5em;"> + @* @if (PowerLevels.UserHasStatePermission(Homeserver.WhoAmI.UserId, Policy.Type)) { *@ + @if (true) { + <LinkButton OnClickAsync="@(() => { + IsEditing = true; + return Task.CompletedTask; + })">Edit + </LinkButton> + <LinkButton OnClickAsync="@RemovePolicyAsync">Remove</LinkButton> + @if (Policy.IsLegacyType) { + <LinkButton OnClickAsync="@RemovePolicyAsync">Update type</LinkButton> + } + + @if (TypedContent.Entity?.StartsWith("@*:", StringComparison.Ordinal) == true) { + <LinkButton OnClickAsync="@ConvertToAclAsync">Convert to ACL</LinkButton> + } + + @* @if (PolicyTypeIds[typeof(ServerPolicyRuleEventContent)].Contains(Policy.Type)) { *@ + @* <LinkButton OnClickAsync="@(() => { *@ + @* ServerPolicyToMakePermanent = Policy; *@ + @* return Task.CompletedTask; *@ + @* })">Make permanent *@ + @* </LinkButton> *@ + @* @if (CurrentUserIsDraupnir) { *@ + @* <LinkButton Color="@(ActiveKicks.ContainsKey(Policy) ? "#FF0000" : null)" OnClick="@(() => DraupnirKickMatching(Policy))">Kick *@ + @* users @(ActiveKicks.TryGetValue(Policy, out var kick) ? $"({kick})" : null) *@ + @* </LinkButton> *@ + @* } *@ + // } + } + else { + <p>No permission to modify</p> + } + </div> + </td> + @foreach (var prop in PolicyCollection.PropertiesToDisplay.Values) { + if (prop.Name == "Entity") { + <td> + <span>@TruncateMxid(TypedContent.Entity)</span> + @foreach (var dup in PolicyInfo.DuplicatedBy) { + <br/> + <span>Duplicated by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span> + } + @foreach (var dup in PolicyInfo.MadeRedundantBy) { + <br/> + <span>Also matched by @dup.FriendlyTypeName.ToLower() <a href="@Anchor(dup.EventId!)">@TruncateMxid(dup.RawContent["entity"]?.GetValue<string>())</a></span> + } + @if (RenderEventInfo) { + <br/> + <pre style="margin-bottom: unset;"> + @PolicyInfo.Policy.Type/@PolicyInfo.Policy.StateKey by @PolicyInfo.Policy.Sender at @PolicyInfo.Policy.OriginServerTimestamp + </pre> + } + </td> + } + else { + <td>@prop.GetGetMethod()?.Invoke(TypedContent, null)</td> + } + } + </tr> + + @if (IsEditing) { + <PolicyEditorModal PolicyEvent="@Policy" OnClose="@(() => IsEditing = false)" OnSaveAsync="@UpdatePolicyAsync"></PolicyEditorModal> + } + @* TODO: Implement ability to turn ACLs into wildcards *@ + @*@if (ServerPolicyToMakePermanent is not null) { + <ModalWindow Title="Make policy permanent"> + + </ModalWindow> + }*@ +} + + + +@code { + + [Parameter] + public PolicyList.PolicyCollection.PolicyInfo PolicyInfo { get; set; } + + [Parameter] + public GenericRoom Room { get; set; } = null!; + + [Parameter] + public required PolicyList.PolicyCollection PolicyCollection { get; set; } + + [Parameter] + public bool RenderEventInfo { get; set; } + + [Parameter] + public required Action PolicyCollectionStateHasChanged { get; set; } + + private MatrixEventResponse Policy => PolicyInfo.Policy; + + private bool IsEditing { + get; + set { + field = value; + _isDirty = true; + StateHasChanged(); + } + } + + public bool IsVisible { + get; + set { + field = value; + _isDirty = true; + } + } = true; + + private PolicyRuleEventContent TypedContent { get; set; } + + private bool _isDirty = true; + private bool _isInitialized; + + protected override bool ShouldRender() => _isDirty; + + protected override void OnParametersSet() { + TypedContent = Policy.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy must have a typed content of type PolicyRuleEventContent."); + _isDirty = true; + _isInitialized = true; + // Console.WriteLine($"ParametersSet {Policy.StateKey}"); + } + + private static string TruncateMxid(string? mxid) { + if (string.IsNullOrWhiteSpace(mxid)) return mxid; + var parts = mxid.Split(':', 2); + if (parts[0].Length > 50) + parts[0] = parts[0][..50] + "[...]"; + + if (parts is [_, { Length: > 50 }]) + parts[1] = parts[1][..50] + "[...]"; + + return parts.Length == 1 ? parts[0] : $"{parts[0]}:{parts[1]}"; + } + + private async Task RemovePolicyAsync() { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, new { }); + bool shouldUpdateVisibility = true; + PolicyCollection.ActivePolicies.Remove((Policy.Type, Policy.StateKey)); + PolicyCollection.RemovedPolicies.Add((Policy.Type, Policy.StateKey), PolicyInfo); + if (PolicyInfo.DuplicatedBy.Count > 0) { + foreach (var evt in PolicyInfo.DuplicatedBy) { + var matchingEntry = PolicyCollection.ActivePolicies + .FirstOrDefault(x => MatrixEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.DuplicatedBy.RemoveAll(x => MatrixEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} duplicates from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + if (PolicyCollection.ViewType == PolicyList.PolicyCollection.SpecialViewType.Duplicates && matchingEntry.DuplicatedBy.Count == 0) { + PolicyCollection.ActivePolicies.Remove((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey)); + PolicyCollection.RemovedPolicies.Add((matchingEntry.Policy.Type, matchingEntry.Policy.StateKey), matchingEntry); + Console.WriteLine($"Also removed {matchingEntry.Policy.EventId} as it is now redundant"); + } + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (PolicyInfo.MadeRedundantBy.Count > 0) { + foreach (var evt in PolicyInfo.MadeRedundantBy) { + var matchingEntry = PolicyCollection.ActivePolicies + .FirstOrDefault(x => MatrixEvent.Equals(x.Value.Policy, evt)).Value; + var removals = matchingEntry.MadeRedundantBy.RemoveAll(x => MatrixEvent.Equals(x, Policy)); + Console.WriteLine($"Removed {removals} redundants from {evt.EventId}, matching entry: {matchingEntry.ToJson()}"); + } + + PolicyCollectionStateHasChanged(); + shouldUpdateVisibility = false; + } + + if (shouldUpdateVisibility) { + IsVisible = false; + StateHasChanged(); + } + // PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent); + // await LoadStatesAsync(); + } + + private async Task UpdatePolicyAsync(MatrixEventResponse evt) { + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey, Policy.RawContent); + // CurrentlyEditingEvent = null; + // await LoadStatesAsync(); + } + + private async Task UpgradePolicyAsync() { + Policy.RawContent["gay.rory.matrixutils.upgraded_from_type"] = Policy.Type; + // await LoadStatesAsync(); + } + + private async Task ConvertToAclAsync() { + if (Policy.RawContent.ContainsKey("entity")) { + var newContent = Policy.ContentAs<ServerPolicyRuleEventContent>(); + newContent!.Entity = newContent.Entity!.Replace("@*:", ""); + await Room.SendStateEventAsync(ServerPolicyRuleEventContent.EventId, newContent.GetDraupnir2StateKey(), newContent); + await Room.SendStateEventAsync(Policy.Type, Policy.StateKey!, new { }); + IsVisible = false; + StateHasChanged(); + } + else { + throw new InvalidOperationException("Policy event must contain an 'entity' field to convert to ACL."); + } + } + + private string Anchor(string anchor) { + return $"{NavigationManager.Uri.Split('#')[0]}#{anchor}"; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
index 2903ab8..a84ef8c 100644 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor +++ b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor
@@ -1,12 +1,24 @@ @page "/PolicyLists" +@using ArcaneLibs @using ArcaneLibs.Extensions @using LibMatrix @using LibMatrix.EventTypes @using LibMatrix.EventTypes.Common @using LibMatrix.EventTypes.Spec.State.Policy +@using LibMatrix.Helpers +@using LibMatrix.Responses @using LibMatrix.RoomTypes @inject ILogger<Index> logger -<h3>Policy lists </h3> @* <LinkButton href="/Rooms/Create">Create new policy list</LinkButton> *@ +<h3> + <span>Policy lists </span> + <LinkButton OnClickAsync="@(() => { + ShowPolicyListCreationWindow = true; + return Task.CompletedTask; + })"> + <span class="oi oi-plus" aria-hidden="true"> Create</span> + </LinkButton> +</h3> + @if (!string.IsNullOrWhiteSpace(Status)) { <p>@Status</p> @@ -16,22 +28,17 @@ } <hr/> -<table> +<table class="table table-striped table-hover table-bordered align-middle" aria-busy="@isLoading"> <thead> <tr> - <th/> <th>Room name</th> <th>Policies</th> + <th/> </tr> </thead> <tbody> @foreach (var room in Rooms.OrderByDescending(x => x.PolicyCounts.Sum(y => y.Value))) { <tr> - <td> - <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")"> - <span class="oi oi-pencil" aria-hidden="true"></span> - </LinkButton> - </td> <td style="padding-right: 24px;"> <span>@room.RoomName</span> @if (room.IsLegacy) { @@ -50,11 +57,44 @@ <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Server) ?? 0) server policies</span><br/> <span>@(room.PolicyCounts.GetValueOrDefault(RoomInfo.PolicyType.Room) ?? 0) room policies</span><br/> </td> + <td> + <LinkButton href="@($"/Rooms/{room.Room.RoomId}/Policies")"> + <span class="oi oi-pencil" aria-hidden="true"> View/edit policies</span> + </LinkButton> + </td> </tr> } </tbody> </table> +@if (ShowPolicyListCreationWindow && Homeserver != null) { + <ModalWindow Title="New policy list"> + @if (!string.IsNullOrWhiteSpace(_roomBuilder.Avatar.Url)) { + <MxcAvatar Homeserver="@Homeserver" MxcUri="@_roomBuilder.Avatar.Url" Circular="true" Size="4" SizeUnit="em"/> + } + else { + <img class="avatar" style="height: 4em; width: 4em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/> + } + <div style="display: inline-block; vertical-align: middle; padding-left: 1em;"> + <FancyTextBox @bind-Value="@_roomBuilder.Name.Name"></FancyTextBox> + <br/> + <span>#</span> + <FancyTextBox @bind-Value="@_roomBuilder.AliasLocalPart"></FancyTextBox> + <span>:@Homeserver!.ServerName</span> + <br/> + <FancyTextBox @bind-Value="@_roomBuilder.Avatar.Url"></FancyTextBox> + <InputFile OnChange="@RoomIconFilePicked"></InputFile> + </div> + <br/> + + <span>Bot shortcode: </span> + <FancyTextBox @bind-Value="@_shortcodeEvent.Shortcode"></FancyTextBox> + <br/> + <LinkButton OnClickAsync="@CreatePolicyList">Create</LinkButton> + + </ModalWindow> +} + @code { private List<RoomInfo> Rooms { get; } = []; @@ -65,44 +105,39 @@ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); if (Homeserver is null) return; + isLoading = true; Status = "Fetching rooms..."; - - var userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>(); - var serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>(); - var roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>(); - var knownPolicyTypes = (List<string>) [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - - List<GenericRoom> roomsByType = []; + List<Task> _tasks = []; await foreach (var room in Homeserver.GetJoinedRoomsByType("support.feline.policy.lists.msc.v1")) { - roomsByType.Add(room); + // roomsByType.Add(room); Status2 = $"Found {room.RoomId} (MSC3784)..."; + _tasks.Add(Task.Run(async () => { + Rooms.Add(await RoomInfo.FromRoom(room)); + StateHasChanged(); + })); } - List<Task<RoomInfo>> tasks = roomsByType.Select(async room => { - Status2 = $"Fetching room {room.RoomId}..."; - return await RoomInfo.FromRoom(room); - }).ToList(); + await Task.WhenAll(_tasks); - var results = tasks.ToAsyncEnumerable(); - await foreach (var result in results) { - Rooms.Add(result); - StateHasChanged(); - } + isLoading = false; + Status = ""; + Status2 = ""; + } + private async Task ScanLegacyLists() { + isLoading = true; Status = "Searching for legacy lists..."; - var rooms = (await Homeserver.GetJoinedRooms()) .Where(x => !Rooms.Any(y => y.Room.RoomId == x.RoomId)) .Select(async room => { var state = await room.GetFullStateAsListAsync(); var policies = state - .Where(x => knownPolicyTypes.Contains(x.Type)) + .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type)) .ToList(); 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) { @@ -111,31 +146,35 @@ } } + isLoading = false; Status = ""; Status2 = ""; - await base.OnInitializedAsync(); } - private string _status; - - public string Status { - get => _status; + private string? Status { + get; set { - _status = value; + field = value; StateHasChanged(); } } - private string _status2; - - public string Status2 { - get => _status2; + private string? Status2 { + get; set { - _status2 = value; + field = value; StateHasChanged(); } } + private bool ShowPolicyListCreationWindow { + get; + set { + field = value; + StateHasChanged(); + } + } = true; + private class RoomInfo { public GenericRoom Room { get; set; } public string RoomName { get; set; } @@ -149,12 +188,7 @@ Server } - private static readonly List<string> userEventTypes = EventContent.GetMatchingEventTypes<UserPolicyRuleEventContent>(); - private static readonly List<string> serverEventTypes = EventContent.GetMatchingEventTypes<ServerPolicyRuleEventContent>(); - private static readonly List<string> roomEventTypes = EventContent.GetMatchingEventTypes<RoomPolicyRuleEventContent>(); - private static readonly List<string> allKnownPolicyTypes = [..userEventTypes, ..serverEventTypes, ..roomEventTypes]; - - public static async Task<RoomInfo> FromRoom(GenericRoom room, List<StateEventResponse>? state = null, bool legacy = false) { + public static async Task<RoomInfo> FromRoom(GenericRoom room, List<MatrixEventResponse>? state = null, bool legacy = false) { state ??= await room.GetFullStateAsListAsync(); return new RoomInfo() { Room = room, @@ -165,12 +199,40 @@ ?? room.RoomId, Shortcode = (await room.GetStateOrNullAsync<MjolnirShortcodeEventContent>(MjolnirShortcodeEventContent.EventId))?.Shortcode, PolicyCounts = new() { - { PolicyType.User, state.Count(x => userEventTypes.Contains(x.Type)) }, - { PolicyType.Server, state.Count(x => serverEventTypes.Contains(x.Type)) }, - { PolicyType.Room, state.Count(x => roomEventTypes.Contains(x.Type)) } + { PolicyType.User, state.Count(x => PolicyRoom.UserPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Server, state.Count(x => PolicyRoom.ServerPolicyEventTypes.Contains(x.Type)) }, + { PolicyType.Room, state.Count(x => PolicyRoom.RoomPolicyEventTypes.Contains(x.Type)) } } }; } } + private readonly RoomBuilder _roomBuilder = new() { + Type = "support.feline.policy.lists.msc.v1", + Name = new() { Name = "New policy list" }, + AliasLocalPart = "policies" + }; + + private readonly MjolnirShortcodeEventContent _shortcodeEvent = new() { + Shortcode = "policy-list" + }; + + private bool isLoading = true; + + private static readonly SvgIdenticonGenerator IdenticonGenerator = new(); + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { + var res = await Homeserver!.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + _roomBuilder.Avatar.Url = res; + StateHasChanged(); + } + + private async Task CreatePolicyList() { + var room = await _roomBuilder.Create(Homeserver!); + Status = $"Created policy list {room.RoomId} ({room.GetNameAsync()})"; + await room.SendStateEventAsync(MjolnirShortcodeEventContent.EventId, _shortcodeEvent); + NavigationManager.NavigateTo($"/Rooms/{room.RoomId}/Policies"); + } + } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css b/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css deleted file mode 100644
index f9b5b3f..0000000 --- a/MatrixUtils.Web/Pages/Rooms/PolicyLists.razor.css +++ /dev/null
@@ -1,6 +0,0 @@ -table, th, td { - border-width: 1px; -} -td { - padding: 8px; -} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor new file mode 100644
index 0000000..c1ee202 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateBasicRoomInfoOptions.razor
@@ -0,0 +1,52 @@ +@using ArcaneLibs +@using LibMatrix.Helpers +<tr> + <td>Room name:</td> + <td> + <FancyTextBox @bind-Value="@roomBuilder.Name.Name"></FancyTextBox> + </td> +</tr> +<tr> + <td>Room alias:</td> + <td> + <InputLocalPart Sigil="#" ServerName="@Homeserver.ServerName" @bind-LocalPart="@roomBuilder.AliasLocalPart"></InputLocalPart> + </td> +</tr> +<tr> + <td>Room icon:</td> + <td> + @if (!string.IsNullOrWhiteSpace(roomBuilder.Avatar.Url)) { + <MxcAvatar Homeserver="Homeserver" MxcUri="@roomBuilder.Avatar.Url" Size="3" SizeUnit="em" Circular="true"/> + } + else { + <img class="avatar" style="height: 3em; width: 3em; border-radius: 50%;" src="@IdenticonGenerator.GenerateAsDataUri(Homeserver.WhoAmI.UserId)"/> + } + <div style="display: inline-block; vertical-align: middle;"> + <FancyTextBox @bind-Value="@roomBuilder.Avatar.Url"></FancyTextBox> + <br/> + <SimpleFilePicker OnFilePicked="@RoomIconFilePicked"></SimpleFilePicker> + </div> + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private static readonly SvgIdenticonGenerator IdenticonGenerator = new(); + + private async Task RoomIconFilePicked(InputFileChangeEventArgs obj) { + var res = await Homeserver.UploadFile(obj.File.Name, obj.File.OpenReadStream(), obj.File.ContentType); + Console.WriteLine(res); + roomBuilder.Avatar.Url = res; + PageStateHasChanged(); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor new file mode 100644
index 0000000..3f4a73d --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateCreateOptions.razor
@@ -0,0 +1,92 @@ +@using Blazored.LocalStorage +@using LibMatrix.Helpers +@inject ILocalStorageService LocalStorage +<tr> + <td>Room type:</td> + <td> + @if (RoomTypes.ContainsKey(roomBuilder.Type ?? "")) { + <InputSelect @bind-Value="@roomBuilder.Type"> + @foreach (var type in RoomTypes) { + <option value="@type.Key">@type.Value</option> + } + <option value="custom">Custom ...</option> + </InputSelect> + } + else { + <FancyTextBox @bind-Value="@roomBuilder.Type"></FancyTextBox> + } + + <span> version </span> + @if (Capabilities is null) { + <span style="color: #888;">Loading...</span> + } + else { + <InputSelect @bind-Value="@roomBuilder.Version"> + @foreach (var version in Capabilities.Capabilities.RoomVersions!.Available!) { + <option value="@version.Key">@version.Key (@version.Value)</option> + } + </InputSelect> + } + </td> +</tr> +<tr> + <td style="vertical-align: top;">Allow attribution:</td> + <td> + <InputCheckbox @bind-Value="@AllowAttribution"/> + <span>Allow attribution to Rory&::MatrixUtils</span> + <LinkButton InlineText="true" OnClick="@(() => ShowAttributionInfo = true)">?</LinkButton> + </td> +</tr> + +@if (ShowAttributionInfo) { + <ModalWindow Title="Allow attribution to Rory&::MatrixUtils" + OnCloseClicked="@(() => ShowAttributionInfo = false)"> + <span>This will add the following to the room creation content:</span> + <br/> + <pre>{ "gay.rory.created_using": "Rory&::MatrixUtils (https://mru.rory.gay)" }</pre> + <span>This is not visible to users unless they manually inspect the room's create event source.</span> + </ModalWindow> +} + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private AuthenticatedHomeserverGeneric.CapabilitiesResponse? Capabilities { get; set; } + + private bool ShowAttributionInfo { + get; + set { + field = value; + StateHasChanged(); + } + } + + private bool AllowAttribution { + get; + set { + field = value; + _ = LocalStorage.SetItemAsync("rmu.room_create.allow_attribution", value); + } + } = true; + + protected override async Task OnInitializedAsync() { + Capabilities = await Homeserver.GetCapabilitiesAsync(); + roomBuilder.Version = Capabilities.Capabilities.RoomVersions!.Default; + AllowAttribution = await LocalStorage.GetItemAsync<bool?>("rmu.room_create.allow_attribution") ?? true; + } + + private static Dictionary<string, string> RoomTypes { get; } = new() { + { "", "Room" }, + { "m.space", "Space" }, + { "support.feline.policy.lists.msc.v1", "Policy list" } + }; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor new file mode 100644
index 0000000..2b1d90a --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateInitialStateOptions.razor
@@ -0,0 +1,83 @@ +@using System.Text.Json +@using ArcaneLibs.Extensions +@using LibMatrix +@using LibMatrix.Helpers +<tr> + <td style="vertical-align: top;">Initial room state:</td> + <td> + @foreach (var (displayName, events) in new Dictionary<string, List<MatrixEvent>>() { + { "Important room state (before final access rules)", roomBuilder.ImportantState }, + { "Additional room state (after final access rules)", roomBuilder.InitialState }, + }) { + <details open> + + @code + { + // private static readonly string[] ImplementedStates = { "m.room.avatar", "m.room.history_visibility", "m.room.guest_access", "m.room.server_acl" }; + } + + @* <summary>@displayName: @events.Count(x => !ImplementedStates.Contains(x.Type)) events</summary> *@ + <summary>@displayName: @events.Count events</summary> + <LinkButton OnClick="@(() => { + events.Clear(); + StateHasChanged(); + })">Remove all + </LinkButton> + <LinkButton OnClick="@(() => { + events.Insert(0, new() { + Type = "", + StateKey = "", + RawContent = new(), + }); + StateHasChanged(); + })">Add new event + </LinkButton> + <br/> + @if (events.Count > 1000) { + <span style="color: red;">Warning: Too many initial state events! (more than 1000) - Please use the save/load feature in the state panel instead.</span> + } + else { + int i = 0; + @foreach (var initialState in events) { + <div id="@(initialState.Type + "/" + initialState.StateKey)"> + <span>Event @(++i) (@GetRemoveButton(events, initialState))</span> + <br/> + @* <FancyTextBox Multiline="true" Value="@initialState.ToJson(ignoreNull: true)" *@ + @* ValueChanged="@(json => { *@ + @* if (string.IsNullOrWhiteSpace(json)) *@ + @* events.Remove(initialState); *@ + @* else *@ + @* events.Replace(initialState, JsonSerializer.Deserialize<MatrixEvent>(json)); *@ + @* StateHasChanged(); *@ + @* })"></FancyTextBox> *@ + <FancyTextBoxLazyJson T="MatrixEvent" Value="@initialState" ValueChanged="@(evt => { events.Replace(initialState, evt); })"></FancyTextBoxLazyJson> + <br/> + </div> + } + } + </details> + } + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private RenderFragment GetRemoveButton(List<MatrixEvent> events, MatrixEvent initialState) { + return @<span> + <LinkButton InlineText="true" OnClick="@(() => { + events.Remove(initialState); + PageStateHasChanged(); + })">Remove</LinkButton> + </span>; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor new file mode 100644
index 0000000..6e300d4 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMembershipOptions.razor
@@ -0,0 +1,60 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +<tr> + <td>Invited members:</td> + <td> + <details> + <summary>@roomBuilder.Invites.Count members</summary> + <LinkButton OnClickAsync="@InviteAllSessions" InlineText="true">Invite all logged in accounts</LinkButton> + <br/> + @foreach (var member in roomBuilder.Invites) { + <FancyTextBox Value="@member.Key" ValueChanged="@(val => roomBuilder.Invites.ChangeKey(member.Key, val))"/> + @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@ + <span>: </span> + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Invites[member.Key] = val)"/> + <br/> + } + </details> + </td> +</tr> +<tr> + <td>Banned members:</td> + <td> + <details> + <summary>@roomBuilder.Bans.Count members</summary> + <br/> + @foreach (var member in roomBuilder.Bans) { + @* <UserListItem _homeserver="Homeserver" UserId="@member.Key"></UserListItem> *@ + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans.ChangeKey(member.Key, val))"/> + <span>: </span> + <FancyTextBox Value="@member.Value" ValueChanged="@(val => roomBuilder.Bans[member.Key] = val)"/> + } + </details> + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private async Task InviteAllSessions() { + var sessions = await sessionStore.GetAllSessions(); + foreach (var session in sessions) { + if (roomBuilder.Invites.ContainsKey(session.Value.Auth.UserId) || session.Value.Auth.UserId == Homeserver!.WhoAmI.UserId) continue; + Console.WriteLine("Inviting " + session.Value.Auth.UserId); + roomBuilder.Invites.Add(session.Value.Auth.UserId, null); + Console.WriteLine("--"); + } + + Console.WriteLine("Got all sessions, invited: " + string.Join(", ", roomBuilder.Invites.Keys)); + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor new file mode 100644
index 0000000..94e9638 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateMsc4321UpgradeOptions.razor
@@ -0,0 +1,19 @@ +@using LibMatrix.Helpers +<div style="border-left: solid 1px white; padding-left: 8px; margin-left: 8px;"> + <span>Policy list upgrade type:</span> + <InputSelect @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.UpgradeType"> + <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Move">Move policy list (copy policies)</option> + <option value="@RoomUpgradeBuilder.Msc4321PolicyListUpgradeOptions.Msc4321PolicyListUpgradeType.Transition">Transition policy list (new list)</option> + </InputSelect> + <br/> +</div> + +@code { + + [Parameter] + public required RoomUpgradeBuilder roomUpgrade { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor new file mode 100644
index 0000000..ba28b82 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePermissionsOptions.razor
@@ -0,0 +1,123 @@ +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +<tr> + <td>Permissions:</td> + <details> + <summary> + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + <span>@(roomBuilder.AdditionalCreators.Count + 1) creators, </span> + } + <span>@roomBuilder.PowerLevels.Users.Count members, @roomBuilder.PowerLevels.Events.Count events</span> + </summary> + + @if (roomBuilder.Version is "org.matrix.hydra.11" or "12") { + <span style="border-bottom: #444;">Creators:</span> + <br/> + <span>@Homeserver.WhoAmI.UserId (you - to change, visit <a href="/">the homepage</a>.)</span> + <br/> + + <StringListEditor @bind-Items="@roomBuilder.AdditionalCreators"></StringListEditor> + <br/> + } + + <span style="border-bottom: #444;">Events:</span><br/> + @foreach (var eventType in roomBuilder.PowerLevels.Events.Keys) { + var _event = eventType; + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Events.Remove(_event); + StateHasChanged(); + })">- + </LinkButton> + <div style="display: inline-flex;"> + <FancyTextBox Formatter="@GetPermissionFriendlyName" + Value="@_event" + ValueChanged="val => { roomBuilder.PowerLevels.Events.ChangeKey(_event, val); }"> + </FancyTextBox> + <span>:</span> + </div> + </td> + <td> + <input type="number" value="@roomBuilder.PowerLevels.Events[_event]" + @oninput="val => { roomBuilder.PowerLevels.Events[_event] = int.Parse(val.Value.ToString()); }" + @onfocusout="@(() => { roomBuilder.PowerLevels.Events = roomBuilder.PowerLevels.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/> + </td> + </tr> + } + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Events[""] = 0; + StateHasChanged(); + })">+ + </LinkButton> + </td> + </tr> + + <span style="border-bottom: #444;">Users:</span><br/> + @foreach (var user in roomBuilder.PowerLevels.Users.Keys) { + var _user = user; + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Users.Remove(_user); + StateHasChanged(); + })">- + </LinkButton> + <div style="display: inline-flex;"> + <FancyTextBox Value="@_user" + ValueChanged="val => { roomBuilder.PowerLevels.Users.ChangeKey(_user, val); }"> + </FancyTextBox> + <span>:</span> + </div> + </td> + <td> + <input type="number" value="@roomBuilder.PowerLevels.Users[_user]" + @oninput="val => { roomBuilder.PowerLevels.Users[_user] = int.Parse(val.Value.ToString()); }" + @onfocusout="@(() => { roomBuilder.PowerLevels.Users = roomBuilder.PowerLevels.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); })"/> + </td> + </tr> + } + <tr> + <td> + <LinkButton InlineText="true" OnClick="@(() => { + roomBuilder.PowerLevels.Users[""] = 0; + StateHasChanged(); + })">+ + </LinkButton> + </td> + </tr> + </details> +</tr> + + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + + private string GetPermissionFriendlyName(string key) => key switch { + "m.reaction" => "Send reaction", + "m.room.avatar" => "Change room icon", + "m.room.canonical_alias" => "Change room alias", + "m.room.encryption" => "Enable encryption", + "m.room.history_visibility" => "Change history visibility", + "m.room.name" => "Change room name", + "m.room.power_levels" => "Change power levels", + "m.room.tombstone" => "Upgrade room", + "m.room.topic" => "Change room topic", + "m.room.pinned_events" => "Pin events", + "m.room.server_acl" => "Change server ACLs", + "org.matrix.msc4284.policy" => "Change policy server", + "m.room.guest_access" => "Change guest access", + _ => key + }; + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor new file mode 100644
index 0000000..76f61c4 --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreatePrivacyOptions.razor
@@ -0,0 +1,70 @@ +@using LibMatrix.Helpers +<tr> + <td style="padding-top: 16px;">Join rules:</td> + <td style="padding-top: 16px;"> + <InputSelect @bind-Value="@roomBuilder.JoinRules.JoinRuleValue"> + <option value="public">Anyone can join</option> + <option value="invite">Invite only</option> + <option value="knock">Ask to join</option> + <option value="restricted">Invite only (or mutual room)</option> + <option value="knock_restricted">Ask to join (or mutual room)</option> + </InputSelect> + </td> +</tr> +<tr> + <td>History visibility:</td> + <td> + <InputSelect @bind-Value="@roomBuilder.HistoryVisibility.HistoryVisibility"> + <option value="invited">Since invite</option> + <option value="joined">Since join</option> + <option value="shared">Since room creation (members only)</option> + <option value="world_readable">World readable (everyone)</option> + </InputSelect> + </td> +</tr> +<tr> + <td>Guest access:</td> + <td> + <InputCheckbox @bind-Value="roomBuilder.GuestAccess.IsGuestAccessEnabled"/> + <span>Allow guests to join</span> + <LinkButton InlineText="true" href="https://spec.matrix.org/v1.15/client-server-api/#guest-access" target="_blank">?</LinkButton> + </td> +</tr> +<tr> + <td>Server ACLs:</td> + <td> + @if (roomBuilder.ServerAcls?.Allow is null) { + <p>No allow rules exist!</p> + <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Allow = ["*"]; })">Create sane defaults</LinkButton> + } + else { + <details> + <summary>@(roomBuilder.ServerAcls.Allow?.Count) allow rules</summary> + <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Allow"></StringListEditor> + </details> + } + @if (roomBuilder.ServerAcls?.Deny is null) { + <p>No deny rules exist!</p> + <LinkButton OnClick="@(() => { roomBuilder.ServerAcls!.Deny = []; })">Create sane defaults</LinkButton> + } + else { + <details> + <summary>@(roomBuilder.ServerAcls.Deny?.Count) deny rules</summary> + <StringListEditor @bind-Items="@roomBuilder.ServerAcls.Deny"></StringListEditor> + </details> + } + </td> +</tr> + +@code { + + [Parameter] + public required RoomBuilder roomBuilder { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + [Parameter] + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor new file mode 100644
index 0000000..eb373ba --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateStateDisplay.razor
@@ -0,0 +1,65 @@ +@using System.Text.Json +@using System.Text.Json.Nodes +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using LibMatrix.Helpers +@inject BlazorSaveFileService SaveFileService +<div + style="position: fixed; top: 56px; right: 0; width: fit-content; max-width: 25%; height: calc(100vh - 56px); overflow: auto; background-color: #2c3054; padding-right: 32px; border-left: 1px solid #ccc;"> + <details open> + <summary>RoomBuilder state</summary> + <InputCheckbox @bind-Value="@ShowNullInState"/> + <span>Show null values</span><br/> + <LinkButton OnClickAsync="@SaveFile">Save</LinkButton> + <SimpleFilePicker OnFilePicked="@LoadFile"/> + <br/> + <pre> + @RoomBuilder.ToJson(ignoreNull: !ShowNullInState) + </pre> + </details> +</div> + +@code { + + [Parameter] + public required RoomBuilder RoomBuilder { get; set; } + + [Parameter] + public required EventCallback<RoomBuilder> RoomBuilderChanged { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + + private bool ShowNullInState { get; set; } + + private async Task SaveFile() { + Console.WriteLine("Saving room builder state to file..."); + await SaveFileService.SaveFileAsync("room-builder.json", RoomBuilder.ToJson(), "application/json"); + } + + private async Task LoadFile(InputFileChangeEventArgs e) { + if (!RoomBuilderChanged.HasDelegate) throw new InvalidOperationException("RoomBuilderChanged must have a delegate."); + if (e.FileCount == 0) return; + Console.WriteLine("Loading room builder state from file..."); + var stream = e.File.OpenReadStream(4 * 1024 * 1024 * 1024L); + var json = await JsonSerializer.DeserializeAsync<JsonObject>(stream); + if (json is null) { + Console.WriteLine("Failed to deserialize JSON from file."); + return; + } + + if (json.ContainsKey(nameof(RoomUpgradeBuilder.UpgradeOptions))) { + Console.WriteLine("Got room upgrade builder state."); + RoomBuilder = json.Deserialize<RoomUpgradeBuilder>(); + } + else { + Console.WriteLine("Got room builder state."); + RoomBuilder = json.Deserialize<RoomBuilder>(); + } + + await RoomBuilderChanged.InvokeAsync(RoomBuilder); + PageStateHasChanged(); + Console.WriteLine("Room builder state loaded from file."); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor new file mode 100644
index 0000000..d4c4bfe --- /dev/null +++ b/MatrixUtils.Web/Pages/Rooms/RoomCreateComponents/RoomCreateUpgradeOptions.razor
@@ -0,0 +1,51 @@ +@using LibMatrix.Helpers +@using LibMatrix.RoomTypes +<tr> + <td>Room upgrade options</td> + <td> + @* <details> *@ + @* <summary>Upgrading from @roomUpgrade.OldRoom.RoomId</summary> *@ + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InviteMembers"></InputCheckbox> + <span>Invite members</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.InvitePowerlevelUsers"></InputCheckbox> + <span>Invite users with powerlevels</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateBans"></InputCheckbox> + <span>Copy bans (do not use with moderation bots!)</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.MigrateEmptyStateEvents"></InputCheckbox> + <span>Include empty state events</span> + <br/> + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.UpgradeUnstableValues"></InputCheckbox> + <span>Update unstable namespaced values to spec versions (experimental)</span> + <br/> + @if (roomUpgrade.Type == "support.feline.policy.lists.msc.v1") { + <InputCheckbox @bind-Value="@roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable"></InputCheckbox> + <span>Enable MSC4321 support</span> + <br/> + @if (roomUpgrade.UpgradeOptions.Msc4321PolicyListUpgradeOptions.Enable) { + <RoomCreateMsc4321UpgradeOptions roomUpgrade="@roomUpgrade" PageStateHasChanged="@PageStateHasChanged"/> + } + } + <LinkButton OnClickAsync="@(async () => { + await roomUpgrade.ImportAsync(OldRoom); + PageStateHasChanged(); + })">Apply + </LinkButton> + @* </details> *@ + </td> +</tr> + +@code { + + [Parameter] + public required GenericRoom OldRoom { get; set; } + + [Parameter] + public required RoomUpgradeBuilder roomUpgrade { get; set; } + + [Parameter] + public required Action PageStateHasChanged { get; set; } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Rooms/Space.razor b/MatrixUtils.Web/Pages/Rooms/Space.razor
index 46e39ed..93df5a9 100644 --- a/MatrixUtils.Web/Pages/Rooms/Space.razor +++ b/MatrixUtils.Web/Pages/Rooms/Space.razor
@@ -30,7 +30,7 @@ private GenericRoom? Room { get; set; } - private StateEventResponse[] States { get; set; } = Array.Empty<StateEventResponse>(); + private MatrixEventResponse[] States { get; set; } = Array.Empty<MatrixEventResponse>(); private List<RoomInfo> Rooms { get; } = new(); private List<string> ServersInSpace { get; } = new(); private string? NewRoomId { get; set; } @@ -61,6 +61,7 @@ } }); } + break; } case "m.room.member": { @@ -68,44 +69,46 @@ if (!ServersInSpace.Contains(serverName)) { ServersInSpace.Add(serverName); } + break; } } } + await base.OnInitializedAsync(); - // var state = await Room.GetStateAsync(""); - // if (state is not null) { - // // Console.WriteLine(state.Value.ToJson()); - // States = state.Value.Deserialize<StateEventResponse[]>()!; - // - // foreach (var stateEvent in States) { - // if (stateEvent.Type == "m.space.child") { - // // if (stateEvent.Content.ToJson().Length < 5) return; - // var roomId = stateEvent.StateKey; - // var room = hs.GetRoom(roomId); - // if (room is not null) { - // Rooms.Add(room); - // } - // } - // else if (stateEvent.Type == "m.room.member") { - // var serverName = stateEvent.StateKey.Split(':').Last(); - // if (!ServersInSpace.Contains(serverName)) { - // ServersInSpace.Add(serverName); - // } - // } - // } + // var state = await Room.GetStateAsync(""); + // if (state is not null) { + // // Console.WriteLine(state.Value.ToJson()); + // States = state.Value.Deserialize<MatrixEventResponse[]>()!; + // + // foreach (var stateEvent in States) { + // if (stateEvent.Type == "m.space.child") { + // // if (stateEvent.Content.ToJson().Length < 5) return; + // var roomId = stateEvent.StateKey; + // var room = hs.GetRoom(roomId); + // if (room is not null) { + // Rooms.Add(room); + // } + // } + // else if (stateEvent.Type == "m.room.member") { + // var serverName = stateEvent.StateKey.Split(':').Last(); + // if (!ServersInSpace.Contains(serverName)) { + // ServersInSpace.Add(serverName); + // } + // } + // } - // if(state.Value.TryGetProperty("Type", out var Type)) - // { - // } - // else - // { - // //this is fine, apprently... - // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); - // } + // if(state.Value.TryGetProperty("Type", out var Type)) + // { + // } + // else + // { + // //this is fine, apprently... + // //Console.WriteLine($"Room {room.RoomId} has no Content.Type in m.room.create!"); + // } - // await base.OnInitializedAsync(); + // await base.OnInitializedAsync(); } private async Task JoinAllRooms() { @@ -120,29 +123,30 @@ var room = Room!.Homeserver.GetRoom(roomId); if (room is null) return; try { - await room.JoinAsync(ServersInSpace.ToArray()); + await room.JoinAsync(ServersInSpace.Take(10).ToArray()); var joined = false; while (!joined) { var ce = await room.GetCreateEventAsync(); - if(ce is null) continue; + if (ce is null) continue; if (ce.Type == "m.space") { - var children = room.AsSpace.GetChildrenAsync(false); - await foreach (var child in children) { - JoinRecursive(child.RoomId); - } + var children = room.AsSpace().GetChildrenAsync(false); + await foreach (var child in children) { + JoinRecursive(child.RoomId); + } } + joined = true; + await Task.Delay(1000); } } catch (Exception e) { Console.WriteLine(e); } - } private async Task AddNewRoom() { if (string.IsNullOrWhiteSpace(NewRoomId)) return; - await Room.AsSpace.AddChildByIdAsync(NewRoomId); + await Room.AsSpace().AddChildByIdAsync(NewRoomId); } } diff --git a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
index 51cb265..47146bc 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateEditor.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateEditor.razor
@@ -37,8 +37,8 @@ [Parameter] public string? RoomId { get; set; } - public List<StateEventResponse> FilteredEvents { get; set; } = new(); - public List<StateEventResponse> Events { get; set; } = new(); + public List<MatrixEventResponse> FilteredEvents { get; set; } = new(); + public List<MatrixEventResponse> Events { get; set; } = new(); public string status = ""; protected override async Task OnInitializedAsync() { @@ -58,7 +58,7 @@ var StateLoaded = 0; var response = (hs.GetRoom(RoomId)).GetFullStateAsync(); await foreach (var _ev in response) { - // var e = new StateEventResponse { + // var e = new MatrixEventResponse { // Type = _ev.Type, // StateKey = _ev.StateKey, // OriginServerTs = _ev.OriginServerTs, @@ -68,6 +68,7 @@ if (string.IsNullOrEmpty(_ev.StateKey)) { FilteredEvents.Add(_ev); } + StateLoaded++; if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; @@ -103,11 +104,12 @@ public string content { get; set; } public long origin_server_ts { get; set; } public string state_key { get; set; } + public string type { get; set; } - // public string Sender { get; set; } - // public string EventId { get; set; } - // public string UserId { get; set; } - // public string ReplacesState { get; set; } + // public string Sender { get; set; } + // public string EventId { get; set; } + // public string UserId { get; set; } + // public string ReplacesState { get; set; } } public bool ShowMembershipEvents { diff --git a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
index c8b87d3..16b1d3d 100644 --- a/MatrixUtils.Web/Pages/Rooms/StateViewer.razor +++ b/MatrixUtils.Web/Pages/Rooms/StateViewer.razor
@@ -12,20 +12,20 @@ <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> - <tr> - <th scope="col">Type</th> - <th scope="col">Content</th> - </tr> - </thead> - <tbody> - @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) { <tr> - <td>@stateEvent.Type</td> - <td style="max-width: fit-Content;"> - <pre>@stateEvent.RawContent.ToJson()</pre> - </td> + <th scope="col">Type</th> + <th scope="col">Content</th> </tr> - } + </thead> + <tbody> + @foreach (var stateEvent in FilteredEvents.Where(x => x.StateKey == "").OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } </tbody> </table> @@ -34,20 +34,20 @@ <summary>@group.Key</summary> <table class="table table-striped table-hover" style="width: fit-Content;"> <thead> - <tr> - <th scope="col">Type</th> - <th scope="col">Content</th> - </tr> - </thead> - <tbody> - @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) { <tr> - <td>@stateEvent.Type</td> - <td style="max-width: fit-Content;"> - <pre>@stateEvent.RawContent.ToJson()</pre> - </td> + <th scope="col">Type</th> + <th scope="col">Content</th> </tr> - } + </thead> + <tbody> + @foreach (var stateEvent in group.OrderBy(x => x.OriginServerTs)) { + <tr> + <td>@stateEvent.Type</td> + <td style="max-width: fit-Content;"> + <pre>@stateEvent.RawContent.ToJson()</pre> + </td> + </tr> + } </tbody> </table> </details> @@ -64,8 +64,8 @@ [Parameter] public string? RoomId { get; set; } - public List<StateEventResponse> FilteredEvents { get; set; } = new(); - public List<StateEventResponse> Events { get; set; } = new(); + public List<MatrixEventResponse> FilteredEvents { get; set; } = new(); + public List<MatrixEventResponse> Events { get; set; } = new(); public string status = ""; protected override async Task OnInitializedAsync() { @@ -88,6 +88,7 @@ if (string.IsNullOrEmpty(_ev.StateKey)) { FilteredEvents.Add(_ev); } + StateLoaded++; if (!((DateTime.Now - _lastUpdate).TotalMilliseconds > 100)) continue; diff --git a/MatrixUtils.Web/Pages/Rooms/Timeline.razor b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
index 108581c..f9137b0 100644 --- a/MatrixUtils.Web/Pages/Rooms/Timeline.razor +++ b/MatrixUtils.Web/Pages/Rooms/Timeline.razor
@@ -3,6 +3,7 @@ @using LibMatrix @using LibMatrix.EventTypes.Spec @using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.Responses <h3>RoomManagerTimeline</h3> <hr/> <p>Loaded @Events.Count events...</p> @@ -21,7 +22,7 @@ public string RoomId { get; set; } private List<TimelineEventItem> Events { get; } = new(); - private List<StateEventResponse> RawEvents { get; } = new(); + private List<MatrixEventResponse> RawEvents { get; } = new(); private AuthenticatedHomeserverGeneric? Homeserver { get; set; } @@ -43,9 +44,9 @@ await base.OnInitializedAsync(); } - // private StateEventResponse GetProfileEventBefore(StateEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender); + // private MatrixEventResponse GetProfileEventBefore(MatrixEventResponse Event) => Events.TakeWhile(x => x != Event).Last(e => e.Type == RoomMemberEventContent.EventId && e.StateKey == Event.Sender); - private Type ComponentType(StateEvent Event) => Event.Type switch { + private Type ComponentType(MatrixEvent Event) => Event.Type switch { RoomCanonicalAliasEventContent.EventId => typeof(TimelineCanonicalAliasItem), RoomHistoryVisibilityEventContent.EventId => typeof(TimelineHistoryVisibilityItem), RoomTopicEventContent.EventId => typeof(TimelineRoomTopicItem), @@ -56,9 +57,9 @@ // RoomMessageReactionEventContent.EventId => typeof(ComponentBase), _ => typeof(TimelineUnknownItem) }; - + private class TimelineEventItem : ComponentBase { - public StateEventResponse Event { get; set; } + public MatrixEventResponse Event { get; set; } public Type Type { get; set; } } diff --git a/MatrixUtils.Web/Pages/ServerInfo.razor b/MatrixUtils.Web/Pages/ServerInfo.razor
index 8dd7907..3da93f2 100644 --- a/MatrixUtils.Web/Pages/ServerInfo.razor +++ b/MatrixUtils.Web/Pages/ServerInfo.razor
@@ -1,6 +1,7 @@ @page "/ServerInfo/{Homeserver}" @using LibMatrix.Responses @using ArcaneLibs.Extensions +@using LibMatrix.Responses.Federation <h3>Server info for @Homeserver</h3> <hr/> @if (ServerVersionResponse is not null) { diff --git a/MatrixUtils.Web/Pages/StreamTest.razor b/MatrixUtils.Web/Pages/StreamTest.razor
index 8b9735e..949bddc 100644 --- a/MatrixUtils.Web/Pages/StreamTest.razor +++ b/MatrixUtils.Web/Pages/StreamTest.razor
@@ -48,9 +48,9 @@ var members = roomState.Where(x => x.Type == RoomMemberEventContent.EventId).ToList(); Console.WriteLine($"Got {members.Count()} members"); var ss = new SemaphoreSlim(1, 1); - foreach (var stateEventResponse in members) { - // Console.WriteLine(stateEventResponse.ToJson()); - var mc = stateEventResponse.TypedContent as RoomMemberEventContent; + foreach (var MatrixEventResponse in members) { + // Console.WriteLine(MatrixEventResponse.ToJson()); + var mc = MatrixEventResponse.TypedContent as RoomMemberEventContent; if (!string.IsNullOrWhiteSpace(mc?.AvatarUrl)) { var uri = mc.AvatarUrl[6..].Split('/'); var url = $"/_matrix/media/v3/download/{uri[0]}/{uri[1]}"; @@ -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/JoinRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor new file mode 100644
index 0000000..cb56a40 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Debug/JoinRoom.razor
@@ -0,0 +1,70 @@ +@page "/Tools/Debug/JoinRoom" +@using System.Collections.ObjectModel +<h3>Join room</h3> +<hr/> +<span>Room ID: </span> +<InputText @bind-Value="@RoomId"></InputText> +<br/> +<span>Via server(s), comma separated: </span> +<InputText @bind-Value="@Servers"></InputText> +<br/> +<span>Unblock room (Synapse): </span> +<InputCheckbox @bind-Value="@Unblock"></InputCheckbox> +<br/> +<LinkButton OnClickAsync="@Join">Join</LinkButton> +<br/><br/> +@foreach (var line in Log) { + <pre>@line</pre> + <br/> +} + +@code { + AuthenticatedHomeserverGeneric? hs { get; set; } + ObservableCollection<string> Log { get; set; } = new ObservableCollection<string>(); + + [Parameter, SupplyParameterFromQuery(Name = "roomId")] + public string? RoomId { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "via")] + public string? Servers { get; set; } + + [Parameter, SupplyParameterFromQuery(Name = "unblock")] + public bool Unblock { get; set; } = false; + + protected override async Task OnInitializedAsync() { + hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true); + if (hs is null) return; + Log.CollectionChanged += (sender, args) => StateHasChanged(); + + StateHasChanged(); + Console.WriteLine("Rerendered!"); + await base.OnInitializedAsync(); + } + + private async Task Join() { + if (string.IsNullOrWhiteSpace(RoomId)) return; + var room = hs.GetRoom(RoomId); + Log.Add("Got room object..."); + + if (Unblock && hs is AuthenticatedHomeserverSynapse synapse) { + try { + await synapse.Admin.BlockRoom(RoomId, false); + Log.Add($"Synapse: unblocked room"); + } + catch (Exception e) { + Log.Add($"Synapse: failed to unblock room: {e}"); + } + } + + try { + await room.JoinAsync(Servers?.Split(','), checkIfAlreadyMember: false); + Log.Add("Joined room!"); + } + catch (Exception e) { + Log.Add(e.ToString()); + } + + Log.Add("Done!"); + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
index 9a56fc0..c40fa0b 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
@@ -1,11 +1,11 @@ -@page "/Tools/LeaveRoom" +@page "/Tools/Debug/LeaveRoom" @using System.Collections.ObjectModel <h3>Leave room</h3> <hr/> <span>Room ID: </span> <InputText @bind-Value="@RoomId"></InputText> <br/> -<LinkButton OnClick="@Leave">Leave</LinkButton> +<LinkButton OnClickAsync="@Leave">Leave</LinkButton> <br/><br/> @foreach (var line in Log) { <p>@line</p> diff --git a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 0943216..067036e 100644 --- a/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
@@ -17,7 +17,7 @@ </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -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/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index f99e932..a0abcd4 100644 --- a/MatrixUtils.Web/Pages/Tools/Index.razor +++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -12,6 +12,7 @@ <a href="/Tools/User/MassRoomJoin">Join room across all session</a><br/> <a href="/Tools/User/CopyPowerlevel">Copy highest powerlevel across all session</a><br/> <a href="/Tools/User/ViewAccountData">View account data</a><br/> +<a href="/Tools/User/StickerManager">Manage custom stickers and emojis</a><br/> <h4 class="tool-category">Room tools</h4> <hr/> @@ -30,6 +31,7 @@ <h4 class="tool-category">Debugging tools</h4> <hr/> <a href="/Tools/Debug/SpaceDebug">Debug space relationships</a><br/> +<a href="/Tools/Debug/JoinRoom">Join room by ID</a><br/> <a href="/Tools/Debug/LeaveRoom">Leave room by ID</a><br/> <a href="/Tools/Debug/MediaLocator">Locate lost media</a><br/> <a href="/Tools/Debug/MigrateRoom">Migrate users from a split room to a new room</a><br/> 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..ba8036c 100644 --- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor +++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -74,7 +74,7 @@ else } //use timeline - var types = StateEventResponse.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))); + var types = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))); var filter = new SyncFilter.EventFilter(types: types.SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName)).ToList()); var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 2500, filter: filter.ToJson(indent: false, ignoreNull: true)); await foreach (var response in timeline) { @@ -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)), @@ -120,10 +119,10 @@ else private readonly struct StateEventEntry { public required DateTime Timestamp { get; init; } public required StateEventTransition State { get; init; } - public required StateEventResponse Event { get; init; } - public required StateEventResponse? Previous { get; init; } + public required MatrixEventResponse Event { get; init; } + public required MatrixEventResponse? Previous { get; init; } - public void Deconstruct(out StateEventTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + public void Deconstruct(out StateEventTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) { transition = State; evt = Event; prev = Previous; diff --git a/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index ce3513b..76ff629 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 OnClick="@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> @@ -21,7 +22,7 @@ } </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <details> @@ -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> @@ -61,7 +60,7 @@ private ObservableCollection<string> log { get; set; } = new(); List<AuthenticatedHomeserverGeneric> hss { get; set; } = new(); ObservableCollection<GenericRoom> rooms { get; set; } = new(); - Dictionary<GenericRoom, FrozenSet<StateEventResponse>> roomMembers { get; set; } = new(); + Dictionary<GenericRoom, FrozenSet<MatrixEventResponse>> roomMembers { get; set; } = new(); Dictionary<string, List<Matches>> matches = new(); private string UserIdString { @@ -97,7 +96,7 @@ rooms = new ObservableCollection<GenericRoom>(distinctRooms); rooms.CollectionChanged += (sender, args) => StateHasChanged(); - var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync(false))).ToAsyncEnumerable(); + var stateTasks = rooms.Select(async x => (x, await x.GetMembersListAsync())).ToAsyncResultEnumerable(); await foreach (var (room, state) in stateTasks) { roomMembers.Add(room, state); @@ -148,7 +147,7 @@ private class Matches { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; // public } diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
index 2313884..16a3853 100644 --- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
@@ -9,7 +9,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
index a252e6b..5b0f510 100644 --- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
@@ -7,7 +7,7 @@ <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
index b0d5a65..1ff97c8 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -53,7 +53,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index ea39c9a..9b0266c 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -49,7 +49,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
index 9e70687..69a9048 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -49,7 +49,7 @@ </div> } <br/> -<LinkButton OnClick="@Apply">Apply</LinkButton> +<LinkButton OnClickAsync="@Apply">Apply</LinkButton> @code { diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
index b62cf57..9139561 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -14,10 +14,10 @@ <p>Users (regex): </p> <InputTextArea @bind-Value="@UserIdString"></InputTextArea> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> -<LinkButton OnClick="RemoveKicks">Remove kicks</LinkButton> -<LinkButton OnClick="RemoveBans">Remove bans</LinkButton> +<LinkButton OnClickAsync="RemoveKicks">Remove kicks</LinkButton> +<LinkButton OnClickAsync="RemoveBans">Remove bans</LinkButton> <br/> @@ -145,7 +145,7 @@ private class Match { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; public string RoomName { get; set; } } @@ -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/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 5c5946f..ac68e3d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -9,7 +9,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@roomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index 8fdad84..605890d 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -8,7 +8,7 @@ <br/> <span>Users:</span> <InputTextArea @bind-Value="@roomId"></InputTextArea> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index 1ec3cd0..ec1d190 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -16,7 +16,7 @@ <br/> <span>Room ID: </span> <InputText @bind-Value="@RoomId"></InputText> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <p> <span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span> <span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span> @@ -30,24 +30,24 @@ <span><InputCheckbox @bind-Value="ShowBans"/> bans</span> </p> <p> - <LinkButton OnClick="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; - StateHasChanged(); - })">Hide all + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false; + StateHasChanged(); + })">Hide all </LinkButton> - <LinkButton OnClick="@(async () => { - ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; - StateHasChanged(); - })">Show all + <LinkButton OnClickAsync="@(async () => { + ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = true; + StateHasChanged(); + })">Show all </LinkButton> - <LinkButton OnClick="@(async () => { - ShowJoins ^= true; - ShowLeaves ^= true; - ShowKnocks ^= true; - ShowInvites ^= true; - ShowBans ^= true; - StateHasChanged(); - })">Toggle all + <LinkButton OnClickAsync="@(async () => { + ShowJoins ^= true; + ShowLeaves ^= true; + ShowKnocks ^= true; + ShowInvites ^= true; + ShowBans ^= true; + StateHasChanged(); + })">Toggle all </LinkButton> </p> <p> @@ -56,25 +56,25 @@ <span><InputCheckbox @bind-Value="DisambiguateKicks"/> kicks</span> <span><InputCheckbox @bind-Value="DisambiguateUnbans"/> unbans</span> <span><InputCheckbox @bind-Value="DisambiguateProfileUpdates"/> profile updates</span> - <details style="display: inline-block; vertical-align: top;"> - <summary> - <InputCheckbox @bind-Value="DisambiguateInviteActions"/> - invite actions - </summary> - <span><InputCheckbox @bind-Value="DisambiguateInviteAccepted"/> accepted</span> - <span><InputCheckbox @bind-Value="DisambiguateInviteRejected"/> rejected</span> - <span><InputCheckbox @bind-Value="DisambiguateInviteRetracted"/> retracted</span> - </details> - <details style="display: inline-block; vertical-align: top;"> - <summary> - <InputCheckbox @bind-Value="DisambiguateKnockActions"/> - knock actions - </summary> - <span><InputCheckbox @bind-Value="DisambiguateKnockAccepted"/> accepted</span> - <span><InputCheckbox @bind-Value="DisambiguateKnockRejected"/> rejected</span> - <span><InputCheckbox @bind-Value="DisambiguateKnockRetracted"/> retracted</span> - </details> - } + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="DisambiguateInviteActions"/> + invite actions + </summary> + <span><InputCheckbox @bind-Value="DisambiguateInviteAccepted"/> accepted</span> + <span><InputCheckbox @bind-Value="DisambiguateInviteRejected"/> rejected</span> + <span><InputCheckbox @bind-Value="DisambiguateInviteRetracted"/> retracted</span> + </details> + <details style="display: inline-block; vertical-align: top;"> + <summary> + <InputCheckbox @bind-Value="DisambiguateKnockActions"/> + knock actions + </summary> + <span><InputCheckbox @bind-Value="DisambiguateKnockAccepted"/> accepted</span> + <span><InputCheckbox @bind-Value="DisambiguateKnockRejected"/> rejected</span> + <span><InputCheckbox @bind-Value="DisambiguateKnockRetracted"/> retracted</span> + </details> +} </p> @if (DoDisambiguate) { <p> @@ -129,30 +129,30 @@ </p> <p> - <LinkButton OnClick="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; - StateHasChanged(); - })">Un-disambiguate all + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = false; + StateHasChanged(); + })">Un-disambiguate all </LinkButton> - <LinkButton OnClick="@(async () => { - DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; - StateHasChanged(); - })">Disambiguate all + <LinkButton OnClickAsync="@(async () => { + DoDisambiguate = DisambiguateProfileUpdates = DisambiguateKicks = DisambiguateUnbans = DisambiguateInviteAccepted = DisambiguateInviteRejected = DisambiguateInviteRetracted = DisambiguateKnockAccepted = DisambiguateKnockRejected = DisambiguateKnockRetracted = DisambiguateKnockActions = DisambiguateInviteActions = true; + StateHasChanged(); + })">Disambiguate all </LinkButton> - <LinkButton OnClick="@(async () => { - DisambiguateProfileUpdates ^= true; - DisambiguateKicks ^= true; - DisambiguateUnbans ^= true; - DisambiguateInviteAccepted ^= true; - DisambiguateInviteRejected ^= true; - DisambiguateInviteRetracted ^= true; - DisambiguateKnockAccepted ^= true; - DisambiguateKnockRejected ^= true; - DisambiguateKnockRetracted ^= true; - DisambiguateKnockActions ^= true; - DisambiguateInviteActions ^= true; - StateHasChanged(); - })">Toggle all + <LinkButton OnClickAsync="@(async () => { + DisambiguateProfileUpdates ^= true; + DisambiguateKicks ^= true; + DisambiguateUnbans ^= true; + DisambiguateInviteAccepted ^= true; + DisambiguateInviteRejected ^= true; + DisambiguateInviteRetracted ^= true; + DisambiguateKnockAccepted ^= true; + DisambiguateKnockRejected ^= true; + DisambiguateKnockRetracted ^= true; + DisambiguateKnockActions ^= true; + DisambiguateInviteActions ^= true; + StateHasChanged(); + })">Toggle all </LinkButton> </p> } @@ -306,18 +306,61 @@ private bool ShowBans { get; set; } = true; private bool DoDisambiguate { get; set; } = true; - private bool DisambiguateProfileUpdates { get => field && DoDisambiguate; set; } = true; - private bool DisambiguateKicks { get => field && DoDisambiguate; set; } = true; - private bool DisambiguateUnbans { get => field && DoDisambiguate; set; } = true; - private bool DisambiguateInviteAccepted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; - private bool DisambiguateInviteRejected { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; - private bool DisambiguateInviteRetracted { get => field && DoDisambiguate && DisambiguateInviteActions; set; } = true; - private bool DisambiguateKnockAccepted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; - private bool DisambiguateKnockRejected { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; - private bool DisambiguateKnockRetracted { get => field && DoDisambiguate && DisambiguateKnockActions; set; } = true; - - private bool DisambiguateKnockActions { get => field && DoDisambiguate; set; } = true; - private bool DisambiguateInviteActions { get => field && DoDisambiguate; set; } = true; + + private bool DisambiguateProfileUpdates { + get => field && DoDisambiguate; + set; + } = true; + + private bool DisambiguateKicks { + get => field && DoDisambiguate; + set; + } = true; + + private bool DisambiguateUnbans { + get => field && DoDisambiguate; + set; + } = true; + + private bool DisambiguateInviteAccepted { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + private bool DisambiguateInviteRejected { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + private bool DisambiguateInviteRetracted { + get => field && DoDisambiguate && DisambiguateInviteActions; + set; + } = true; + + private bool DisambiguateKnockAccepted { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + private bool DisambiguateKnockRejected { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + private bool DisambiguateKnockRetracted { + get => field && DoDisambiguate && DisambiguateKnockActions; + set; + } = true; + + private bool DisambiguateKnockActions { + get => field && DoDisambiguate; + set; + } = true; + + private bool DisambiguateInviteActions { + get => field && DoDisambiguate; + set; + } = true; private bool ShowProfileUpdates { get => field && DisambiguateProfileUpdates; @@ -399,7 +442,7 @@ #endregion private ObservableCollection<string> Log { get; set; } = new(); - private List<StateEventResponse> Memberships { get; set; } = []; + private List<MatrixEventResponse> Memberships { get; set; } = []; private AuthenticatedHomeserverGeneric Homeserver { get; set; } [Parameter, SupplyParameterFromQuery(Name = "room")] @@ -444,10 +487,10 @@ private readonly struct MembershipEntry { public required MembershipTransition State { get; init; } - public required StateEventResponse Event { get; init; } - public required StateEventResponse? Previous { get; init; } + public required MatrixEventResponse Event { get; init; } + public required MatrixEventResponse? Previous { get; init; } - public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) { + public void Deconstruct(out MembershipTransition transition, out MatrixEventResponse evt, out MatrixEventResponse? prev) { transition = State; evt = Event; prev = Previous; @@ -474,7 +517,7 @@ KnockRetracted } - private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> evts) { + private static IEnumerable<MembershipEntry> GetTransitions(List<MatrixEventResponse> evts) { Dictionary<string, MembershipEntry> transitions = new(); foreach (var evt in evts.OrderBy(x => x.OriginServerTs)) { var content = evt.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!"); @@ -528,7 +571,7 @@ { MembershipTransition.KnockRejected, MembershipTransition.Leave }, { MembershipTransition.KnockRetracted, MembershipTransition.Leave } }.ToFrozenDictionary(); - + foreach (var entry in entries) { if (!DoDisambiguate) { yield return entry; diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index 736e59a..a8ae603 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -8,13 +8,13 @@ <p>Set A: </p> <InputText @bind-Value="@ImportSetASpaceId"></InputText> -<LinkButton OnClick="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton> +<LinkButton OnClickAsync="@(() => AppendSet(ImportSetASpaceId, RoomsA))">Append Set A</LinkButton> <p>Set B: </p> <InputText @bind-Value="@ImportSetBSpaceId"></InputText> -<LinkButton OnClick="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton> +<LinkButton OnClickAsync="@(() => AppendSet(ImportSetBSpaceId, RoomsB))">Append Set B</LinkButton> <br/> -<LinkButton OnClick="@Execute">Execute</LinkButton> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> <br/> <details> @@ -55,7 +55,7 @@ <td>@sets.Item2[0].Room.RoomId</td> <td>@((sets.Item2[i].Member.TypedContent as RoomMemberEventContent).Membership)</td> <td>@(roomNames.ContainsKey(sets.Item2[i].Room) ? roomNames[sets.Item2[i].Room] : "")</td> - <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td> + <td>@(roomAliasses.ContainsKey(sets.Item2[i].Room) ? roomAliasses[sets.Item2[i].Room] : "")</td> } else { <td/> @@ -88,7 +88,7 @@ [Parameter, SupplyParameterFromQuery(Name = "b")] public string ImportSetBSpaceId { get; set; } = ""; - Dictionary<string, Dictionary<GenericRoom, StateEventResponse>> roomMembers { get; set; } = new(); + Dictionary<string, Dictionary<GenericRoom, MatrixEventResponse>> roomMembers { get; set; } = new(); Dictionary<string, (List<Match>, List<Match>)> matches { get; set; } = new(); @@ -127,7 +127,7 @@ var setBusers = new Dictionary<string, List<Match>>(); await Task.WhenAll(GetMembers(RoomsA, setAusers), GetMembers(RoomsB, setBusers)); - + Log.Add($"Got {setAusers.Count} users in set A"); Log.Add($"Got {setBusers.Count} users in set B"); Log.Add("Calculating intersections..."); @@ -144,7 +144,7 @@ public async Task GetMembers(List<GenericRoom> rooms, Dictionary<string, List<Match>> users) { foreach (var room in rooms) { Log.Add($"Getting members for {room.RoomId}"); - var members = await room.GetMembersListAsync(false); + var members = await room.GetMembersListAsync(); foreach (var member in members) { if (member.RawContent?["membership"]?.ToString() == "ban") continue; if (member.RawContent?["membership"]?.ToString() == "invite") continue; @@ -158,7 +158,7 @@ } public async Task AppendSet(string spaceId, List<GenericRoom> rooms) { - var space = hs.GetRoom(spaceId).AsSpace; + var space = hs.GetRoom(spaceId).AsSpace(); Log.Add($"Found space {spaceId}"); var roomIdsEnum = space.GetChildrenAsync(true); List<Task> tasks = new(); @@ -191,7 +191,7 @@ public class Match { public GenericRoom Room { get; set; } - public StateEventResponse Member { get; set; } + public MatrixEventResponse Member { get; set; } } } \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index c3cc09c..d160922 100644 --- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor +++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -11,7 +11,7 @@ <InputTextArea @bind-Value="@UserIdString"></InputTextArea> <br/> <InputText @bind-Value="@ImportFromRoomId"></InputText> -<LinkButton OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton> +<LinkButton OnClickAsync="@DoImportFromRoomId">Import from room (ID)</LinkButton> <details> <summary>Rooms to be searched (@rooms.Count)</summary> @@ -21,15 +21,15 @@ } </details> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <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> @@ -139,7 +139,7 @@ private class Match { public GenericRoom Room; - public StateEventResponse Event; + public MatrixEventResponse Event; public string RoomName { get; set; } } @@ -161,7 +161,7 @@ } return null; - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var result in results) { if (result is not null) { yield return result; @@ -169,7 +169,7 @@ } } - public string SummarizeMembership(StateEventResponse state) { + public string SummarizeMembership(MatrixEventResponse state) { var membership = state.ContentAs<RoomMemberEventContent>(); var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value); return membership switch { @@ -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/Room/DropPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor new file mode 100644
index 0000000..208cd19 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Room/DropPowerlevel.razor
@@ -0,0 +1,51 @@ +@page "/Tools/Room/DropPowerlevel" +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +<h3>DropPowerlevel</h3> +<hr/> + +<span>User ID: </span><FancyTextBox @bind-Value="@UserId"/><br/> +<span>Room ID: </span><FancyTextBox @bind-Value="@RoomId"/><br/> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> + +<pre>@Result</pre> + +@code { + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } = null!; + + [Parameter, SupplyParameterFromQuery(Name = "RoomId")] + public string RoomId { get; set; } = ""; + + [Parameter, SupplyParameterFromQuery(Name = "UserId")] + public string UserId { get; set; } = ""; + + private string Result { get; set; } = ""; + + protected override async Task OnInitializedAsync() { + Homeserver = await sessionStore.GetCurrentHomeserver(); + Result = "I am: " + Homeserver.WhoAmI.ToJson() + "\n"; + StateHasChanged(); + } + + private async Task Execute() { + try { + if (Homeserver is not AuthenticatedHomeserverGeneric hs) { + Result = "Not authenticated"; + return; + } + + var room = hs.GetRoom(RoomId); + + var powerlevels = await room.GetPowerLevelsAsync(); + powerlevels.Users.Remove(UserId); + Result = (await room.SendStateEventAsync(RoomPowerLevelEventContent.EventId, powerlevels)).ToJson(); + } + catch (Exception e) { + Result = e.Message; + } + finally { + StateHasChanged(); + } + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor new file mode 100644
index 0000000..a47d7f5 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/Room/SpacePermissions.razor
@@ -0,0 +1,204 @@ +@page "/Tools/Room/SpacePermissions" +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Spec.State.RoomInfo +@using LibMatrix.RoomTypes +@using MatrixUtils.Web.Pages.Rooms +<h3>Space Permissions</h3> +<hr/> +<span>Space ID: </span> +<FancyTextBox @bind-Value="@SpaceId"/> +<LinkButton OnClickAsync="@Execute">Execute</LinkButton> +<br/> +<InputCheckbox @bind-Value="@AutoRecurseSpaces"/> +<span> Auto-recurse into child spaces</span> +<br/> + +@if (RoomPowerLevels.Count == 0) { + <p>No data loaded.</p> +} +else { + <span>Loaded @LoadedSpaceRooms.Count spaces.</span> + <br/> + @if (SpaceRooms.Count > 0) { + <h3>Load more spaces:</h3> + @foreach (var room in SpaceRooms) { + <LinkButton OnClickAsync="@(() => LoadSpaceAsync(room.Key))">@room.Value</LinkButton> + } + } + + <h3>By event type:</h3> + <table class="table-striped table-hover table-bordered align-middle"> + <thead> + <td>Room</td> + @foreach (var key in OrderedEventTypes) { + <td>@key.Key + <br/> + ~ @Math.Round(key.Value, 2) + </td> + } + </thead> + <tbody> + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Events!.Values.Average())) { + <tr> + <td>@roomName</td> + @foreach (var eventType in OrderedEventTypes) { + if (!powerLevels.Events!.ContainsKey(eventType.Key)) { + <td style="background-color: #ff000044;">-</td> + continue; + } + + <td>@(powerLevels.Events![eventType.Key])</td> + } + </tr> + } + </tbody> + </table> + <br/> + <h3>By user:</h3> + <table class="table-striped table-hover table-bordered align-middle"> + <thead> + <td>Room</td> + @foreach (var key in OrderedUsers) { + <td>@key.Key + <br/> + ~ @Math.Round(key.Value, 2) + </td> + } + </thead> + <tbody> + @foreach (var (roomName, powerLevels) in RoomPowerLevels.OrderByDescending(x => x.Value.Users!.Values.Average())) { + <tr> + <td>@roomName</td> + @foreach (var eventType in OrderedUsers) { + if (!powerLevels.Users!.ContainsKey(eventType.Key)) { + <td style="background-color: #ff000044;">-</td> + continue; + } + + <td>@(powerLevels.Users![eventType.Key])</td> + } + </tr> + } + </tbody> + </table> +} + +@code { + + [Parameter, SupplyParameterFromQuery] + public string? SpaceId { get; set; } + + [Parameter, SupplyParameterFromQuery] + public bool AutoRecurseSpaces { get; set; } + + private AuthenticatedHomeserverGeneric? Homeserver { get; set; } + private List<AuthenticatedHomeserverGeneric> AllHomeservers { get; set; } = []; + private Dictionary<string, List<GenericRoom>> JoinedHomeserversByRoom { get; set; } = []; + + private Dictionary<string, RoomPowerLevelEventContent> RoomPowerLevels { get; set; } = []; + private Dictionary<string, string> SpaceRooms { get; set; } = []; + private List<string> LoadedSpaceRooms { get; set; } = []; + + private Dictionary<string, double> OrderedEventTypes { get; set; } = new(); + private Dictionary<string, double> OrderedUsers { get; set; } = new(); + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not AuthenticatedHomeserverGeneric hs) return; + Homeserver = hs; + await foreach (var server in sessionStore.TryGetAllHomeservers()) { + AllHomeservers.Add(server); + var joinedRooms = await server.GetJoinedRooms(); + foreach (var room in joinedRooms) { + if (!JoinedHomeserversByRoom.ContainsKey(room.RoomId)) { + JoinedHomeserversByRoom[room.RoomId] = []; + } + + JoinedHomeserversByRoom[room.RoomId].Add(room); + } + } + + if (!string.IsNullOrWhiteSpace(SpaceId)) { + await Execute(); + } + } + + private async Task Execute() { + RoomPowerLevels = []; + SpaceRooms = []; + await LoadSpaceAsync(SpaceId); + } + + private async Task<GenericRoom> GetJoinedRoomAsync(string roomId) { + var room = Homeserver.GetRoom(roomId); + if (await room.IsJoinedAsync()) return room; + + if (JoinedHomeserversByRoom.TryGetValue(roomId, out var rooms)) { + foreach (var r in rooms) { + if (await r.IsJoinedAsync()) return r; + } + } + + foreach (var hs in AllHomeservers) { + if (hs == Homeserver) continue; + room = hs.GetRoom(roomId); + if (await room.IsJoinedAsync()) return room; + } + + Console.WriteLine($"Not joined to room {roomId} on any known homeserver."); + return room; // not null, in case we can preview the room + } + + private async Task LoadSpaceAsync(string spaceId) { + LoadedSpaceRooms.Add(spaceId); + SpaceRooms.Remove(spaceId); + + var space = (await GetJoinedRoomAsync(spaceId)).AsSpace(); + RoomPowerLevels[await space.GetNameOrFallbackAsync()] = AddFakeEvents(await space.GetPowerLevelsAsync()); + var children = space.GetChildrenAsync(); + await foreach (var childRoom in children) { + var child = await GetJoinedRoomAsync(childRoom.RoomId); + try { + var powerlevels = await child.GetPowerLevelsAsync(); + RoomPowerLevels[await child.GetNameOrFallbackAsync()] = AddFakeEvents(powerlevels!); + if (await child.GetRoomType() == SpaceRoom.TypeName) { + if (AutoRecurseSpaces) + await LoadSpaceAsync(child.RoomId); + else + SpaceRooms.Add(child.RoomId, await child.GetNameOrFallbackAsync()); + } + + OrderedEventTypes = RoomPowerLevels + .SelectMany(x => x.Value.Events!) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Average(y => y.Value)) + .OrderByDescending(x => x.Value) + .ToDictionary(x => x.Key, x => x.Value); + + OrderedUsers = RoomPowerLevels + .SelectMany(x => x.Value.Users!) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Average(y => y.Value)) + .OrderByDescending(x => x.Value) + .ToDictionary(x => x.Key, x => x.Value); + StateHasChanged(); + } + catch (Exception ex) { + Console.WriteLine($"Failed to get power levels for room {child.RoomId}: {ex}"); + } + } + } + + private RoomPowerLevelEventContent AddFakeEvents(RoomPowerLevelEventContent powerlevels) { + powerlevels.Events ??= []; + powerlevels.Events["[user_default]"] = powerlevels.UsersDefault ?? 0; + powerlevels.Events["[event_default]"] = powerlevels.EventsDefault ?? 0; + powerlevels.Events["[state_default]"] = powerlevels.StateDefault ?? 100; + powerlevels.Events["[ban]"] = powerlevels.Ban ?? 100; + powerlevels.Events["[invite]"] = powerlevels.Invite ?? 100; + powerlevels.Events["[kick]"] = powerlevels.Kick ?? 100; + powerlevels.Events["[ping_room]"] = powerlevels.NotificationsPl?.Room ?? 100; + powerlevels.Events["[redact]"] = powerlevels.Redact ?? 100; + return powerlevels; + } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
index ac3c651..d6ae945 100644 --- a/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor +++ b/MatrixUtils.Web/Pages/Tools/Room/SpaceRestrictedJoins.razor
@@ -10,7 +10,7 @@ <p><InputCheckbox @bind-Value="@ChangeKnocking"/> Change knock access: <InputCheckbox @bind-Value="@Knocking"/></p> <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> <br/> @@ -40,7 +40,7 @@ } private async Task Execute() { - var space = hs.GetRoom(RoomId).AsSpace; + var space = hs.GetRoom(RoomId).AsSpace(); await foreach (var room in space.GetChildrenAsync()) { log.Add($"Got room {room.RoomId}"); if (ChangeGuestAccess) { diff --git a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index e5ffd5b..acc86a2 100644 --- a/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor +++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
@@ -12,7 +12,7 @@ } <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -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 c373a37..ee17f1d 100644 --- a/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor +++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
@@ -13,7 +13,7 @@ } <br/> -<LinkButton OnClick="Execute">Execute</LinkButton> +<LinkButton OnClickAsync="Execute">Execute</LinkButton> <br/> @foreach (var line in Enumerable.Reverse(log)) { <p>@line</p> @@ -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 new file mode 100644
index 0000000..0e838c7 --- /dev/null +++ b/MatrixUtils.Web/Pages/Tools/User/StickerManager.razor
@@ -0,0 +1,80 @@ +@page "/Tools/User/StickerManager" +@using System.Diagnostics +@using ArcaneLibs.Extensions +@using LibMatrix.EventTypes.Common +@using LibMatrix.EventTypes.Spec +@inject ILogger<StickerManager> Logger +<h3>Sticker/emoji manager</h3> + +@if (TotalStepsProgress is not null) { + <SimpleProgressIndicator ObservableProgress="@TotalStepsProgress"/> + <br/> +} +@if (_observableProgressState is not null) { + <SimpleProgressIndicator ObservableProgress="@_observableProgressState"/> + <br/> +} + +@code { + + private AuthenticatedHomeserverGeneric Homeserver { get; set; } = null!; + private Msc2545EmoteRoomsAccountDataEventContent? EnabledEmoteRooms { get; set; } + private Dictionary<string, StickerRoom> StickerRooms { get; set; } = []; + + private SimpleProgressIndicator.ObservableProgressState? _observableProgressState; + + private SimpleProgressIndicator.ObservableProgressState? TotalStepsProgress { get; set; } = new() { + Label = "Authenticating with Matrix...", + Max = 2, + Value = 0 + }; + + protected override async Task OnInitializedAsync() { + if (await sessionStore.GetCurrentHomeserver(navigateOnFailure: true) is not { } hs) + return; + Homeserver = hs; + TotalStepsProgress?.Next("Fetching enabled emote packs..."); + _ = hs.GetAccountDataOrNullAsync<Msc2545EmoteRoomsAccountDataEventContent>(Msc2545EmoteRoomsAccountDataEventContent.EventId) + .ContinueWith(r => { + EnabledEmoteRooms = r.Result; + StateHasChanged(); + }); + + TotalStepsProgress?.Next("Getting joined rooms..."); + _observableProgressState = new() { + Label = "Loading rooms...", + Max = 1, + Value = 0 + }; + var rooms = await hs.GetJoinedRooms(); + _observableProgressState.Max.Value = rooms.Count; + StateHasChanged(); + + var ss = new SemaphoreSlim(32, 32); + var ss1 = new SemaphoreSlim(1, 1); + var roomScanTasks = rooms.Select(async room => { + // await Task.Delay(Random.Shared.Next(100, 1000 + (rooms.Count * 100))); + // await ss.WaitAsync(); + var state = await room.GetFullStateAsListAsync(); + StickerRoom sr = new(); + foreach (var evt in state) { + if (evt.Type == RoomEmotesEventContent.EventId) { } + } + + // ss.Release(); + // await ss1.WaitAsync(); + Console.WriteLine("Got state for room " + room.RoomId); + // _observableProgressState.Next($"Got state for room {room.RoomId}"); + // await Task.Delay(1); + // ss1.Release(); + return room.RoomId; + }) + .ToList(); + await foreach (var roomScanResult in roomScanTasks.ToAsyncResultEnumerable()) { + _observableProgressState.Label.Value = roomScanResult; + } + } + + private class StickerRoom { } + +} \ No newline at end of file diff --git a/MatrixUtils.Web/Pages/User/Profile.razor b/MatrixUtils.Web/Pages/User/Profile.razor
index ccd3e7b..2b7b6cf 100644 --- a/MatrixUtils.Web/Pages/User/Profile.razor +++ b/MatrixUtils.Web/Pages/User/Profile.razor
@@ -12,11 +12,15 @@ <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/> - <LinkButton OnClick="@(() => UpdateProfile())">Update profile</LinkButton> - <LinkButton OnClick="@(() => UpdateProfile(true))">Update profile (restore room overrides)</LinkButton> + <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> </div> @if (!string.IsNullOrWhiteSpace(Status)) { @@ -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,10 +47,16 @@ @* <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/> - <LinkButton OnClick="@(() => UpdateRoomProfile(room.Room.RoomId))">Update profile</LinkButton> + <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/> @if (!string.IsNullOrWhiteSpace(Status)) { @@ -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}";