@page "/Tools/Moderation/UserTrace"
@using ArcaneLibs.Extensions
@using LibMatrix.RoomTypes
@using System.Collections.ObjectModel
@using LibMatrix
@using LibMatrix.EventTypes.Spec.State.RoomInfo
User Trace
Users:
Import from room (ID)
Rooms to be searched (@rooms.Count)
@foreach (var room in rooms) {
@room.RoomId
}
Execute
Results
@foreach (var (userId, events) in matches.OrderBy(x=>x.Key)) {
@userId
@foreach (var match in events.OrderBy(x=>x.RoomName)) {
@match.RoomName (@match.Room.RoomId) |
@SummarizeMembership(match.Event)
@match.Event.RawContent.ToJson(indent: true)
|
}
}
@foreach (var line in log.Reverse()) {
@line
}
@code {
private ObservableCollection log { get; set; } = new();
// List rooms { get; set; } = new();
List rooms { get; set; } = [];
Dictionary> 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 UserIDs { get; set; } = new();
protected override async Task OnInitializedAsync() {
log.CollectionChanged += (sender, args) => StateHasChanged();
var hs = await RmuStorage.GetCurrentSessionOrNavigate();
if (hs is null) return;
var sessions = await RmuStorage.GetAllTokens();
var tasks = sessions.Select(async session => {
try {
var _hs = await RmuStorage.GetSession(session);
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.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.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();
log.Add($"Got {rooms.Count} rooms");
StateHasChanged();
Console.WriteLine("Rerendered!");
await base.OnInitializedAsync();
}
private async Task Execute() {
foreach (var userId in UserIDs) {
matches.Add(userId, new List());
log.Add($"Searching for {userId}...");
await foreach (var match in GetMatches(userId)) {
matches[userId].Add(match);
}
}
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 GetMatches(string userId) {
var results = rooms.Select(async room => {
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;
}).ToAsyncEnumerable();
await foreach (var result in results) {
if (result is not null) {
yield return result;
}
}
}
public string SummarizeMembership(StateEventResponse state) {
var membership = state.ContentAs();
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();
}
}