@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(); } }