about summary refs log tree commit diff
path: root/MatrixUtils.Web/Pages/Tools
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixUtils.Web/Pages/Tools')
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor (renamed from MatrixUtils.Web/Pages/Tools/LeaveRoom.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor (renamed from MatrixUtils.Web/Pages/Tools/MediaLocator.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor (renamed from MatrixUtils.Web/Pages/Tools/MigrateRoom.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor (renamed from MatrixUtils.Web/Pages/Tools/SpaceDebug.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor39
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor.css6
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor (renamed from MatrixUtils.Web/Pages/Tools/KnownHomeserverList.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor (renamed from MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor.css (renamed from MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor.css)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor (renamed from MatrixUtils.Web/Pages/Tools/SessionCount.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor102
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor (renamed from MatrixUtils.Web/Pages/Tools/InviteCounter.razor)9
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor (renamed from MatrixUtils.Web/Pages/Tools/MassCMEBan.razor)8
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor276
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor (renamed from MatrixUtils.Web/Pages/Tools/RoomIntersections.razor)2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor (renamed from MatrixUtils.Web/Pages/Tools/UserTrace.razor)6
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor (renamed from MatrixUtils.Web/Pages/Tools/CopyPowerlevel.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor (renamed from MatrixUtils.Web/Pages/Tools/MassJoinRoom.razor)0
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor (renamed from MatrixUtils.Web/Pages/Tools/ViewAccountData.razor)0
19 files changed, 419 insertions, 29 deletions
diff --git a/MatrixUtils.Web/Pages/Tools/LeaveRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
index 841552e..841552e 100644
--- a/MatrixUtils.Web/Pages/Tools/LeaveRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/LeaveRoom.razor
diff --git a/MatrixUtils.Web/Pages/Tools/MediaLocator.razor b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
index 6e87926..6e87926 100644
--- a/MatrixUtils.Web/Pages/Tools/MediaLocator.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MediaLocator.razor
diff --git a/MatrixUtils.Web/Pages/Tools/MigrateRoom.razor b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
index 11d35f1..11d35f1 100644
--- a/MatrixUtils.Web/Pages/Tools/MigrateRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/MigrateRoom.razor
diff --git a/MatrixUtils.Web/Pages/Tools/SpaceDebug.razor b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
index 263879b..263879b 100644
--- a/MatrixUtils.Web/Pages/Tools/SpaceDebug.razor
+++ b/MatrixUtils.Web/Pages/Tools/Debug/SpaceDebug.razor
diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index f1e04a3..3aec2e3 100644
--- a/MatrixUtils.Web/Pages/Tools/Index.razor
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -1,10 +1,31 @@
 @page "/Tools"
-<h3>Other tools</h3>
-
-<a href="/Tools/CopyPowerlevel">Copy highest powerlevel across all session</a><br/>
-<a href="/Tools/KnownHomeserverList">Find all homeservers you share a room with</a><br/>
-<a href="/Tools/MassRoomJoin">Join room across all session</a><br/>
-<a href="/Tools/MediaLocator">Locate lost media</a><br/>
-<a href="/Tools/SpaceDebug">Debug space relationships</a><br/>
-<a href="/Tools/MigrateRoom">Migrate users from a split room to a new room</a><br/>
-<a href="/Tools/LeaveRoom">Leave room by ID</a><br/>
+<h3>Index of /Tools</h3>
+
+<h4 class="tool-category">Information tools</h4>
+<hr/>
+<a href="/Tools/Info/PolicyListActivity">View policy list activity</a><br/>
+<a href="/Tools/Info/KnownHomeserverList">Find all homeservers you share a room with</a><br/>
+<a href="/Tools/Info/SessionCount">Show session counts for users in a given room</a><br/>
+
+<h4 class="tool-category">User tools</h4>
+<hr/>
+<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/>
+
+<h4 class="tool-category">Moderation tools</h4>
+<hr/>
+<a href="/Tools/Moderation/InviteCounter">Count invites by inviter</a><br/>
+<a href="/Tools/Moderation/MembershipHistory">View membership history</a><br/>
+<a href="/Tools/Moderation/UserTrace">Trace user across rooms</a><br/>
+<a href="/tools/Moderation/MassCMEBan">Mass write policies to Community Moderation Effort</a><br/>
+<a href="/tools/Moderation/RoomIntersections">Find rooms with common users</a><br/>
+<a href="/tools/Moderation/DraupnirProtectedRoomsEditor">Edit Draupnir protected rooms set</a><br/>
+
+
+<h4 class="tool-category">Debugging tools</h4>
+<hr/>
+<a href="/Tools/Debug/SpaceDebug">Debug space relationships</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/Index.razor.css b/MatrixUtils.Web/Pages/Tools/Index.razor.css
new file mode 100644
index 0000000..c9bd995
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor.css
@@ -0,0 +1,6 @@
+.tool-category {
+    margin-top: 20px;
+}
+hr{
+    margin: unset;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/KnownHomeserverList.razor b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
index ddd7b15..ddd7b15 100644
--- a/MatrixUtils.Web/Pages/Tools/KnownHomeserverList.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/KnownHomeserverList.razor
diff --git a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index c94d0b0..c94d0b0 100644
--- a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
diff --git a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor.css b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor.css
index 443fdb5..443fdb5 100644
--- a/MatrixUtils.Web/Pages/Tools/PolicyListActivity.razor.css
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor.css
diff --git a/MatrixUtils.Web/Pages/Tools/SessionCount.razor b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
index 3b68bfa..3b68bfa 100644
--- a/MatrixUtils.Web/Pages/Tools/SessionCount.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/SessionCount.razor
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor
new file mode 100644
index 0000000..805bd40
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,102 @@
+@page "/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@using System.Text.Json.Serialization
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.RoomTypes
+<h3>Edit Draupnir protected rooms</h3>
+<hr/>
+<p><b>Note:</b> You will need to restart Draupnir after applying changes!</p>
+<p>Minor note: This <i>should</i> also work with Mjolnir, but this hasn't been tested, and as such functionality cannot be guaranteed.</p>
+
+@if (data is not null) {
+    <div class="row">
+        <div class="col-12">
+            <h4>Current rooms</h4>
+            <ul>
+                @foreach (var room in data.Rooms) {
+                    <li>@room</li>
+                }
+            </ul>
+            <hr/>
+            <h4>Tickyboxes</h4>
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th></th> @* Checkbox column *@
+                        <th>Kick?</th> @* PL > kick *@
+                        <th>Ban?</th> @* PL > ban *@
+                        <th>ACL?</th> @* PL > m.room.server_acls event *@
+                        <th>Room ID</th>
+                        <th>Room name</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    @foreach (var room in Rooms.OrderBy(x => x.RoomName)) {
+                        <tr>
+                            <td>
+                                <input type="checkbox" @bind="room.IsProtected"/>
+                            </td>
+                            <td>@(room.PowerLevels.Kick <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+                            <td>@(room.PowerLevels.Ban <= room.PowerLevels.GetUserPowerLevel(hs.UserId) ? "X" : "")</td>
+                            <td>@(room.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerACLEventContent.EventId) ? "X" : "")</td>
+                            <td>@room.Room.RoomId</td>
+                            <td>@room.RoomName</td>
+                        </tr>
+                    }
+                </tbody>
+            </table>
+        </div>
+    </div>
+}
+<br/>
+<LinkButton OnClick="@Apply">Apply</LinkButton>
+
+
+@code {
+    private DraupnirProtectedRoomsData data { get; set; } = new();
+    private List<EditorRoomInfo> Rooms { get; set; } = new();
+    private AuthenticatedHomeserverGeneric hs { get; set; }
+
+    protected override async Task OnInitializedAsync() {
+        hs = await RMUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+        data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms");
+        StateHasChanged();
+        var tasks = (await hs.GetJoinedRooms()).Select(async room => {
+            var plTask = room.GetPowerLevelsAsync();
+            var roomNameTask = room.GetNameOrFallbackAsync();
+            var EditorRoomInfo = new EditorRoomInfo {
+                Room = room,
+                IsProtected = data.Rooms.Contains(room.RoomId),
+                RoomName = await roomNameTask,
+                PowerLevels = await plTask
+            };
+
+            Rooms.Add(EditorRoomInfo);
+            StateHasChanged();
+            return Task.CompletedTask;
+        }).ToList();
+        await Task.WhenAll(tasks);
+        await Task.Delay(500);
+        StateHasChanged();
+    }
+
+    private class DraupnirProtectedRoomsData {
+        [JsonPropertyName("rooms")]
+        public List<string> Rooms { get; set; } = new();
+    }
+
+    private class EditorRoomInfo {
+        public GenericRoom Room { get; set; }
+        public bool IsProtected { get; set; }
+        public string RoomName { get; set; }
+        public RoomPowerLevelEventContent PowerLevels { get; set; }
+    }
+
+    private async Task Apply() {
+        Console.WriteLine(string.Join('\n', Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId)));
+        data.Rooms = Rooms.Where(x => x.IsProtected).Select(x => x.Room.RoomId).ToList();
+        await hs.SetAccountDataAsync("org.matrix.mjolnir.protected_rooms", data);
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 8f4b4dd..2123d4d 100644
--- a/MatrixUtils.Web/Pages/Tools/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -1,12 +1,7 @@
-@page "/Tools/InviteCounter"
-@using ArcaneLibs.Extensions
-@using LibMatrix.RoomTypes
+@page "/Tools/Moderation/InviteCounter"
 @using System.Collections.ObjectModel
-@using LibMatrix
-@using System.Collections.Frozen
 @using LibMatrix.EventTypes.Spec.State
-@using MatrixUtils.Abstractions
-<h3>User Trace</h3>
+<h3>Invite counter</h3>
 <hr/>
 
 <br/>
diff --git a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index cbbca9e..ea1e5f6 100644
--- a/MatrixUtils.Web/Pages/Tools/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,12 +1,6 @@
-@page "/Tools/MassCMEBan"
-@using ArcaneLibs.Extensions
-@using LibMatrix.RoomTypes
+@page "/Tools/Moderation/MassCMEBan"
 @using System.Collections.ObjectModel
-@using LibMatrix
-@using System.Collections.Frozen
-@using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.EventTypes.Spec.State.Policy
-@using MatrixUtils.Abstractions
 <h3>User Trace</h3>
 <hr/>
 
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
new file mode 100644
index 0000000..e5ba004
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -0,0 +1,276 @@
+@page "/Tools/Moderation/MembershipHistory"
+@using System.Collections.ObjectModel
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+<h3>Membership history viewer</h3>
+<hr/>
+
+<br/>
+<span>Room ID: </span>
+<InputText @bind-Value="@roomId"></InputText>
+<LinkButton OnClick="@Execute">Execute</LinkButton>
+<p><InputCheckbox @bind-Value="ChronologicalOrder"/> Chronological order</p>
+<p>
+    <span>Show </span>
+    <InputCheckbox @bind-Value="ShowJoins"/> joins
+    <InputCheckbox @bind-Value="ShowLeaves"/> leaves
+    <InputCheckbox @bind-Value="ShowUpdates"/> profile updates
+    <InputCheckbox @bind-Value="ShowKnocks"/> knocks
+    <InputCheckbox @bind-Value="ShowInvites"/> invites
+    <InputCheckbox @bind-Value="ShowKicks"/> kicks
+    <InputCheckbox @bind-Value="ShowBans"/> bans
+</p>
+<p>
+    <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = false; })">Hide all</LinkButton>
+    <LinkButton OnClick="@(async () => { ShowJoins = ShowLeaves = ShowUpdates = ShowKnocks = ShowInvites = ShowKicks = ShowBans = true; })">Show all</LinkButton>
+    <LinkButton OnClick="@(async () => { ShowJoins ^= true; ShowLeaves ^= true; ShowUpdates ^= true; ShowKnocks ^= true; ShowInvites ^= true; ShowKicks ^= true; ShowBans ^= true; })">Toggle all</LinkButton>
+</p>
+<p>
+    <span>Sender: </span>
+    <InputSelect @bind-Value="Sender">
+        <option value="">All</option>
+        @foreach (var sender in Memberships.Select(x => x.Sender).Distinct()) {
+            <option value="@sender">@sender</option>
+        }
+    </InputSelect>
+</p>
+<p>
+    <span>User: </span>
+    <InputSelect @bind-Value="User">
+        <option value="">All</option>
+        @foreach (var user in Memberships.Select(x => x.StateKey).Distinct()) {
+            <option value="@user">@user</option>
+        }
+    </InputSelect>
+</p>
+
+
+<br/>
+
+<details>
+    <summary>Results</summary>
+    @{
+        Dictionary<string, StateEventResponse> previousMemberships = [];
+        var filteredMemberships = Memberships.AsEnumerable();
+        if (ChronologicalOrder) {
+            filteredMemberships = filteredMemberships.Reverse();
+        }
+        if(!string.IsNullOrWhiteSpace(Sender)) {
+            filteredMemberships = filteredMemberships.Where(x => x.Sender == Sender);
+        }
+        if(!string.IsNullOrWhiteSpace(User)) {
+            filteredMemberships = filteredMemberships.Where(x => x.StateKey == User);
+        }
+
+        @foreach (var membership in filteredMemberships) {
+            RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent;
+            @switch (content.Membership) {
+                case RoomMemberEventContent.MembershipTypes.Invite: {
+                    if (_showInvites) {
+                        <p style="color: green;">@membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+                    }
+
+                    break;
+                }
+                case RoomMemberEventContent.MembershipTypes.Ban: {
+                    if (_showBans) {
+                        <p style="color: red;">@membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+                    }
+
+                    break;
+                }
+                case RoomMemberEventContent.MembershipTypes.Leave: {
+                    if (membership.Sender == membership.StateKey) {
+                        if (_showLeaves) {
+                            <p style="color: #C66;">@membership.Sender left the room</p>
+                        }
+                    }
+                    else {
+                        if (_showKicks) {
+                            <p style="color: darkorange;">@membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+                        }
+                    }
+
+                    break;
+                }
+                case RoomMemberEventContent.MembershipTypes.Knock: {
+                    if (_showKnocks) {
+                        <p>@membership.Sender knocked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+                    }
+
+                    break;
+                }
+                case RoomMemberEventContent.MembershipTypes.Join: {
+                    if (previousMemberships.TryGetValue(membership.StateKey, out var previous)
+                        && (previous.TypedContent as RoomMemberEventContent).Membership == RoomMemberEventContent.MembershipTypes.Join) {
+                        if (_showUpdates) {
+                            <p style="color: #777;">@membership.Sender changed their profile</p>
+                        }
+                    }
+                    else {
+                        if (_showJoins) {
+                            <p style="color: #6C6;">@membership.Sender joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")</p>
+                        }
+                    }
+
+                    break;
+                }
+                default: {
+                    <b>Unknown membership @content.Membership!</b>
+                    break;
+                }
+            }
+
+            previousMemberships[membership.StateKey] = membership;
+        }
+    }
+</details>
+
+<br/>
+<details open>
+    <summary>Log</summary>
+    @foreach (var line in log.Reverse()) {
+        <pre>@line</pre>
+    }
+</details>
+
+@code {
+
+#region Filter bindings
+
+    private bool _chronologicalOrder = false;
+
+    private bool ChronologicalOrder {
+        get => _chronologicalOrder;
+        set {
+            _chronologicalOrder = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showJoins = true;
+
+    private bool ShowJoins {
+        get => _showJoins;
+        set {
+            _showJoins = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showLeaves = true;
+
+    private bool ShowLeaves {
+        get => _showLeaves;
+        set {
+            _showLeaves = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showUpdates = true;
+
+    private bool ShowUpdates {
+        get => _showUpdates;
+        set {
+            _showUpdates = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showKnocks = true;
+
+    private bool ShowKnocks {
+        get => _showKnocks;
+        set {
+            _showKnocks = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showInvites = true;
+
+    private bool ShowInvites {
+        get => _showInvites;
+        set {
+            _showInvites = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showKicks = true;
+
+    private bool ShowKicks {
+        get => _showKicks;
+        set {
+            _showKicks = value;
+            StateHasChanged();
+        }
+    }
+
+    private bool _showBans = true;
+
+    private bool ShowBans {
+        get => _showBans;
+        set {
+            _showBans = value;
+            StateHasChanged();
+        }
+    }
+    
+    private string sender = "";
+    
+    private string Sender {
+        get => sender;
+        set {
+            sender = value;
+            StateHasChanged();
+        }
+    }
+    
+    private string user = "";
+    
+    private string User {
+        get => user;
+        set {
+            user = value;
+            StateHasChanged();
+        }
+    }
+
+#endregion
+
+    private ObservableCollection<string> log { get; set; } = new();
+    private List<StateEventResponse> Memberships { get; set; } = [];
+    private AuthenticatedHomeserverGeneric hs { get; set; }
+
+    [Parameter, SupplyParameterFromQuery(Name = "room")]
+    public string roomId { get; set; }
+
+    protected override async Task OnInitializedAsync() {
+        log.CollectionChanged += (sender, args) => StateHasChanged();
+        hs = await RMUStorage.GetCurrentSessionOrNavigate();
+        if (hs is null) return;
+
+        StateHasChanged();
+        Console.WriteLine("Rerendered!");
+        await base.OnInitializedAsync();
+        if (!string.IsNullOrWhiteSpace(roomId))
+            await Execute();
+    }
+
+    private async Task Execute() {
+        Memberships.Clear();
+        var room = hs.GetRoom(roomId);
+        var events = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000);
+        await foreach (var resp in events) {
+            var all = resp.State.Concat(resp.Chunk);
+            Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId));
+
+            log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline");
+        }
+
+        StateHasChanged();
+    }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index 173ff01..b8baeb8 100644
--- a/MatrixUtils.Web/Pages/Tools/RoomIntersections.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -1,4 +1,4 @@
-@page "/Tools/RoomIntersections"
+@page "/Tools/Moderation/RoomIntersections"
 @using LibMatrix.RoomTypes
 @using System.Collections.ObjectModel
 @using LibMatrix
diff --git a/MatrixUtils.Web/Pages/Tools/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index 95fe02b..915f8dc 100644
--- a/MatrixUtils.Web/Pages/Tools/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -1,12 +1,8 @@
-@page "/Tools/UserTrace"
+@page "/Tools/Moderation/UserTrace"
 @using ArcaneLibs.Extensions
 @using LibMatrix.RoomTypes
 @using System.Collections.ObjectModel
 @using LibMatrix
-@using System.Collections.Frozen
-@using LibMatrix.EventTypes.Spec.State
-@using LibMatrix.Filters
-@using MatrixUtils.Abstractions
 <h3>User Trace</h3>
 <hr/>
 
diff --git a/MatrixUtils.Web/Pages/Tools/CopyPowerlevel.razor b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
index 667b518..667b518 100644
--- a/MatrixUtils.Web/Pages/Tools/CopyPowerlevel.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/CopyPowerlevel.razor
diff --git a/MatrixUtils.Web/Pages/Tools/MassJoinRoom.razor b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
index a2ad388..a2ad388 100644
--- a/MatrixUtils.Web/Pages/Tools/MassJoinRoom.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/MassJoinRoom.razor
diff --git a/MatrixUtils.Web/Pages/Tools/ViewAccountData.razor b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
index d8b02bb..d8b02bb 100644
--- a/MatrixUtils.Web/Pages/Tools/ViewAccountData.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor