about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2024-10-03 20:53:24 +0200
committerRory& <root@rory.gay>2024-10-03 20:53:24 +0200
commit1041c0ade1700d1a56e4b2585c6a4e20d7123f81 (patch)
treee6af21cd725b34517501acd5088fb5805506cf16
parentChanges (diff)
downloadMatrixUtils-1041c0ade1700d1a56e4b2585c6a4e20d7123f81.tar.xz
Small fixes, fix policy list editor
m---------LibMatrix0
-rw-r--r--MatrixUtils.Web/Pages/Index.razor12
-rw-r--r--MatrixUtils.Web/Pages/LoginPage.razor44
-rw-r--r--MatrixUtils.Web/Pages/Rooms/PolicyList.razor4
-rw-r--r--MatrixUtils.Web/Pages/Tools/Index.razor2
-rw-r--r--MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor27
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor143
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor (renamed from MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor)41
-rw-r--r--MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor139
-rw-r--r--MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor2
10 files changed, 389 insertions, 25 deletions
diff --git a/LibMatrix b/LibMatrix
-Subproject ba7c369846086133b59af1cee68b2d836589f37
+Subproject 77650d16a9cc66fdfe393320164cd8248cdff38
diff --git a/MatrixUtils.Web/Pages/Index.razor b/MatrixUtils.Web/Pages/Index.razor
index b1a52d5..4be91b7 100644
--- a/MatrixUtils.Web/Pages/Index.razor
+++ b/MatrixUtils.Web/Pages/Index.razor
@@ -168,17 +168,11 @@ Small collection of tools to do not-so-everyday things.
         }
 
         List<string> offlineServers = [];
-        var sema = new SemaphoreSlim(64, 64);
+        var sema = new SemaphoreSlim(8, 8);
         var updateSw = Stopwatch.StartNew();
         var tasks = tokens.Select(async token => {
             await sema.WaitAsync();
-            if ((!string.IsNullOrWhiteSpace(token.Proxy) && offlineServers.Contains(token.Proxy)) || offlineServers.Contains(token.Homeserver)) {
-                _offlineSessions.Add(token);
-                sema.Release();
-                scannedSessions++;
-                return;
-            }
-
+            
             AuthenticatedHomeserverGeneric hs;
             try {
                 hs = await hsProvider.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy);
@@ -187,7 +181,7 @@ Small collection of tools to do not-so-everyday things.
                 var serverVersionTask = hs.FederationClient?.GetServerVersionAsync();
                 _sessions.Add(new() {
                     UserInfo = new() {
-                        AvatarUrl = (await profileTask).AvatarUrl ?? "meow",
+                        AvatarUrl = (await profileTask).AvatarUrl,
                         RoomCount = (await joinedRoomsTask).Count,
                         DisplayName = (await profileTask).DisplayName ?? hs.WhoAmI.UserId
                     },
diff --git a/MatrixUtils.Web/Pages/LoginPage.razor b/MatrixUtils.Web/Pages/LoginPage.razor
index 6c869ac..d43913c 100644
--- a/MatrixUtils.Web/Pages/LoginPage.razor
+++ b/MatrixUtils.Web/Pages/LoginPage.razor
@@ -27,6 +27,27 @@
 <br/>
 <br/>
 
+
+<h4>Add with access token</h4>
+<hr/>
+
+<span style="display: block;">
+    <label>Homeserver:</label>
+    <FancyTextBox @bind-Value="@newRecordInput.Homeserver"></FancyTextBox>
+</span>
+<span style="display: block;">
+    <label>Access token:</label>
+    <FancyTextBox @bind-Value="@newRecordInput.Password" IsPassword="true"></FancyTextBox>
+</span>
+<span style="display: block">
+    <label>Proxy (<a href="https://cgit.rory.gay/matrix/MxApiExtensions.git">MxApiExtensions</a> or similar):</label>
+    <FancyTextBox @bind-Value="@newRecordInput.Proxy"></FancyTextBox>
+</span>
+<br/>
+<LinkButton OnClick="@(() => AddWithAccessToken(newRecordInput))">Add session</LinkButton>
+<br/>
+<br/>
+
 <h4>Import from TSV</h4>
 <hr/>
 <span>Import credentials from a TSV (Tab Separated Values) file</span><br/>
@@ -156,4 +177,27 @@
         internal Exception? Exception { get; set; }
     }
 
+    private async Task AddWithAccessToken(LoginStruct record) {
+        try {
+            var session = await hsProvider.GetAuthenticatedWithToken(record.Homeserver, record.Password, record.Proxy);
+            if (session == null) {
+                Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
+                return;
+            }
+            
+            await RMUStorage.AddToken(new UserAuth() {
+                UserId = session.WhoAmI.UserId,
+                AccessToken = session.AccessToken,
+                Proxy = record.Proxy,
+                DeviceId = session.WhoAmI.DeviceId
+            });
+            LoggedInSessions = await RMUStorage.GetAllTokens();
+        }
+        catch (Exception e) {
+            Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!");
+            Console.WriteLine(e);
+            record.Exception = e;
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
index 4dc2adc..d57aa43 100644
--- a/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
+++ b/MatrixUtils.Web/Pages/Rooms/PolicyList.razor
@@ -214,13 +214,13 @@ else {
     private string GetPolicyTypeName(Type type) => GetPolicyTypeNameOrNull(type) ?? type.Name;
 
     private async Task RemovePolicyAsync(StateEventResponse policyEvent) {
-        await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), new { });
+        await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, new { });
         PolicyEventsByType[policyEvent.MappedType].Remove(policyEvent);
         await LoadStatesAsync();
     }
 
     private async Task UpdatePolicyAsync(StateEventResponse policyEvent) {
-        await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey.UrlEncode(), policyEvent.RawContent);
+        await Room.SendStateEventAsync(policyEvent.Type, policyEvent.StateKey, policyEvent.RawContent);
         CurrentlyEditingEvent = null;
         await LoadStatesAsync();
     }
diff --git a/MatrixUtils.Web/Pages/Tools/Index.razor b/MatrixUtils.Web/Pages/Tools/Index.razor
index e68bb9a..f99e932 100644
--- a/MatrixUtils.Web/Pages/Tools/Index.razor
+++ b/MatrixUtils.Web/Pages/Tools/Index.razor
@@ -24,7 +24,7 @@
 <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/>
+<a href="/tools/Moderation/Draupnir/ProtectedRoomsEditor">Draupnir: edit protected rooms set</a><br/>
 
 
 <h4 class="tool-category">Debugging tools</h4>
diff --git a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
index de0bfe7..f2af9b5 100644
--- a/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
+++ b/MatrixUtils.Web/Pages/Tools/Info/PolicyListActivity.razor
@@ -5,6 +5,8 @@
 @using LibMatrix.EventTypes.Common
 
 
+
+@* <ActivityGraph Data="TestData"/> *@
 @if (RoomData.Count == 0)
 {
     <p>Loading...</p>
@@ -15,10 +17,8 @@ else
         <h3>@room.Key</h3>
         @foreach (var year in room.Value.OrderBy(x => x.Key))
         {
-            <h5>@year.Key</h5>
-            <ActivityGraph Data="@year.Value" GlobalMax="MaxValue"
-                           RLabel="removed" GLabel="new" BLabel="updated policies">
-            </ActivityGraph>
+            <span>@year.Key</span>
+            <ActivityGraph Data="@year.Value" GlobalMax="MaxValue" RLabel="removed" GLabel="new" BLabel="updated policies"/>
         }
     }
 
@@ -42,15 +42,22 @@ else
         await base.OnInitializedAsync();
         Homeserver = (await RMUStorage.GetCurrentSessionOrNavigate())!;
         if (Homeserver is null) return;
-
+        //
         //random test data
-        for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(Random.Shared.Next(5)))
+        for (DateOnly i = new DateOnly(2020, 1, 1); i < new DateOnly(2020, 12, 30); i = i.AddDays(2))
         {
+            // TestData[i] = new()
+            // {
+            //     R = (int)(Random.Shared.NextSingle() * 255),
+            //     G = (int)(Random.Shared.NextSingle() * 255),
+            //     B = (int)(Random.Shared.NextSingle() * 255)
+            // };
+            // rgb based on number of week
             TestData[i] = new()
             {
-                R = (int)(Random.Shared.NextSingle() * 255),
-                G = (int)(Random.Shared.NextSingle() * 255),
-                B = (int)(Random.Shared.NextSingle() * 255)
+                R = i.DayOfYear % 255,
+                G = i.DayOfYear + 96 % 255,
+                B = i.DayOfYear + 192 % 255
             };
         }
 
@@ -109,7 +116,7 @@ else
             }
 
             //use timeline
-            var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 5000);
+            var timeline = room.GetManyMessagesAsync(limit: int.MaxValue, chunkSize: 2000);
             await foreach (var response in timeline)
             {
                 Console.WriteLine($"Got {response.State.Count} state, {response.Chunk.Count} timeline");
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
new file mode 100644
index 0000000..8745459
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,143 @@
+@page "/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectedRoomsEditor"
+@using System.Text.Json.Serialization
+@using LibMatrix
+@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">
+            <details>
+                <summary>Currently protected room IDs</summary>
+                <ul>
+                    @foreach (var room in data.Rooms) {
+                        <li>@room</li>
+                    }
+                </ul>
+            </details>
+            <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);
+
+        foreach (var protectedRoomId in data.Rooms) {
+            if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+            var room = hs.GetRoom(protectedRoomId);
+            var editorRoomInfo = new EditorRoomInfo {
+                Room = room,
+                IsProtected = true
+            };
+
+            try {
+                var pl = await room.GetPowerLevelsAsync();
+                editorRoomInfo.PowerLevels = pl;
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+            }
+
+            try {
+                editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+            }
+
+            try {
+                var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+                if (membership is not null) {
+                    editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+                }
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+            }
+
+            Rooms.Add(editorRoomInfo);
+        }
+
+        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/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
index 805bd40..b722596 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -1,6 +1,6 @@
-@page "/Moderation/DraupnirProtectedRoomsEditor"
-@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectionsEditor"
 @using System.Text.Json.Serialization
+@using LibMatrix
 @using LibMatrix.EventTypes.Spec.State
 @using LibMatrix.RoomTypes
 <h3>Edit Draupnir protected rooms</h3>
@@ -78,6 +78,43 @@
         }).ToList();
         await Task.WhenAll(tasks);
         await Task.Delay(500);
+
+        foreach (var protectedRoomId in data.Rooms) {
+            if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+            var room = hs.GetRoom(protectedRoomId);
+            var editorRoomInfo = new EditorRoomInfo {
+                Room = room,
+                IsProtected = true
+            };
+
+            try {
+                var pl = await room.GetPowerLevelsAsync();
+                editorRoomInfo.PowerLevels = pl;
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+            }
+
+            try {
+                editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+            }
+
+            try {
+                var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+                if (membership is not null) {
+                    editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+                }
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+            }
+
+            Rooms.Add(editorRoomInfo);
+        }
+
         StateHasChanged();
     }
 
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
new file mode 100644
index 0000000..b2f4026
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirWatchedListsEditor.razor
@@ -0,0 +1,139 @@
+@page "/Tools/Moderation/Draupnir/WatchedListsEditor"
+@using System.Text.Json.Serialization
+@using LibMatrix
+@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);
+
+        foreach (var protectedRoomId in data.Rooms) {
+            if (Rooms.Any(x => x.Room.RoomId == protectedRoomId)) continue;
+            var room = hs.GetRoom(protectedRoomId);
+            var editorRoomInfo = new EditorRoomInfo {
+                Room = room,
+                IsProtected = true
+            };
+
+            try {
+                var pl = await room.GetPowerLevelsAsync();
+                editorRoomInfo.PowerLevels = pl;
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get power levels for {room.RoomId}: {e}");
+            }
+
+            try {
+                editorRoomInfo.RoomName = await room.GetNameOrFallbackAsync();
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get name for {room.RoomId}: {e}");
+            }
+
+            try {
+                var membership = await room.GetStateEventOrNullAsync(hs.UserId);
+                if (membership is not null) {
+                    editorRoomInfo.RoomName = $"(!! {membership.ContentAs<RoomMemberEventContent>()?.Membership ?? "null"} !!) {editorRoomInfo.RoomName}";
+                }
+            }
+            catch (MatrixException e) {
+                Console.WriteLine($"Failed to get membership for {room.RoomId}: {e}");
+            }
+
+            Rooms.Add(editorRoomInfo);
+        }
+
+        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/User/ViewAccountData.razor b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
index d8b02bb..153518e 100644
--- a/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
+++ b/MatrixUtils.Web/Pages/Tools/User/ViewAccountData.razor
@@ -1,4 +1,4 @@
-@page "/Tools/ViewAccountData"
+@page "/Tools/User/ViewAccountData"
 @using ArcaneLibs.Extensions
 @using LibMatrix
 <h3>View account data</h3>