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..b0d5a65
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectedRoomsEditor.razor
@@ -0,0 +1,138 @@
+@page "/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectedRoomsEditor"
+@using LibMatrix
+@using LibMatrix.EventTypes.Interop.Draupnir
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@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 sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+ data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>(DraupnirProtectedRoomsData.EventId);
+ 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 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..ea39c9a 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/DraupnirProtectedRoomsEditor.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/Draupnir/DraupnirProtectionsEditor.razor
@@ -1,7 +1,7 @@
-@page "/Moderation/DraupnirProtectedRoomsEditor"
-@page "/Tools/Moderation/DraupnirProtectedRoomsEditor"
+@page "/Tools/Moderation/Draupnir/ProtectionsEditor"
@using System.Text.Json.Serialization
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
@using LibMatrix.RoomTypes
<h3>Edit Draupnir protected rooms</h3>
<hr/>
@@ -38,7 +38,7 @@
</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.PowerLevels.UserHasStatePermission(hs.UserId, RoomServerAclEventContent.EventId) ? "X" : "")</td>
<td>@room.Room.RoomId</td>
<td>@room.RoomName</td>
</tr>
@@ -58,7 +58,7 @@
private AuthenticatedHomeserverGeneric hs { get; set; }
protected override async Task OnInitializedAsync() {
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
data = await hs.GetAccountDataAsync<DraupnirProtectedRoomsData>("org.matrix.mjolnir.protected_rooms");
StateHasChanged();
@@ -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..9e70687
--- /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.RoomInfo
+@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 sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ 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/FindUsersByRegex.razor b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
new file mode 100644
index 0000000..b62cf57
--- /dev/null
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/FindUsersByRegex.razor
@@ -0,0 +1,192 @@
+@page "/Tools/Moderation/FindUsersByRegex"
+@using System.Collections.Frozen
+@using ArcaneLibs.Extensions
+@using LibMatrix.RoomTypes
+@using System.Collections.ObjectModel
+@using System.Text.RegularExpressions
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
+@using LibMatrix.Helpers
+<h3>Find users by regex</h3>
+<hr/>
+
+<p>Users (regex): </p>
+<InputTextArea @bind-Value="@UserIdString"></InputTextArea>
+
+<LinkButton OnClick="Execute">Execute</LinkButton>
+<br/>
+<LinkButton OnClick="RemoveKicks">Remove kicks</LinkButton>
+<LinkButton OnClick="RemoveBans">Remove bans</LinkButton>
+<br/>
+
+
+<details>
+ <summary>Results</summary>
+ @foreach (var (userId, events) in matches) {
+ <h4>@userId</h4>
+ <ul>
+ @foreach (var match in events) {
+ <li>
+ <ul>
+ <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li>
+ <li>Membership: @(match.Event.RawContent.ToJson(indent: false)) (sent by @match.Event.Sender)</li>
+ </ul>
+ </li>
+ }
+ </ul>
+ }
+</details>
+
+<br/>
+@foreach (var line in log.Reverse()) {
+ <pre>@line</pre>
+}
+
+@code {
+
+ private ObservableCollection<string> log { get; set; } = new();
+
+ // List<RoomInfo> rooms { get; set; } = new();
+ List<GenericRoom> rooms { get; set; } = [];
+ Dictionary<string, List<Match>> matches = new();
+
+ private string UserIdString {
+ get => string.Join("\n", UserIDs);
+ set => UserIDs = value.Split("\n").Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
+ }
+
+ private List<string> UserIDs { get; set; } = new();
+
+ private AuthenticatedHomeserverGeneric hs { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ log.CollectionChanged += (sender, args) => StateHasChanged();
+ log.Add("Authenticating");
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (hs is null) return;
+
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ }
+
+ private async Task<string> Execute() {
+ log.Add("Constructing sync helper...");
+ var sh = new SyncHelper(hs) {
+ Filter = new SyncFilter() {
+ AccountData = new(types: []),
+ Presence = new(types: []),
+ Room = new() {
+ AccountData = new(types: []),
+ Ephemeral = new(types: []),
+ State = new(types: [RoomMemberEventContent.EventId]),
+ Timeline = new(types: []),
+ IncludeLeave = false
+ },
+ }
+ };
+
+ log.Add("Starting sync...");
+ var res = await sh.SyncAsync();
+
+ log.Add("Got sync response, parsing...");
+
+ var roomNames = (await Task.WhenAll((await hs.GetJoinedRooms()).Select(async room => { return (room.RoomId, await room.GetNameOrFallbackAsync()); }).ToList())).ToFrozenDictionary(x => x.Item1, x => x.Item2);
+
+ foreach (var userIdRegex in UserIDs) {
+ var regex = new Regex(userIdRegex, RegexOptions.Compiled);
+ log.Add($"Searching for {regex}:");
+ foreach (var (roomId, joinedRoom) in res.Rooms.Join) {
+ log.Add($"- Checking room {roomId}...");
+ foreach (var evt in joinedRoom.State.Events) {
+ if (evt.StateKey is null) continue;
+ if (evt.Type is not RoomMemberEventContent.EventId) continue;
+
+ if (regex.IsMatch(evt.StateKey)) {
+ log.Add($" - Found match in {roomId} for {evt.StateKey}");
+ if (!matches.ContainsKey(evt.StateKey)) {
+ matches[evt.StateKey] = new();
+ }
+
+ var room = hs.GetRoom(roomId);
+ matches[evt.StateKey].Add(new Match {
+ Room = room,
+ Event = evt,
+ RoomName = roomNames[roomId]
+ });
+ }
+ }
+ }
+ }
+
+ log.Add("Done!");
+
+ StateHasChanged();
+
+ return "";
+ }
+
+ public string? ImportFromRoomId { get; set; }
+
+ private async Task DoImportFromRoomId() {
+ try {
+ if (ImportFromRoomId is null) return;
+ var room = rooms.FirstOrDefault(x => x.RoomId == ImportFromRoomId);
+ UserIdString = string.Join("\n", (await room.GetMembersListAsync()).Select(x => x.StateKey));
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ log.Add("Could not fetch members list!\n" + e.ToString());
+ }
+
+ StateHasChanged();
+ }
+
+ private class Match {
+ public GenericRoom Room;
+ public StateEventResponse Event;
+ public string RoomName { get; set; }
+ }
+
+ private async IAsyncEnumerable<Match> GetMatches(string userId) {
+ var results = rooms.Select(async room => {
+ var state = await room.GetStateEventOrNullAsync(room.RoomId, userId);
+ if (state is not null) {
+ return new Match {
+ Room = room,
+ Event = state,
+ RoomName = await room.GetNameOrFallbackAsync()
+ };
+ }
+
+ return null;
+ }).ToAsyncEnumerable();
+ await foreach (var result in results) {
+ if (result is not null) {
+ yield return result;
+ }
+ }
+ }
+
+ private Task RemoveKicks() {
+ foreach (var (userId, matches) in matches) {
+ matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "leave" && x.Event.Sender != x.Event.StateKey);
+ }
+
+ matches.RemoveAll((x, y) => y.Count == 0);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
+ private Task RemoveBans() {
+ foreach (var (userId, matches) in matches) {
+ matches.RemoveAll(x => x.Event.ContentAs<RoomMemberEventContent>()!.Membership == "ban" && x.Event.Sender != x.Event.StateKey);
+ }
+
+ matches.RemoveAll((x, y) => y.Count == 0);
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
index 2123d4d..5c5946f 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/InviteCounter.razor
@@ -1,6 +1,8 @@
@page "/Tools/Moderation/InviteCounter"
@using System.Collections.ObjectModel
-@using LibMatrix.EventTypes.Spec.State
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
<h3>Invite counter</h3>
<hr/>
@@ -13,7 +15,7 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in invites.OrderByDescending(x=>x.Value).ToList()) {
+ @foreach (var (userId, events) in invites.OrderByDescending(x => x.Value).ToList()) {
<p>@userId: @events</p>
}
</details>
@@ -27,16 +29,15 @@
private ObservableCollection<string> log { get; set; } = new();
private Dictionary<string, int> invites { get; set; } = new();
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();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -44,22 +45,21 @@
private async Task<string> Execute() {
var room = hs.GetRoom(roomId);
- var events = room.GetManyMessagesAsync(limit: int.MaxValue);
+ var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] };
+ var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false));
await foreach (var resp in events) {
var all = resp.State.Concat(resp.Chunk);
foreach (var evt in all) {
- if(evt.Type != RoomMemberEventContent.EventId) continue;
+ if (evt.Type != RoomMemberEventContent.EventId) continue;
var content = evt.TypedContent as RoomMemberEventContent;
- if(content.Membership != "invite") continue;
- if(!invites.ContainsKey(evt.Sender)) invites[evt.Sender] = 0;
- invites[evt.Sender]++;
+ if (content?.Membership != "invite") continue;
+ invites.TryAdd(evt.Sender!, 0);
+ invites[evt.Sender!]++;
}
log.Add($"{resp.State.Count} state, {resp.Chunk.Count} timeline");
}
-
-
-
+
StateHasChanged();
return "";
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
index ea1e5f6..8fdad84 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MassCMEBan.razor
@@ -1,6 +1,7 @@
@page "/Tools/Moderation/MassCMEBan"
@using System.Collections.ObjectModel
@using LibMatrix.EventTypes.Spec.State.Policy
+@using LibMatrix.RoomTypes
<h3>User Trace</h3>
<hr/>
@@ -17,19 +18,19 @@
}
@code {
+
// TODO: Properly implement page to be more useful
private ObservableCollection<string> log { get; set; } = new();
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();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
-
+
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
@@ -37,33 +38,41 @@
private async Task<string> Execute() {
var room = hs.GetRoom("!fTjMjIzNKEsFlUIiru:neko.dev");
- // var room = hs.GetRoom("!yf7OpOiRDXx6zUGpT6:conduit.rory.gay");
- var users = roomId.Split("\n").Select(x => x.Trim()).Where(x=>x.StartsWith('@')).ToList();
- foreach (var user in users) {
- var exists = false;
- try {
- exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
- } catch (Exception e) {
- log.Add($"Failed to get {user}");
- }
+ // var room = hs.GetRoom("!IVSjKMsVbjXsmUTuRR:rory.gay");
+ var users = roomId.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
+ var tasks = users.Select(x => ExecuteBan(room, x)).ToList();
+ await Task.WhenAll(tasks);
+
+ StateHasChanged();
+
+ return "";
+ }
- if (!exists) {
+ private async Task ExecuteBan(GenericRoom room, string user) {
+ var exists = false;
+ try {
+ exists = !string.IsNullOrWhiteSpace((await room.GetStateAsync<UserPolicyRuleEventContent>(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'))).Entity);
+ }
+ catch (Exception e) {
+ log.Add($"Failed to get {user}");
+ }
+
+ if (!exists) {
+ try {
var evt = await room.SendStateEventAsync(UserPolicyRuleEventContent.EventId, user.Replace('@', '_'), new UserPolicyRuleEventContent() {
Entity = user,
- Reason = "spam (invite)",
+ Reason = "spam",
Recommendation = "m.ban"
});
log.Add($"Sent {evt.EventId} to ban {user}");
}
- else {
- log.Add($"User {user} already exists");
+ catch (Exception e) {
+ log.Add($"Failed to ban {user}: {e}");
}
}
-
-
- StateHasChanged();
-
- return "";
+ else {
+ log.Add($"User {user} already exists");
+ }
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
index e5ba004..1ec3cd0 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/MembershipHistory.razor
@@ -1,31 +1,162 @@
@page "/Tools/Moderation/MembershipHistory"
+@using System.Collections.Frozen
@using System.Collections.ObjectModel
+@using System.Diagnostics
+@using System.Text.Json
+@using ArcaneLibs.Extensions
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
+@using LibMatrix.Filters
+@{
+ var sw = Stopwatch.StartNew();
+ Console.WriteLine("Start render");
+}
<h3>Membership history viewer</h3>
<hr/>
-
<br/>
<span>Room ID: </span>
-<InputText @bind-Value="@roomId"></InputText>
+<InputText @bind-Value="@RoomId"></InputText>
<LinkButton OnClick="@Execute">Execute</LinkButton>
-<p><InputCheckbox @bind-Value="ChronologicalOrder"/> Chronological order</p>
+<p>
+ <span><InputCheckbox @bind-Value="ChronologicalOrder"/>Chronological order</span>
+ <span><InputCheckbox @bind-Value="DoDisambiguate"/>Enable extended filters</span>
+</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
+ <span><InputCheckbox @bind-Value="ShowJoins"/> joins</span>
+ <span><InputCheckbox @bind-Value="ShowLeaves"/> leaves</span>
+ <span><InputCheckbox @bind-Value="ShowKnocks"/> knocks</span>
+ <span><InputCheckbox @bind-Value="ShowInvites"/> invites</span>
+ <span><InputCheckbox @bind-Value="ShowBans"/> bans</span>
</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>
+ <LinkButton OnClick="@(async () => {
+ ShowJoins = ShowLeaves = ShowKnocks = ShowInvites = ShowBans = false;
+ StateHasChanged();
+ })">Hide all
+ </LinkButton>
+ <LinkButton OnClick="@(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>
</p>
<p>
+ <span><InputCheckbox @bind-Value="DoDisambiguate"/> Disambiguate </span>
+ @if (DoDisambiguate) {
+ <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>
+ }
+</p>
+@if (DoDisambiguate) {
+ <p>
+ <span>Show </span>
+ @if (DisambiguateKicks) {
+ <span><InputCheckbox @bind-Value="ShowKicks"/> kicks</span>
+ }
+ @if (DisambiguateUnbans) {
+ <span><InputCheckbox @bind-Value="ShowUnbans"/> unbans</span>
+ }
+ @if (DisambiguateProfileUpdates) {
+ <span><InputCheckbox @bind-Value="ShowProfileUpdates"/> profile updates</span>
+ }
+ @if (DisambiguateInviteActions) {
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="ShowInviteActions"/>
+ invite actions
+ </summary>
+ @if (DisambiguateInviteAccepted) {
+ <span><InputCheckbox @bind-Value="ShowInviteAccepted"/> accepted</span>
+ }
+
+ @if (DisambiguateInviteRejected) {
+ <span><InputCheckbox @bind-Value="ShowInviteRejected"/> rejected</span>
+ }
+
+ @if (DisambiguateInviteRetracted) {
+ <span><InputCheckbox @bind-Value="ShowInviteRetracted"/> retracted</span>
+ }
+ </details>
+ }
+ @if (DisambiguateKnockActions) {
+ <details style="display: inline-block; vertical-align: top;">
+ <summary>
+ <InputCheckbox @bind-Value="ShowKnockActions"/>
+ knock actions
+ </summary>
+ @if (DisambiguateKnockAccepted) {
+ <span><InputCheckbox @bind-Value="ShowKnockAccepted"/> accepted</span>
+ }
+
+ @if (DisambiguateKnockRejected) {
+ <span><InputCheckbox @bind-Value="ShowKnockRejected"/> rejected</span>
+ }
+
+ @if (DisambiguateKnockRetracted) {
+ <span><InputCheckbox @bind-Value="ShowKnockRetracted"/> retracted</span>
+ }
+ </details>
+ }
+ </p>
+
+ <p>
+ <LinkButton OnClick="@(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>
+ <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>
+ </p>
+}
+<p>
<span>Sender: </span>
<InputSelect @bind-Value="Sender">
<option value="">All</option>
@@ -44,92 +175,121 @@
</InputSelect>
</p>
-
+@{ Console.WriteLine($"Rendering took {sw.Elapsed} for {Memberships.Count} items"); }
<br/>
-<details>
+<details open>
<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>
- }
+ var filteredMemberships = GetFilteredMemberships();
+ }
+ <table>
+ @foreach (var membershipEntry in filteredMemberships) {
+ var (transition, membership, previousMembership) = membershipEntry;
+ RoomMemberEventContent content = membership.TypedContent as RoomMemberEventContent ?? throw new InvalidOperationException("Event is not a RoomMemberEventContent!");
+ RoomMemberEventContent? previousContent = previousMembership?.TypedContent as RoomMemberEventContent;
+
+ <tr>
+ <td>@DateTimeOffset.FromUnixTimeMilliseconds(membership.OriginServerTs ?? 0).ToString("g")</td>
+ <td>
+ @switch (transition) {
+ case MembershipTransition.None:
+ <b>Unknown membership! Got None</b>
+ break;
+ case MembershipTransition.Join:
+ <p style="color: #6C6;">
+ @membership.StateKey joined the room @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")<br/>
+ Display name: @content.DisplayName<br/>
+ Avatar URL: @content.AvatarUrl
+ </p>
+ break;
+ case MembershipTransition.Leave:
+ <p style="color: #C66;">
+ @membership.StateKey left the room
+ </p>
+ break;
+ case MembershipTransition.Knock:
+ <p style="color: #426">
+ @membership.StateKey knocked @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Invite:
+ <p style="color: #262;">
+ @membership.Sender invited @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Ban:
+ <p style="color: red;">
+ @membership.Sender banned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ @* disambiguated *@
+ case MembershipTransition.Kick:
+ <p style="color: darkorange;">
+ @membership.Sender kicked @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.ProfileUpdate:
+ <p style="color: #777;">
+ @membership.Sender changed their profile<br/>
+ Display name: @previousContent!.DisplayName -> @content.DisplayName<br/>
+ Avatar URL: @previousContent.AvatarUrl -> @content.AvatarUrl
+ </p>
+ break;
+ case MembershipTransition.InviteAccepted:
+ <p style="color: #084;">
+ @membership.StateKey accepted the invite
+ from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockAccepted:
+ <p style="color: #288;">
+ @membership.StateKey's knock was accepted
+ by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(accept reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockRejected:
+ <p style="color: #828;">
+ @membership.StateKey's knock was rejected
+ by @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(knock reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.Unban:
+ <p style="color: #0C0;">
+ @membership.Sender unbanned @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.InviteRejected:
+ <p style="color: #733;">
+ @membership.StateKey rejected the invite
+ from @previousMembership!.Sender @(string.IsNullOrWhiteSpace(previousContent?.Reason) ? "" : $"(invite reason: {previousContent.Reason})") @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reject reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.InviteRetracted:
+ <p style="color: #844;">
+ @membership.Sender retracted the invite
+ for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ case MembershipTransition.KnockRetracted:
+ <p style="color: #b55;">
+ @membership.Sender retracted the knock
+ for @membership.StateKey @(string.IsNullOrWhiteSpace(content.Reason) ? "" : $"(reason: {content.Reason})")
+ </p>
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
-
- break;
- }
- default: {
- <b>Unknown membership @content.Membership!</b>
- break;
- }
- }
-
- previousMemberships[membership.StateKey] = membership;
+ </td>
+ </tr>
}
- }
+ </table>
</details>
<br/>
<details open>
<summary>Log</summary>
- @foreach (var line in log.Reverse()) {
+ @foreach (var line in Log.Reverse()) {
<pre>@line</pre>
}
</details>
@@ -138,139 +298,289 @@
#region Filter bindings
- private bool _chronologicalOrder = false;
-
- private bool ChronologicalOrder {
- get => _chronologicalOrder;
- set {
- _chronologicalOrder = value;
- StateHasChanged();
- }
- }
+ private bool ChronologicalOrder { get; set; }
+ private bool ShowJoins { get; set; } = true;
+ private bool ShowLeaves { get; set; } = true;
+ private bool ShowKnocks { get; set; } = true;
+ private bool ShowInvites { get; set; } = true;
+ 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 _showJoins = true;
+ private bool ShowProfileUpdates {
+ get => field && DisambiguateProfileUpdates;
+ set;
+ } = true;
- private bool ShowJoins {
- get => _showJoins;
+ private bool ShowKicks {
+ get => field && DisambiguateKicks;
+ set;
+ } = true;
+
+ private bool ShowUnbans {
+ get => field && DisambiguateUnbans;
+ set;
+ } = true;
+
+ private bool ShowInviteAccepted {
+ get => field && DisambiguateInviteAccepted;
+ set;
+ } = true;
+
+ private bool ShowInviteRejected {
+ get => field && DisambiguateInviteRejected;
+ set;
+ } = true;
+
+ private bool ShowInviteRetracted {
+ get => field && DisambiguateInviteRetracted;
+ set;
+ } = true;
+
+ private bool ShowKnockAccepted {
+ get => field && DisambiguateKnockAccepted;
+ set;
+ } = true;
+
+ private bool ShowKnockRejected {
+ get => field && DisambiguateKnockRejected;
+ set;
+ } = true;
+
+ private bool ShowKnockRetracted {
+ get => field && DisambiguateKnockRetracted;
+ set;
+ } = true;
+
+ private bool ShowKnockActions {
+ get => field && DisambiguateKnockActions;
+ set;
+ } = true;
+
+ private bool ShowInviteActions {
+ get => field && DisambiguateInviteActions;
+ set;
+ } = true;
+
+ [Parameter, SupplyParameterFromQuery(Name = "sender")]
+ public string Sender { get; set; } = "";
+
+ [Parameter, SupplyParameterFromQuery(Name = "user")]
+ public string User { get; set; } = "";
+
+ [Parameter, SupplyParameterFromQuery(Name = "filter")]
+ public string Filter {
+ get;
set {
- _showJoins = value;
+ field = value;
+ if (string.IsNullOrWhiteSpace(value)) return;
+ var parts = value.Split(',');
+ ShowJoins = parts.Contains("join");
+ ShowLeaves = parts.Contains("leave");
+ ShowKnocks = parts.Contains("knock");
+ ShowInvites = parts.Contains("invite");
+ ShowBans = parts.Contains("ban");
StateHasChanged();
}
- }
-
- private bool _showLeaves = true;
+ } = "";
- private bool ShowLeaves {
- get => _showLeaves;
- set {
- _showLeaves = value;
- StateHasChanged();
- }
- }
+#endregion
- private bool _showUpdates = true;
+ private ObservableCollection<string> Log { get; set; } = new();
+ private List<StateEventResponse> Memberships { get; set; } = [];
+ private AuthenticatedHomeserverGeneric Homeserver { get; set; }
- private bool ShowUpdates {
- get => _showUpdates;
- set {
- _showUpdates = value;
- StateHasChanged();
- }
- }
+ [Parameter, SupplyParameterFromQuery(Name = "room")]
+ public string RoomId { get; set; } = "";
- private bool _showKnocks = true;
+ protected override async Task OnInitializedAsync() {
+ Log.CollectionChanged += (sender, args) => StateHasChanged();
+ Homeserver = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
+ if (Homeserver is null) return;
- private bool ShowKnocks {
- get => _showKnocks;
- set {
- _showKnocks = value;
- StateHasChanged();
- }
+ StateHasChanged();
+ Console.WriteLine("Rerendered!");
+ await base.OnInitializedAsync();
+ if (!string.IsNullOrWhiteSpace(RoomId))
+ await Execute();
}
- private bool _showInvites = true;
+ private async Task Execute() {
+ Memberships.Clear();
+ var room = Homeserver.GetRoom(RoomId);
+ var filter = new SyncFilter.EventFilter() { Types = [RoomMemberEventContent.EventId] };
+ var events = room.GetManyMessagesAsync(limit: int.MaxValue, filter: filter.ToJson(ignoreNull: true, indent: false));
+ await foreach (var resp in events) {
+ var all = resp.State.Concat(resp.Chunk)
+ // ugly hack, because some users fuck around too much
+ .Select(x => {
+ if (x.RawContent?["displayname"]?.GetValueKind() != JsonValueKind.String)
+ x.RawContent?.Remove("displayname");
+ if (x.RawContent?["avatar_url"]?.GetValueKind() is not JsonValueKind.String)
+ x.RawContent?.Remove("avatar_url");
+ return x;
+ });
+ Memberships.AddRange(all.Where(x => x.Type == RoomMemberEventContent.EventId));
- private bool ShowInvites {
- get => _showInvites;
- set {
- _showInvites = value;
- StateHasChanged();
+ Log.Add($"Got {resp.State.Count} state and {resp.Chunk.Count} timeline events.");
}
- }
- private bool _showKicks = true;
+ Log.Add("Reached end of timeline!");
- private bool ShowKicks {
- get => _showKicks;
- set {
- _showKicks = value;
- StateHasChanged();
- }
+ StateHasChanged();
}
- private bool _showBans = true;
+ private readonly struct MembershipEntry {
+ public required MembershipTransition State { get; init; }
+ public required StateEventResponse Event { get; init; }
+ public required StateEventResponse? Previous { get; init; }
- private bool ShowBans {
- get => _showBans;
- set {
- _showBans = value;
- StateHasChanged();
+ public void Deconstruct(out MembershipTransition transition, out StateEventResponse evt, out StateEventResponse? prev) {
+ transition = State;
+ evt = Event;
+ prev = Previous;
}
}
-
- private string sender = "";
-
- private string Sender {
- get => sender;
- set {
- sender = value;
- StateHasChanged();
- }
+
+ private enum MembershipTransition : byte {
+ None,
+ Join,
+ Leave,
+ Knock,
+ Invite,
+ Ban,
+
+ // disambiguated
+ ProfileUpdate,
+ Kick,
+ Unban,
+ InviteAccepted,
+ InviteRejected,
+ InviteRetracted,
+ KnockAccepted,
+ KnockRejected,
+ KnockRetracted
}
-
- private string user = "";
-
- private string User {
- get => user;
- set {
- user = value;
- StateHasChanged();
+
+ private static IEnumerable<MembershipEntry> GetTransitions(List<StateEventResponse> 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!");
+ var prev = transitions.GetValueOrDefault(evt.StateKey!) as MembershipEntry?;
+ transitions[evt.StateKey ?? throw new Exception("Member event has no state key??")] = new MembershipEntry {
+ Event = evt,
+ Previous = prev?.Event,
+ State = content.Membership switch {
+ RoomMemberEventContent.MembershipTypes.Join =>
+ prev?.State switch {
+ MembershipTransition.Join or MembershipTransition.InviteAccepted => MembershipTransition.ProfileUpdate,
+ MembershipTransition.Invite => MembershipTransition.InviteAccepted,
+ _ => MembershipTransition.Join
+ },
+ RoomMemberEventContent.MembershipTypes.Leave =>
+ evt.Sender == evt.StateKey
+ ? prev?.State switch {
+ MembershipTransition.Knock => MembershipTransition.KnockRetracted,
+ MembershipTransition.Invite => MembershipTransition.InviteRejected,
+ _ => MembershipTransition.Leave
+ }
+ : prev?.State switch {
+ // not self
+ MembershipTransition.Knock => MembershipTransition.KnockRejected,
+ MembershipTransition.Invite => MembershipTransition.InviteRetracted,
+ _ => MembershipTransition.Kick,
+ },
+ RoomMemberEventContent.MembershipTypes.Invite =>
+ prev?.State switch {
+ MembershipTransition.Knock => MembershipTransition.KnockAccepted,
+ _ => MembershipTransition.Invite
+ },
+ RoomMemberEventContent.MembershipTypes.Knock => MembershipTransition.Knock,
+ RoomMemberEventContent.MembershipTypes.Ban => MembershipTransition.Ban,
+ _ => MembershipTransition.None
+ }
+ };
+ yield return transitions[evt.StateKey];
}
}
-#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;
+ private IEnumerable<MembershipEntry> Disambiguated(IEnumerable<MembershipEntry> entries) {
+ FrozenDictionary<MembershipTransition, MembershipTransition> disambiguated = new Dictionary<MembershipTransition, MembershipTransition>() {
+ { MembershipTransition.ProfileUpdate, MembershipTransition.Join },
+ { MembershipTransition.Kick, MembershipTransition.Leave },
+ { MembershipTransition.Unban, MembershipTransition.Leave },
+ { MembershipTransition.InviteAccepted, MembershipTransition.Join },
+ { MembershipTransition.InviteRejected, MembershipTransition.Leave },
+ { MembershipTransition.InviteRetracted, MembershipTransition.Leave },
+ { MembershipTransition.KnockAccepted, MembershipTransition.Invite },
+ { MembershipTransition.KnockRejected, MembershipTransition.Leave },
+ { MembershipTransition.KnockRetracted, MembershipTransition.Leave }
+ }.ToFrozenDictionary();
+
+ foreach (var entry in entries) {
+ if (!DoDisambiguate) {
+ yield return entry;
+ continue;
+ }
- StateHasChanged();
- Console.WriteLine("Rerendered!");
- await base.OnInitializedAsync();
- if (!string.IsNullOrWhiteSpace(roomId))
- await Execute();
+ var newState = entry.State switch {
+ MembershipTransition.ProfileUpdate when !DoDisambiguate || !DisambiguateProfileUpdates => MembershipTransition.Join,
+ MembershipTransition.Kick when !DoDisambiguate || !DisambiguateKicks => MembershipTransition.Leave,
+ MembershipTransition.Unban when !DoDisambiguate || !DisambiguateUnbans => MembershipTransition.Leave,
+ MembershipTransition.InviteAccepted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteAccepted => MembershipTransition.Join,
+ MembershipTransition.InviteRejected when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRejected => MembershipTransition.Leave,
+ MembershipTransition.InviteRetracted when !DoDisambiguate || !DisambiguateInviteActions || !DisambiguateInviteRetracted => MembershipTransition.Leave,
+ MembershipTransition.KnockAccepted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockAccepted => MembershipTransition.Invite,
+ MembershipTransition.KnockRejected when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRejected => MembershipTransition.Leave,
+ MembershipTransition.KnockRetracted when !DoDisambiguate || !DisambiguateKnockActions || !DisambiguateKnockRetracted => MembershipTransition.Leave,
+ _ => entry.State
+ };
+ if (newState != entry.State) {
+ yield return entry with { State = newState };
+ }
+ else yield return entry;
+ }
}
- 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");
+ private IEnumerable<MembershipEntry> GetFilteredMemberships() {
+ var filteredMemberships = GetTransitions(Memberships);
+ if (!string.IsNullOrWhiteSpace(Sender)) filteredMemberships = filteredMemberships.Where(x => x.Event.Sender == Sender);
+ if (!string.IsNullOrWhiteSpace(User)) filteredMemberships = filteredMemberships.Where(x => x.Event.StateKey == User);
+ filteredMemberships = Disambiguated(filteredMemberships);
+
+ if (!ShowJoins) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Join);
+ if (!ShowLeaves) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Leave);
+ if (!ShowKnocks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Knock);
+ if (!ShowInvites) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Invite);
+ if (!ShowBans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Ban);
+ // extended filters
+ if (DoDisambiguate) {
+ if (!DisambiguateProfileUpdates || !ShowProfileUpdates) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.ProfileUpdate);
+ if (!DisambiguateKicks || !ShowKicks) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Kick);
+ if (!DisambiguateUnbans || !ShowUnbans) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.Unban);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteAccepted || !ShowInviteAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteAccepted);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRejected || !ShowInviteRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRejected);
+ if (!DisambiguateInviteActions || !ShowInviteActions || !DisambiguateInviteRetracted || !ShowInviteRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.InviteRetracted);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockAccepted || !ShowKnockAccepted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockAccepted);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRejected || !ShowKnockRejected) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRejected);
+ if (!DisambiguateKnockActions || !ShowKnockActions || !DisambiguateKnockRetracted || !ShowKnockRetracted) filteredMemberships = filteredMemberships.Where(x => x.State != MembershipTransition.KnockRetracted);
}
- StateHasChanged();
+ if (!ChronologicalOrder) filteredMemberships = filteredMemberships.Reverse();
+
+ return filteredMemberships;
}
}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
index b8baeb8..736e59a 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/RoomIntersections.razor
@@ -2,7 +2,7 @@
@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
@using LibMatrix
-@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>Room intersections</h3>
<hr/>
@@ -113,7 +113,7 @@
protected override async Task OnInitializedAsync() {
Log.CollectionChanged += (sender, args) => StateHasChanged();
- hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
StateHasChanged();
diff --git a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
index 915f8dc..c3cc09c 100644
--- a/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
+++ b/MatrixUtils.Web/Pages/Tools/Moderation/UserTrace.razor
@@ -3,13 +3,15 @@
@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State.RoomInfo
<h3>User Trace</h3>
<hr/>
<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 OnClick="@DoImportFromRoomId">Import from room (ID)</LinkButton>
<details>
<summary>Rooms to be searched (@rooms.Count)</summary>
@@ -24,18 +26,21 @@
<details>
<summary>Results</summary>
- @foreach (var (userId, events) in matches) {
+ @foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) {
<h4>@userId</h4>
- <ul>
- @foreach (var match in events) {
- <li>
- <ul>
- <li>@match.RoomName (<span>@match.Room.RoomId</span>)</li>
- <li>Membership: @(match.Event.RawContent.ToJson(indent: false))</li>
- </ul>
- </li>
+ <table>
+ @foreach (var match in events.OrderBy(x=>x.RoomName)) {
+ <tr>
+ <td>@match.RoomName (<span>@match.Room.RoomId</span>)</td>
+ <td>
+ <details>
+ <summary>@SummarizeMembership(match.Event)</summary>
+ <pre>@match.Event.RawContent.ToJson(indent: true)</pre>
+ </details>
+ </td>
+ </tr>
}
- </ul>
+ </table>
}
</details>
@@ -61,56 +66,34 @@
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
- var hs = await RMUStorage.GetCurrentSessionOrNavigate();
+ var hs = await sessionStore.GetCurrentHomeserver(navigateOnFailure: true);
if (hs is null) return;
- // var sessions = await RMUStorage.GetAllTokens();
- // var baseRooms = new List<GenericRoom>();
- // foreach (var userAuth in sessions) {
- // var session = await RMUStorage.GetSession(userAuth);
- // if (session is not null) {
- // baseRooms.AddRange(await session.GetJoinedRooms());
- // var sessionRooms = (await session.GetJoinedRooms()).Where(x => !rooms.Any(y => y.Room.RoomId == x.RoomId)).ToList();
- // StateHasChanged();
- // log.Add($"Got {sessionRooms.Count} rooms for {userAuth.UserId}");
- // }
- // }
- //
- // log.Add("Done fetching rooms!");
- //
- // baseRooms = baseRooms.DistinctBy(x => x.RoomId).ToList();
- //
- // // rooms.CollectionChanged += (sender, args) => StateHasChanged();
- // var tasks = baseRooms.Select(async newRoom => {
- // bool success = false;
- // while (!success)
- // try {
- // var state = await newRoom.GetFullStateAsListAsync();
- // var newRoomInfo = new RoomInfo(newRoom, state);
- // rooms.Add(newRoomInfo);
- // log.Add($"Got {newRoomInfo.StateEvents.Count} events for {newRoomInfo.RoomName}");
- // success = true;
- // }
- // catch (MatrixException e) {
- // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
- // throw;
- // }
- // catch (HttpRequestException e) {
- // log.Add($"Failed to fetch room {newRoom.RoomId}! {e}");
- // }
- // });
- // await Task.WhenAll(tasks);
- //
- // log.Add($"Done fetching members!");
- //
- // UserIDs.RemoveAll(x => sessions.Any(y => y.UserId == x));
-
- foreach (var session in await RMUStorage.GetAllTokens()) {
- var _hs = await RMUStorage.GetSession(session);
- if (_hs is not null) {
- rooms.AddRange(await _hs.GetJoinedRooms());
- log.Add($"Got {rooms.Count} rooms after adding {_hs.UserId}");
+
+ var sessions = await sessionStore.GetAllSessions();
+ var tasks = sessions.Select(async session => {
+ try {
+ var _hs = await sessionStore.GetHomeserver(session.Key);
+ if (_hs is not null) {
+ try {
+ var _rooms = await _hs.GetJoinedRooms();
+ if (!_rooms.Any()) return;
+ // Check if homeserver supports `?format=event`:
+ await _rooms.First().GetStateEventAsync(RoomMemberEventContent.EventId, session.Value.Auth.UserId);
+ rooms.AddRange(_rooms);
+ log.Add($"Got {_rooms.Count} rooms for {_hs.UserId}, total {rooms.Count}");
+ }
+ catch (Exception e) {
+ if (e is LibMatrixException { ErrorCode: LibMatrixException.ErrorCodes.M_UNSUPPORTED })
+ log.Add($"Homeserver {_hs.UserId} does not support `?format=event`! Skipping...");
+ else log.Add($"Failed to fetch rooms for {_hs.UserId}! {e}");
+ }
+ }
}
- }
+ catch (Exception e) {
+ log.Add($"Failed to fetch rooms for {session.Value.Auth.UserId}! {e}");
+ }
+ });
+ await Task.WhenAll(tasks);
//get distinct rooms evenly distributed per session, accounting for count per session
rooms = rooms.OrderBy(x => rooms.Count(y => y.Homeserver == x.Homeserver)).DistinctBy(x => x.RoomId).ToList();
@@ -125,17 +108,6 @@
foreach (var userId in UserIDs) {
matches.Add(userId, new List<Match>());
- // foreach (var room in rooms) {
- // var state = room.StateEvents.Where(x => x!.Type == RoomMemberEventContent.EventId).ToList();
- // if (state!.Any(x => x.StateKey == userId)) {
- // matches[userId].Add(new() {
- // Event = state.First(x => x.StateKey == userId),
- // Room = room.Room,
- // RoomName = room.RoomName ?? "No name"
- // });
- // }
- // }
-
log.Add($"Searching for {userId}...");
await foreach (var match in GetMatches(userId)) {
matches[userId].Add(match);
@@ -173,13 +145,19 @@
private async IAsyncEnumerable<Match> GetMatches(string userId) {
var results = rooms.Select(async room => {
- var state = await room.GetStateEventOrNullAsync(room.RoomId, userId);
- if (state is not null) {
- return new Match {
- Room = room,
- Event = state,
- RoomName = await room.GetNameOrFallbackAsync()
- };
+ try {
+ var state = await room.GetStateEventOrNullAsync(RoomMemberEventContent.EventId, userId);
+ if (state is not null) {
+ log.Add($"Found {userId} in {room.RoomId} with membership {state.RawContent.ToJson(indent: false)}");
+ return new Match {
+ Room = room,
+ Event = state,
+ RoomName = await room.GetNameOrFallbackAsync()
+ };
+ }
+ }
+ catch (Exception e) {
+ log.Add($"Failed to fetch state for {userId} in {room.RoomId}! {e}");
}
return null;
@@ -191,4 +169,27 @@
}
}
+ public string SummarizeMembership(StateEventResponse state) {
+ var membership = state.ContentAs<RoomMemberEventContent>();
+ var time = DateTimeOffset.FromUnixTimeMilliseconds(state.OriginServerTs!.Value);
+ return membership switch {
+ { Membership: "invite", Reason: null } => $"Invited by {state.Sender} at {time}",
+ { Membership: "invite", Reason: not null } => $"Invited by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "join", Reason: null } => $"Joined at {time}",
+ { Membership: "join", Reason: not null } => $"Joined at {time} for {membership.Reason}",
+ { Membership: "leave", Reason: null } => state.Sender == state.StateKey ? $"Left at {time}" : $"Kicked by {state.Sender} at {time}",
+ { Membership: "leave", Reason: not null } => state.Sender == state.StateKey ? $"Left at {time} with reason {membership.Reason}" : $"Kicked by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "ban", Reason: null } => $"Banned by {state.Sender} at {time}",
+ { Membership: "ban", Reason: not null } => $"Banned by {state.Sender} at {time} for {membership.Reason}",
+ { Membership: "knock", Reason: null } => $"Knocked at {time}",
+ { Membership: "knock", Reason: not null } => $"Knocked at {time} for {membership.Reason}",
+ _ => $"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
|