summary refs log tree commit diff
path: root/MxApiExtensions/Controllers/Client
diff options
context:
space:
mode:
Diffstat (limited to 'MxApiExtensions/Controllers/Client')
-rw-r--r--MxApiExtensions/Controllers/Client/ClientVersionsController.cs52
-rw-r--r--MxApiExtensions/Controllers/Client/LoginController.cs73
-rw-r--r--MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs18
-rw-r--r--MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs72
-rw-r--r--MxApiExtensions/Controllers/Client/SyncController.cs243
5 files changed, 458 insertions, 0 deletions
diff --git a/MxApiExtensions/Controllers/Client/ClientVersionsController.cs b/MxApiExtensions/Controllers/Client/ClientVersionsController.cs
new file mode 100644

index 0000000..d29e3b2 --- /dev/null +++ b/MxApiExtensions/Controllers/Client/ClientVersionsController.cs
@@ -0,0 +1,52 @@ +using System.Net.Http.Headers; +using LibMatrix.Responses; +using Microsoft.AspNetCore.Mvc; +using MxApiExtensions.Services; + +namespace MxApiExtensions.Controllers; + +[ApiController] +[Route("/")] +public class ClientVersionsController : ControllerBase { + private readonly ILogger _logger; + private readonly AuthenticatedHomeserverProviderService _authenticatedHomeserverProviderService; + private static Dictionary<string, string> _tokenMap = new(); + + public ClientVersionsController(ILogger<ClientVersionsController> logger, MxApiExtensionsConfiguration config, AuthenticationService authenticationService, AuthenticatedHomeserverProviderService authenticatedHomeserverProviderService) { + _logger = logger; + _authenticatedHomeserverProviderService = authenticatedHomeserverProviderService; + } + + [HttpGet("/_matrix/client/versions")] + public async Task<ClientVersionsResponse> Proxy([FromQuery] string? access_token, string? _) { + var clientVersions = new ClientVersionsResponse { + Versions = new() { + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2", + "v1.3", + "v1.4", + "v1.5", + "v1.6" + }, + UnstableFeatures = new() + }; + try { + var hs = await _authenticatedHomeserverProviderService.GetHomeserver(); + clientVersions = await hs.GetClientVersionsAsync(); + + _logger.LogInformation("Fetching client versions for {}: {}{}", hs.WhoAmI.UserId, Request.Path, Request.QueryString); + } + catch { } + + clientVersions.UnstableFeatures.Add("gay.rory.mxapiextensions.v0", true); + return clientVersions; + } +} diff --git a/MxApiExtensions/Controllers/Client/LoginController.cs b/MxApiExtensions/Controllers/Client/LoginController.cs new file mode 100644
index 0000000..009aaef --- /dev/null +++ b/MxApiExtensions/Controllers/Client/LoginController.cs
@@ -0,0 +1,73 @@ +using System.Net.Http.Headers; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.Extensions; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; +using MxApiExtensions.Classes.LibMatrix; +using MxApiExtensions.Services; + +namespace MxApiExtensions.Controllers; + +[ApiController] +[Route("/")] +public class LoginController(ILogger<LoginController> logger, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, AuthenticationService auth, + MxApiExtensionsConfiguration conf) + : ControllerBase { + private readonly ILogger _logger = logger; + private readonly HomeserverProviderService _hsProvider = hsProvider; + private readonly MxApiExtensionsConfiguration _conf = conf; + + [HttpPost("/_matrix/client/{_}/login")] + public async Task Proxy([FromBody] LoginRequest request, string _) { + string hsCanonical = null; + if (Request.Headers.Keys.Any(x => x.ToUpper() == "MXAE_UPSTREAM")) { + hsCanonical = Request.Headers.GetByCaseInsensitiveKey("MXAE_UPSTREAM")[0]!; + _logger.LogInformation("Found upstream: {}", hsCanonical); + } + else { + if (!request.Identifier.User.Contains("#")) { + Response.StatusCode = (int)StatusCodes.Status403Forbidden; + Response.ContentType = "application/json"; + await Response.StartAsync(); + await Response.WriteAsync(new MxApiMatrixException { + ErrorCode = "M_FORBIDDEN", + Error = "[MxApiExtensions] Invalid username, must be of the form @user#domain:" + Request.Host.Value + }.GetAsJson() ?? ""); + await Response.CompleteAsync(); + } + + hsCanonical = request.Identifier.User.Split('#')[1].Split(':')[0]; + request.Identifier.User = request.Identifier.User.Split(':')[0].Replace('#', ':'); + if (!request.Identifier.User.StartsWith('@')) request.Identifier.User = '@' + request.Identifier.User; + } + + var hs = await hsResolver.ResolveHomeserverFromWellKnown(hsCanonical); + //var hs = await _hsProvider.Login(hsCanonical, mxid, request.Password); + var hsClient = new MatrixHttpClient { BaseAddress = new Uri(hs.Client) }; + //hsClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", hsClient.DefaultRequestHeaders.Authorization!.Parameter); + if (!string.IsNullOrWhiteSpace(request.InitialDeviceDisplayName)) + request.InitialDeviceDisplayName += $" (via MxApiExtensions at {Request.Host.Value})"; + var resp = await hsClient.PostAsJsonAsync("/_matrix/client/r0/login", request); + var loginResp = await resp.Content.ReadAsStringAsync(); + Response.StatusCode = (int)resp.StatusCode; + Response.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json"; + await Response.StartAsync(); + await Response.WriteAsync(loginResp); + await Response.CompleteAsync(); + var token = (await resp.Content.ReadFromJsonAsync<LoginResponse>())!.AccessToken; + await auth.SaveMxidForToken(token, request.Identifier.User); + } + + [HttpGet("/_matrix/client/{_}/login")] + public async Task<object> Proxy(string? _) { + return new { + flows = new[] { + new { + type = "m.login.password" + } + } + }; + } +} \ No newline at end of file diff --git a/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs b/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs new file mode 100644
index 0000000..3d1d4e2 --- /dev/null +++ b/MxApiExtensions/Controllers/Client/Room/RoomsSendMessageController.cs
@@ -0,0 +1,18 @@ +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; +using MxApiExtensions.Services; + +namespace MxApiExtensions.Controllers.Client.Room; + +[ApiController] +[Route("/")] +public class RoomController(ILogger<LoginController> logger, HomeserverResolverService hsResolver, AuthenticationService auth, MxApiExtensionsConfiguration conf, + AuthenticatedHomeserverProviderService hsProvider) + : ControllerBase { + [HttpGet("/_matrix/client/{_}/rooms/{roomId}/members_by_homeserver")] + public async Task<Dictionary<string, List<string>>> GetRoomMembersByHomeserver(string _, [FromRoute] string roomId, [FromQuery] bool joinedOnly = true) { + var hs = await hsProvider.GetHomeserver(); + var room = hs.GetRoom(roomId); + return await room.GetMembersByHomeserverAsync(); + } +} \ No newline at end of file diff --git a/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs b/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs new file mode 100644
index 0000000..6d3a774 --- /dev/null +++ b/MxApiExtensions/Controllers/Client/RoomsSendMessageController.cs
@@ -0,0 +1,72 @@ +using System.Buffers.Text; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Extensions; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.Services; +using Microsoft.AspNetCore.Mvc; +using MxApiExtensions.Classes; +using MxApiExtensions.Classes.LibMatrix; +using MxApiExtensions.Services; + +namespace MxApiExtensions.Controllers; + +[ApiController] +[Route("/")] +public class RoomsSendMessageController(ILogger<LoginController> logger, HomeserverResolverService hsResolver, AuthenticationService auth, MxApiExtensionsConfiguration conf, + AuthenticatedHomeserverProviderService hsProvider) + : ControllerBase { + [HttpPut("/_matrix/client/{_}/rooms/{roomId}/send/m.room.message/{txnId}")] + public async Task Proxy([FromBody] JsonObject request, [FromRoute] string roomId, [FromRoute] string txnId, string _) { + var hs = await hsProvider.GetHomeserver(); + + var msg = request.Deserialize<RoomMessageEventContent>(); + if (msg is not null && msg.Body.StartsWith("mxae!")) { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + handleMxaeCommand(hs, roomId, msg); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + await Response.WriteAsJsonAsync(new EventIdResponse() { + EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100)) + }); + await Response.CompleteAsync(); + } + else { + try { + var resp = await hs.ClientHttpClient.PutAsJsonAsync($"{Request.Path}{Request.QueryString}", request); + var loginResp = await resp.Content.ReadAsStringAsync(); + Response.StatusCode = (int)resp.StatusCode; + Response.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json"; + await Response.StartAsync(); + await Response.WriteAsync(loginResp); + await Response.CompleteAsync(); + } + catch (MatrixException e) { + await Response.StartAsync(); + await Response.WriteAsync(e.GetAsJson()); + await Response.CompleteAsync(); + } + } + } + + private async Task handleMxaeCommand(AuthenticatedHomeserverGeneric hs, string roomId, RoomMessageEventContent msg) { + var syncState = SyncController._syncStates.GetValueOrDefault(hs.AccessToken); + if (syncState is null) return; + syncState.SendEphemeralTimelineEventInRoom(roomId, new() { + Sender = "@mxae:" + Request.Host.Value, + Type = "m.room.message", + TypedContent = MessageFormatter.FormatSuccess("Thinking..."), + OriginServerTs = (ulong)new DateTimeOffset(DateTime.UtcNow.ToUniversalTime()).ToUnixTimeMilliseconds(), + Unsigned = new() { + Age = 1 + }, + RoomId = roomId, + EventId = "$" + string.Join("", Random.Shared.GetItems("abcdefghijklmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ0123456789".ToCharArray(), 100)) + }); + } +} \ No newline at end of file diff --git a/MxApiExtensions/Controllers/Client/SyncController.cs b/MxApiExtensions/Controllers/Client/SyncController.cs new file mode 100644
index 0000000..2944c3b --- /dev/null +++ b/MxApiExtensions/Controllers/Client/SyncController.cs
@@ -0,0 +1,243 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Web; +using ArcaneLibs; +using LibMatrix; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using Microsoft.AspNetCore.Mvc; +using MxApiExtensions.Classes; +using MxApiExtensions.Classes.LibMatrix; +using MxApiExtensions.Extensions; +using MxApiExtensions.Services; + +namespace MxApiExtensions.Controllers; + +[ApiController] +[Route("/")] +public class SyncController(ILogger<SyncController> logger, MxApiExtensionsConfiguration config, AuthenticationService auth, AuthenticatedHomeserverProviderService hsProvider) + : ControllerBase { + public static readonly ConcurrentDictionary<string, SyncState> _syncStates = new(); + + private static SemaphoreSlim _semaphoreSlim = new(1, 1); + private Stopwatch _syncElapsed = Stopwatch.StartNew(); + + [HttpGet("/_matrix/client/{_}/sync")] + public async Task Sync(string _, [FromQuery] string? since, [FromQuery] int timeout = 1000) { + Task? preloadTask = null; + AuthenticatedHomeserverGeneric? hs = null; + try { + hs = await hsProvider.GetHomeserver(); + } + catch (Exception e) { + Console.WriteLine(e); + } + + var qs = HttpUtility.ParseQueryString(Request.QueryString.Value!); + qs.Remove("access_token"); + if (since == "null") qs.Remove("since"); + + if (!config.FastInitialSync.Enabled) { + logger.LogInformation("Starting sync for {} on {} ({})", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken); + var result = await hs.ClientHttpClient.GetAsync($"{Request.Path}?{qs}"); + await Response.WriteHttpResponse(result); + return; + } + + await _semaphoreSlim.WaitAsync(); + var syncState = _syncStates.GetOrAdd($"{hs.WhoAmI.UserId}/{hs.WhoAmI.DeviceId}/{hs.ServerName}:{hs.AccessToken}", _ => { + logger.LogInformation("Started tracking sync state for {} on {} ({})", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken); + var ss = new SyncState { + IsInitialSync = string.IsNullOrWhiteSpace(since), + Homeserver = hs + }; + if (ss.IsInitialSync) { + preloadTask = EnqueuePreloadData(ss); + } + + logger.LogInformation("Starting sync for {} on {} ({})", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken); + + ss.NextSyncResponseStartedAt = DateTime.Now; + ss.NextSyncResponse = Task.Delay(15_000); + ss.NextSyncResponse.ContinueWith(async x => { + logger.LogInformation("Sync for {} on {} ({}) starting", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken); + ss.NextSyncResponse = hs.ClientHttpClient.GetAsync($"/_matrix/client/v3/sync?{qs}"); + (ss.NextSyncResponse as Task<HttpResponseMessage>).ContinueWith(async x => EnqueueSyncResponse(ss, await x)); + }); + return ss; + }); + _semaphoreSlim.Release(); + + if (syncState.SyncQueue.Count > 0) { + logger.LogInformation("Sync for {} on {} ({}) has {} queued results", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken, syncState.SyncQueue.Count); + syncState.SyncQueue.TryDequeue(out var result); + while (result is null) + syncState.SyncQueue.TryDequeue(out result); + Response.StatusCode = StatusCodes.Status200OK; + Response.ContentType = "application/json"; + await Response.StartAsync(); + result.NextBatch ??= since ?? syncState.NextBatch; + await JsonSerializer.SerializeAsync(Response.Body, result, new JsonSerializerOptions { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + await Response.CompleteAsync(); + return; + } + + var newTimeout = Math.Clamp(timeout, 0, syncState.IsInitialSync ? syncState.SyncQueue.Count >= 2 ? 0 : 250 : timeout); + logger.LogInformation("Sync for {} on {} ({}) is still running, waiting for {}ms, {} elapsed", hs.WhoAmI.UserId, hs.ServerName, hs.AccessToken, newTimeout, + DateTime.Now.Subtract(syncState.NextSyncResponseStartedAt)); + + try { + if (syncState.NextSyncResponse is not null) + await syncState.NextSyncResponse.WaitAsync(TimeSpan.FromMilliseconds(newTimeout)); + else { + syncState.NextSyncResponse = hs.ClientHttpClient.GetAsync($"/_matrix/client/v3/sync?{qs}"); + (syncState.NextSyncResponse as Task<HttpResponseMessage>).ContinueWith(async x => EnqueueSyncResponse(syncState, await x)); + // await Task.Delay(250); + } + } + catch (TimeoutException) { } + + // if (_syncElapsed.ElapsedMilliseconds > timeout) + if(syncState.NextSyncResponse?.IsCompleted == false) + syncState.SendStatusMessage( + $"M={Util.BytesToString(Process.GetCurrentProcess().WorkingSet64)} TE={DateTime.Now.Subtract(syncState.NextSyncResponseStartedAt)} S={syncState.NextSyncResponse?.Status} QL={syncState.SyncQueue.Count}"); + Response.StatusCode = StatusCodes.Status200OK; + Response.ContentType = "application/json"; + await Response.StartAsync(); + var response = syncState.SyncQueue.FirstOrDefault(); + if (response is null) + response = new(); + response.NextBatch ??= since ?? syncState.NextBatch; + await JsonSerializer.SerializeAsync(Response.Body, response, new JsonSerializerOptions { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + await Response.CompleteAsync(); + + Response.Body.Close(); + if (preloadTask is not null) + await preloadTask; + } + + private async Task EnqueuePreloadData(SyncState syncState) { + var rooms = await syncState.Homeserver.GetJoinedRooms(); + var dm_rooms = (await syncState.Homeserver.GetAccountDataAsync<Dictionary<string, List<string>>>("m.direct")).Aggregate(new List<string>(), (list, entry) => { + list.AddRange(entry.Value); + return list; + }); + + var ownHs = syncState.Homeserver.WhoAmI.UserId.Split(':')[1]; + rooms = rooms.OrderBy(x => { + if (dm_rooms.Contains(x.RoomId)) return -1; + var parts = x.RoomId.Split(':'); + if (parts[1] == ownHs) return 200; + if (HomeserverWeightEstimation.EstimatedSize.ContainsKey(parts[1])) return HomeserverWeightEstimation.EstimatedSize[parts[1]] + parts[0].Length; + return 5000; + }).ToList(); + var roomDataTasks = rooms.Select(room => EnqueueRoomData(syncState, room)).ToList(); + logger.LogInformation("Preloading data for {} rooms on {} ({})", roomDataTasks.Count, syncState.Homeserver.ServerName, syncState.Homeserver.AccessToken); + + await Task.WhenAll(roomDataTasks); + } + + private SemaphoreSlim _roomDataSemaphore = new(32, 32); + + private async Task EnqueueRoomData(SyncState syncState, GenericRoom room) { + await _roomDataSemaphore.WaitAsync(); + var roomState = room.GetFullStateAsync(); + var timeline = await room.GetMessagesAsync(limit: 100, dir: "b"); + timeline.Chunk.Reverse(); + var SyncResponse = new SyncResponse { + Rooms = new() { + Join = new() { + { + room.RoomId, + new SyncResponse.RoomsDataStructure.JoinedRoomDataStructure { + AccountData = new() { + Events = new() + }, + Ephemeral = new() { + Events = new() + }, + State = new() { + Events = timeline.State + }, + UnreadNotifications = new() { + HighlightCount = 0, + NotificationCount = 0 + }, + Timeline = new() { + Events = timeline.Chunk, + Limited = false, + PrevBatch = timeline.Start + }, + Summary = new() { + Heroes = new(), + InvitedMemberCount = 0, + JoinedMemberCount = 1 + } + } + } + } + }, + Presence = new() { + Events = new() { + await GetStatusMessage(syncState, $"{DateTime.Now.Subtract(syncState.NextSyncResponseStartedAt)} {syncState.NextSyncResponse.Status} {room.RoomId}") + } + }, + NextBatch = "" + }; + + await foreach (var stateEvent in roomState) { + SyncResponse.Rooms.Join[room.RoomId].State.Events.Add(stateEvent); + } + + var joinRoom = SyncResponse.Rooms.Join[room.RoomId]; + joinRoom.Summary.Heroes.AddRange(joinRoom.State.Events + .Where(x => + x.Type == "m.room.member" + && x.StateKey != syncState.Homeserver.WhoAmI.UserId + && (x.TypedContent as RoomMemberEventContent).Membership == "join" + ) + .Select(x => x.StateKey)); + joinRoom.Summary.JoinedMemberCount = joinRoom.Summary.Heroes.Count; + + syncState.SyncQueue.Enqueue(SyncResponse); + _roomDataSemaphore.Release(); + } + + private async Task<StateEventResponse> GetStatusMessage(SyncState syncState, string message) { + return new StateEventResponse { + TypedContent = new PresenceEventContent { + DisplayName = "MxApiExtensions", + Presence = "online", + StatusMessage = message, + // AvatarUrl = (await syncState.Homeserver.GetProfile(syncState.Homeserver.WhoAmI.UserId)).AvatarUrl + AvatarUrl = "" + }, + Type = "m.presence", + StateKey = syncState.Homeserver.WhoAmI.UserId, + Sender = syncState.Homeserver.WhoAmI.UserId, + EventId = Guid.NewGuid().ToString(), + OriginServerTs = 0 + }; + } + + private async Task EnqueueSyncResponse(SyncState ss, HttpResponseMessage task) { + var sr = await task.Content.ReadFromJsonAsync<JsonObject>(); + if (sr.ContainsKey("error")) throw sr.Deserialize<MatrixException>()!; + ss.NextBatch = sr["next_batch"].GetValue<string>(); + ss.IsInitialSync = false; + ss.SyncQueue.Enqueue(sr.Deserialize<SyncResponse>()); + ss.NextSyncResponse = null; + } +} \ No newline at end of file