diff options
Diffstat (limited to 'ModAS.Server/Controllers')
-rw-r--r-- | ModAS.Server/Controllers/AppService/TransactionsController.cs | 80 | ||||
-rw-r--r-- | ModAS.Server/Controllers/DebugController.cs | 94 | ||||
-rw-r--r-- | ModAS.Server/Controllers/HomeController.cs | 8 | ||||
-rw-r--r-- | ModAS.Server/Controllers/RoomQueryController.cs | 135 | ||||
-rw-r--r-- | ModAS.Server/Controllers/TransactionsController.cs | 15 |
5 files changed, 283 insertions, 49 deletions
diff --git a/ModAS.Server/Controllers/AppService/TransactionsController.cs b/ModAS.Server/Controllers/AppService/TransactionsController.cs new file mode 100644 index 0000000..b74e1e1 --- /dev/null +++ b/ModAS.Server/Controllers/AppService/TransactionsController.cs @@ -0,0 +1,80 @@ +using System.IO.Pipelines; +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using ArcaneLibs; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration.Json; +using ModAS.Server; +using ModAS.Server.Services; +using MxApiExtensions.Services; + +namespace ModAS.Server.Controllers.AppService; + +[ApiController] +public class TransactionsController( + AppServiceRegistration asr, + ModASConfiguration config, + UserProviderService userProvider, + RoomContextService roomContextService, + RoomStateCacheService stateCacheService) : ControllerBase { + private static List<string> _ignoredInvalidationEvents { get; set; } = [ + RoomMessageEventContent.EventId, + RoomMessageReactionEventContent.EventId + ]; + + [HttpPut("/_matrix/app/v1/transactions/{txnId}")] + public async Task<IActionResult> PutTransactions(string txnId) { + if (!Request.Headers.ContainsKey("Authorization")) { + Console.WriteLine("PutTransaction: missing authorization header"); + return Unauthorized(); + } + + if (Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")?.Parameter != asr.HomeserverToken) { + Console.WriteLine($"PutTransaction: invalid authorization header: {Request.Headers["Authorization"]}"); + return Unauthorized(); + } + + var data = await JsonSerializer.DeserializeAsync<EventList>(Request.Body); + Console.WriteLine( + $"PutTransaction: {txnId}: {data.Events.Count} events, {Util.BytesToString(Request.Headers.ContentLength ?? Request.ContentLength ?? Request.Body.Length)}"); + + if (!Directory.Exists("data")) + Directory.CreateDirectory("data"); + Directory.CreateDirectory($"data/{txnId}"); + // var pipe = PipeReader.Create(Request.Body); + // await using var file = System.IO.File.OpenWrite($"data/{txnId}"); + // await pipe.CopyToAsync(file); + // await pipe.CompleteAsync(); + // + // Console.WriteLine($"PutTransaction: {txnId}: {Util.BytesToString(file.Length)}"); + for (var i = 0; i < data.Events.Count; i++) { + var evt = data.Events[i]; + Console.WriteLine($"PutTransaction: {txnId}/{i}: {evt.Type} {evt.StateKey} {evt.Sender}"); + await System.IO.File.WriteAllTextAsync($"data/{txnId}/{i}-{evt.Type}-{evt.StateKey}-{evt.Sender}.json", JsonSerializer.Serialize(evt)); + + if (evt.Sender.EndsWith(':' + config.ServerName)) { + Console.WriteLine("PutTransaction: sender is local user, updating data..."); + try { + var user = await userProvider.GetImpersonatedHomeserver(evt.Sender); + var rooms = await user.GetJoinedRooms(); + foreach (var room in rooms) { + await roomContextService.GetRoomContext(room); + } + } + catch (Exception e) { + Console.WriteLine($"PutTransaction: failed to update data: {e}"); + } + } + else + Console.WriteLine("PutTransaction: sender is remote user"); + + if (!string.IsNullOrWhiteSpace(evt.RoomId) && !_ignoredInvalidationEvents.Contains(evt.Type)) + await stateCacheService.InvalidateRoomState(evt.RoomId); + } + + return Ok(new { }); + } +} \ No newline at end of file diff --git a/ModAS.Server/Controllers/DebugController.cs b/ModAS.Server/Controllers/DebugController.cs index d3c7ad0..f0fe91e 100644 --- a/ModAS.Server/Controllers/DebugController.cs +++ b/ModAS.Server/Controllers/DebugController.cs @@ -1,14 +1,17 @@ +using System.Collections.Frozen; using ArcaneLibs.Extensions; +using Elastic.Apm; +using Elastic.Apm.Api; using LibMatrix; using LibMatrix.Homeservers; using Microsoft.AspNetCore.Mvc; using ModAS.Server.Services; using MxApiExtensions.Services; -namespace WebApplication1.Controllers; +namespace ModAS.Server.Controllers; [ApiController] -public class DebugController(ModASConfiguration config, AuthenticatedHomeserverProviderService authHsProvider) : ControllerBase { +public class DebugController(ModASConfiguration config, UserProviderService authHsProvider, RoomContextService roomContextService) : ControllerBase { [HttpGet("/_matrix/_modas/debug")] public IActionResult Index() { return Ok(new { @@ -29,40 +32,73 @@ public class DebugController(ModASConfiguration config, AuthenticatedHomeserverP [HttpGet("/_matrix/_modas/debug/test_locate_users")] public async IAsyncEnumerable<string> TestLocateUsers([FromQuery] string startUser) { - List<string> foundUsers = [startUser], processedRooms = new List<string>(); + List<AuthenticatedHomeserverGeneric> foundUsers = (await authHsProvider.GetValidUsers()).Select(x=>x.Value).ToList(); + if(!foundUsers.Any(x=>x.WhoAmI.UserId == startUser)) { + foundUsers.Add(await authHsProvider.GetImpersonatedHomeserver(startUser)); + } + + List<string> processedRooms = [], processedUsers = []; var foundNew = true; while (foundNew) { + var span1 = currentTransaction.StartSpan("iterateUsers", ApiConstants.TypeApp); foundNew = false; - foreach (var user in foundUsers.ToList()) { - AuthenticatedHomeserverGeneric? ahs = null; - try { - ahs = await authHsProvider.GetImpersonatedHomeserver(user); - await ahs.GetJoinedRooms(); - } - catch (MatrixException e) { - if (e is { ErrorCode: "M_FORBIDDEN" }) continue; - throw; - } + var usersToProcess = foundUsers.Where(x => !processedUsers.Any(y=>x.WhoAmI.UserId == y)).ToFrozenSet(); + Console.WriteLine($"Got {usersToProcess.Count} users: {string.Join(", ", usersToProcess)}"); - if(ahs is null) continue; - var rooms = await ahs.GetJoinedRooms(); - Console.WriteLine($"Got {rooms.Count} rooms"); - rooms.RemoveAll(r => processedRooms.Contains(r.RoomId)); - processedRooms.AddRange(rooms.Select(r => r.RoomId)); - foundNew = rooms.Count > 0; - Console.WriteLine($"Found {rooms.Count} new rooms"); - - var roomMemberTasks = rooms.Select(r => r.GetMembersListAsync(false)).ToAsyncEnumerable(); - await foreach (var roomMembers in roomMemberTasks) { - Console.WriteLine($"Got {roomMembers.Count} members"); - foreach (var member in roomMembers) { - if (!member.StateKey.EndsWith(':' + config.ServerName)) continue; - if (foundUsers.Contains(member.StateKey)) continue; - foundUsers.Add(member.StateKey); - yield return member.StateKey; + var rooms = usersToProcess.Select(async x => await x.GetJoinedRooms()); + var roomLists = rooms.ToAsyncEnumerable(); + await foreach (var roomList in roomLists) { + if (roomList is null) continue; + foreach (var room in roomList) { + if (processedRooms.Contains(room.RoomId)) continue; + processedRooms.Add(room.RoomId); + var roomMembers = await room.GetMembersListAsync(false); + foreach (var roomMember in roomMembers) { + if (roomMember.StateKey.EndsWith(':' + config.ServerName) && !foundUsers.Any(x=>x.WhoAmI.UserId == roomMember.StateKey)) { + foundUsers.Add(await authHsProvider.GetImpersonatedHomeserver(roomMember.StateKey)); + foundNew = true; + yield return roomMember.StateKey; + } } } } + + // await foreach (var task in tasks) { + // if (task is null) continue; + // foreach (var user in task) { + // if (foundUsers.Contains(user)) continue; + // foundUsers.Add(user); + // foundNew = true; + // yield return user; + // } + // } + + span1.End(); } } + + [HttpGet("/_matrix/_modas/debug/room_contexts")] + public IActionResult RoomContexts() { + return Ok(roomContextService.RoomContexts.Values); + } + + [HttpGet("/_matrix/_modas/debug/room_contexts/{roomId}")] + public async Task<IActionResult> RoomContext(string roomId) { + var roomContext = await roomContextService.GetRoomContext(roomId); + if (roomContext is null) return NotFound("Room not found"); + return Ok(roomContext); + } + + [HttpGet("/_matrix/_modas/debug/room_contexts/by_user/{userId}")] + public async IAsyncEnumerable<RoomContextService.RoomContext> RoomContextByUser(string userId) { + var user = await authHsProvider.GetImpersonatedHomeserver(userId); + var rooms = await user.GetJoinedRooms(); + var contexts = rooms.Select(x => roomContextService.GetRoomContext(x.RoomId)).ToAsyncEnumerable(); + await foreach (var context in contexts) { + if (context is null) continue; + yield return context; + } + } + + private static ITransaction currentTransaction => Agent.Tracer.CurrentTransaction; } \ No newline at end of file diff --git a/ModAS.Server/Controllers/HomeController.cs b/ModAS.Server/Controllers/HomeController.cs index 290441e..5fd309f 100644 --- a/ModAS.Server/Controllers/HomeController.cs +++ b/ModAS.Server/Controllers/HomeController.cs @@ -1,15 +1,13 @@ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; -namespace WebApplication1.Controllers; +namespace ModAS.Server.Controllers; [ApiController] -public class HomeController : Controller -{ +public class HomeController : Controller { private readonly ILogger<HomeController> _logger; - public HomeController(ILogger<HomeController> logger) - { + public HomeController(ILogger<HomeController> logger) { _logger = logger; } diff --git a/ModAS.Server/Controllers/RoomQueryController.cs b/ModAS.Server/Controllers/RoomQueryController.cs new file mode 100644 index 0000000..a49e5c0 --- /dev/null +++ b/ModAS.Server/Controllers/RoomQueryController.cs @@ -0,0 +1,135 @@ +using System.Collections.Frozen; +using ArcaneLibs.Extensions; +using Elastic.Apm; +using Elastic.Apm.Api; +using LibMatrix; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Responses.ModAS; +using LibMatrix.RoomTypes; +using Microsoft.AspNetCore.Mvc; +using ModAS.Classes; +using ModAS.Server.Services; +using MxApiExtensions.Services; + +namespace ModAS.Server.Controllers; + +[ApiController] +public class RoomQueryController(UserProviderService ahsProvider, ModASConfiguration config, RoomStateCacheService stateCacheService, UserProviderService userProviderService) : ControllerBase { + [HttpGet("/_matrix/_modas/room_query")] + public async IAsyncEnumerable<ModASRoomQueryResult> RoomQuery() { + var fetchRoomQueryDataSpan = currentTransaction.StartSpan("fetchRoomQueryData", ApiConstants.TypeApp); + List<string> processedRooms = new(); + var getUsersSpan = currentTransaction.StartSpan("getUsers", ApiConstants.TypeApp); + var validUsers = await ahsProvider.GetValidUsers(); + getUsersSpan.End(); + + var collectRoomsSpan = currentTransaction.StartSpan("collectRooms", ApiConstants.TypeApp); + // var userRoomLists = validUsers.Values.Select(ahs => ahs.GetJoinedRooms()).ToList(); + // await Task.WhenAll(userRoomLists); + // var rooms = userRoomLists.SelectMany(r => r.Result).DistinctBy(x => x.RoomId).ToFrozenSet(); + var roomsTasks = validUsers.Values.Select(u => userProviderService.GetUserRoomsCached(u.WhoAmI.UserId)).ToList(); + await Task.WhenAll(roomsTasks); + var rooms = roomsTasks.SelectMany(r => r.Result).DistinctBy(x => x.RoomId).ToFrozenSet(); + collectRoomsSpan.End(); + + var collectRoomStateSpan = currentTransaction.StartSpan("collectRoomState", ApiConstants.TypeApp); + //make sure we lock!!!! + await stateCacheService.EnsureCachedFromRoomList(rooms); + var roomStateTasks = rooms.Select(GetRoomState).ToAsyncEnumerable(); + var awaitRoomStateSpan = currentTransaction.StartSpan("fetchRoomState", ApiConstants.TypeApp); + await foreach (var (room, roomState) in roomStateTasks) { + awaitRoomStateSpan.Name = $"awaitRoomState {room.RoomId}"; + awaitRoomStateSpan.End(); + + var filterStateEventsSpan = currentTransaction.StartSpan($"filterStateEvents {room.RoomId}", ApiConstants.TypeApp); + var roomMembers = roomState.Where(r => r.Type == RoomMemberEventContent.EventId).ToFrozenSet(); + var localRoomMembers = roomMembers.Where(x => x.StateKey.EndsWith(':' + config.ServerName)).ToFrozenSet(); + var nonMemberState = roomState.Where(x => !roomMembers.Contains(x)).ToFrozenSet(); + filterStateEventsSpan.End(); + var buildResultSpan = currentTransaction.StartSpan($"buildResult {room.RoomId}", ApiConstants.TypeApp); + + // forgive me for raw json access... attempt at optimisation -emma + yield return new ModASRoomQueryResult { + RoomId = room.RoomId, + StateEvents = roomState.Count, + //members + TotalMembers = roomMembers.Count, + TotalLocalMembers = localRoomMembers.Count, + JoinedMembers = roomMembers.Count(x => (x.TypedContent as RoomMemberEventContent)?.Membership == "join"), + JoinedLocalMembers = localRoomMembers.Count(x => (x.TypedContent as RoomMemberEventContent)?.Membership == "join"), + //-members + //creation event + Creator = nonMemberState.FirstOrDefault(r => r.Type == RoomCreateEventContent.EventId)?.Sender, + Version = nonMemberState.FirstOrDefault(r => r.Type == RoomCreateEventContent.EventId)?.RawContent?["room_version"]?.GetValue<string>(), + Type = nonMemberState.FirstOrDefault(r => r.Type == RoomCreateEventContent.EventId)?.RawContent?["type"]?.GetValue<string>(), + Federatable = nonMemberState.FirstOrDefault(r => r.Type == RoomCreateEventContent.EventId)?.RawContent?["m.federate"]?.GetValue<bool>() ?? true, + //-creation event + Name = nonMemberState.FirstOrDefault(r => r.Type == RoomNameEventContent.EventId)?.RawContent?["name"]?.GetValue<string>(), + CanonicalAlias = nonMemberState.FirstOrDefault(r => r.Type == RoomCanonicalAliasEventContent.EventId)?.RawContent?["alias"]?.GetValue<string>(), + JoinRules = nonMemberState.FirstOrDefault(r => r.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.GetValue<string>(), + GuestAccess = nonMemberState.FirstOrDefault(r => r.Type == RoomGuestAccessEventContent.EventId)?.RawContent?["guest_access"]?.GetValue<string>(), + HistoryVisibility = + nonMemberState.FirstOrDefault(r => r.Type == RoomHistoryVisibilityEventContent.EventId)?.RawContent?["history_visibility"]?.GetValue<string>(), + Public = nonMemberState.FirstOrDefault(r => r.Type == RoomJoinRulesEventContent.EventId)?.RawContent?["join_rule"]?.GetValue<string>() == "public", + Encryption = nonMemberState.FirstOrDefault(r => r.Type == RoomEncryptionEventContent.EventId)?.RawContent?["algorithm"]?.GetValue<string>(), + AvatarUrl = nonMemberState.FirstOrDefault(r => r.Type == RoomAvatarEventContent.EventId)?.RawContent?["url"]?.GetValue<string>(), + RoomTopic = nonMemberState.FirstOrDefault(r => r.Type == RoomTopicEventContent.EventId)?.RawContent?["topic"]?.GetValue<string>() + }; + buildResultSpan.End(); + awaitRoomStateSpan = currentTransaction.StartSpan("fetchRoomState", ApiConstants.TypeApp); + } + + collectRoomStateSpan.End(); + fetchRoomQueryDataSpan.End(); + } + + [HttpPost("/_matrix/_modas/room_query")] + public async IAsyncEnumerable<ModASRoomQueryResult> RoomQuery([FromBody] RoomQueryFilter request) { + await foreach (var room in RoomQuery()) { + if (!string.IsNullOrWhiteSpace(request.RoomIdContains) && !room.RoomId.Contains(request.RoomIdContains)) continue; + + if (!string.IsNullOrWhiteSpace(request.NameContains) && room.Name?.Contains(request.NameContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.CanonicalAliasContains) && room.CanonicalAlias?.Contains(request.CanonicalAliasContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.VersionContains) && room.Version?.Contains(request.VersionContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.CreatorContains) && room.Creator?.Contains(request.CreatorContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.EncryptionContains) && room.Encryption?.Contains(request.EncryptionContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.JoinRulesContains) && room.JoinRules?.Contains(request.JoinRulesContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.GuestAccessContains) && room.GuestAccess?.Contains(request.GuestAccessContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.HistoryVisibilityContains) && room.HistoryVisibility?.Contains(request.HistoryVisibilityContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.AvatarUrlContains) && room.AvatarUrl?.Contains(request.AvatarUrlContains) != true) continue; + + if (!string.IsNullOrWhiteSpace(request.RoomTopicContains) && room.RoomTopic?.Contains(request.RoomTopicContains) != true) continue; + + if (request.JoinedMembersMin.HasValue && room.JoinedMembers < request.JoinedMembersMin.Value) continue; + if (request.JoinedMembersMax.HasValue && room.JoinedMembers > request.JoinedMembersMax.Value) continue; + + if (request.JoinedLocalMembersMin.HasValue && room.JoinedLocalMembers < request.JoinedLocalMembersMin.Value) continue; + if (request.JoinedLocalMembersMax.HasValue && room.JoinedLocalMembers > request.JoinedLocalMembersMax.Value) continue; + + if (request.StateEventsMin.HasValue && room.StateEvents < request.StateEventsMin.Value) continue; + if (request.StateEventsMax.HasValue && room.StateEvents > request.StateEventsMax.Value) continue; + + if (request.IsFederatable.HasValue && room.Federatable != request.IsFederatable.Value) continue; + + if (request.IsPublic.HasValue && room.Public != request.IsPublic.Value) continue; + + yield return room; + } + } + + private async Task<KeyValuePair<GenericRoom, FrozenSet<StateEventResponse>>> GetRoomState(GenericRoom room) { + var roomState = await stateCacheService.GetRoomState(room.RoomId, room); + return new(room, roomState); + } + + private static ITransaction currentTransaction => Agent.Tracer.CurrentTransaction; +} \ No newline at end of file diff --git a/ModAS.Server/Controllers/TransactionsController.cs b/ModAS.Server/Controllers/TransactionsController.cs deleted file mode 100644 index 8e4e018..0000000 --- a/ModAS.Server/Controllers/TransactionsController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.IO.Pipelines; -using Microsoft.AspNetCore.Mvc; -using ModAS.Server; - -namespace WebApplication1.Controllers; - -[ApiController] -public class TransactionsController(AppServiceRegistration asr) : ControllerBase { - [HttpPut(" /_matrix/app/v1/transactions/{txnId}")] - public async Task<IActionResult> PutTransactions(string txnId) { - if(!Request.Headers.ContainsKey("Authorization") || Request.Headers["Authorization"] != asr.HomeserverToken) return Unauthorized(); - await Request.Body.CopyToAsync(Console.OpenStandardOutput()); - return Ok(new{}); - } -} \ No newline at end of file |