diff --git a/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor
new file mode 100644
index 0000000..fb4f9bf
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Moderation/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,97 @@
+@page "/Moderation/DraupnirProtectedRoomsEditor"
+@using System.Text.Json.Serialization
+@using MatrixUtils.Abstractions
+@using System.Collections.Frozen
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.RoomTypes
+<h3>Edit Draupnir protected rooms</h3>
+<hr/>
+
+@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();
+ foreach (var room in await hs.GetJoinedRooms()) {
+ 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();
+ }
+ }
+
+ 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/Moderation/UserRoomHistory.razor b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
new file mode 100644
index 0000000..e4eea83
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Moderation/UserRoomHistory.razor
@@ -0,0 +1,115 @@
+@page "/Moderation/UserRoomHistory/{UserId}"
+@using LibMatrix.Homeservers
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.RoomTypes
+@using ArcaneLibs.Extensions
+@using MatrixUtils.Abstractions
+<h3>UserRoomHistory</h3>
+
+<span>Enter mxid: </span>
+<FancyTextBox @bind-Value="@UserId"></FancyTextBox>
+
+@if (string.IsNullOrWhiteSpace(UserId)) {
+ <p>UserId is null!</p>
+}
+else {
+ <p>Checked @checkedRooms.Count so far...</p>
+ @if (currentHs is not null) {
+ <p>Checking rooms from @currentHs.UserId's perspective</p>
+ }
+ else if (checkedRooms.Count > 1) {
+ <p>Done!</p>
+ }
+ @foreach (var (state, rooms) in matchingStates) {
+ <u>@state</u>
+ <br/>
+ @foreach (var roomInfo in rooms) {
+ <RoomListItem RoomInfo="roomInfo" LoadData="true"></RoomListItem>
+ }
+ }
+}
+
+@code {
+ private string? _userId;
+
+ [Parameter]
+ public string? UserId {
+ get => _userId;
+ set {
+ _userId = value;
+ FindMember(value);
+ }
+ }
+
+ private List<AuthenticatedHomeserverGeneric> hss = new();
+ private AuthenticatedHomeserverGeneric? currentHs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ if (hs is null) return;
+ var sessions = await RMUStorage.GetAllTokens();
+ foreach (var userAuth in sessions) {
+ var session = await RMUStorage.GetSession(userAuth);
+ if (session is not null) {
+ hss.Add(session);
+ StateHasChanged();
+ }
+ }
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ if (!string.IsNullOrWhiteSpace(UserId)) FindMember(UserId);
+ }
+
+ public Dictionary<string, List<RoomInfo>> matchingStates = new();
+ public List<string> checkedRooms = new();
+ private SemaphoreSlim _semaphoreSlim = new(1, 1);
+
+ public async Task FindMember(string mxid) {
+ await _semaphoreSlim.WaitAsync();
+ if (mxid != UserId) {
+ _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();
+ await foreach (var (room, state) in tasks) {
+ if (state is null) continue;
+ if (!matchingStates.ContainsKey(state.Membership))
+ matchingStates.Add(state.Membership, new());
+ var roomInfo = new RoomInfo() {
+ Room = room
+ };
+ matchingStates[state.Membership].Add(roomInfo);
+ roomInfo.StateEvents.Add(new() {
+ Type = RoomNameEventContent.EventId,
+ TypedContent = new RoomNameEventContent() {
+ Name = await room.GetNameOrFallbackAsync(4)
+ },
+ RoomId = null, Sender = null, EventId = null //TODO implement
+ });
+ StateHasChanged();
+ if (mxid != UserId) {
+ _semaphoreSlim.Release();
+ return; //abort if changed
+ }
+ }
+ StateHasChanged();
+ }
+ currentHs = null;
+ StateHasChanged();
+ _semaphoreSlim.Release();
+ }
+
+ public async Task<(GenericRoom roomId, RoomMemberEventContent? content)> GetMembershipAsync(GenericRoom room, string mxid) {
+ return (room, await room.GetStateOrNullAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, mxid));
+ }
+
+}
\ No newline at end of file
|