diff --git a/MxApiExtensions/Controllers/ClientVersionsController.cs b/MxApiExtensions/Controllers/ClientVersionsController.cs
new file mode 100644
index 0000000..60a3364
--- /dev/null
+++ b/MxApiExtensions/Controllers/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.GetClientVersions();
+
+ _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/Extensions/JoinedRoomListController.cs b/MxApiExtensions/Controllers/Extensions/JoinedRoomListController.cs
new file mode 100644
index 0000000..3c4161d
--- /dev/null
+++ b/MxApiExtensions/Controllers/Extensions/JoinedRoomListController.cs
@@ -0,0 +1,144 @@
+using System.Collections.Concurrent;
+using System.Net.Http.Headers;
+using ArcaneLibs.Extensions;
+using LibMatrix.Homeservers;
+using LibMatrix.MxApiExtensions;
+using LibMatrix.RoomTypes;
+using LibMatrix.StateEventTypes.Spec;
+using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Services;
+
+namespace MxApiExtensions.Controllers.Extensions;
+
+[ApiController]
+[Route("/_matrix/client/unstable/gay.rory.mxapiextensions")]
+public class JoinedRoomListController : ControllerBase {
+ private static ILogger _logger;
+ private static MxApiExtensionsConfiguration _config;
+ private readonly AuthenticationService _authenticationService;
+ private readonly AuthenticatedHomeserverProviderService _authenticatedHomeserverProviderService;
+
+ private static ConcurrentDictionary<string, RoomInfoEntry> _roomInfoCache = new();
+
+ public JoinedRoomListController(ILogger<JoinedRoomListController> logger, MxApiExtensionsConfiguration config, AuthenticationService authenticationService,
+ AuthenticatedHomeserverProviderService authenticatedHomeserverProviderService) {
+ _logger = logger;
+ _config = config;
+ _authenticationService = authenticationService;
+ _authenticatedHomeserverProviderService = authenticatedHomeserverProviderService;
+ }
+
+ [HttpGet("joined_rooms_with_info")]
+ public async IAsyncEnumerable<RoomInfoEntry> GetJoinedRooms([FromQuery] string? access_token) {
+ List<GenericRoom> rooms = new();
+ AuthenticatedHomeserverGeneric? hs = null;
+ try {
+ hs = await _authenticatedHomeserverProviderService.GetHomeserver();
+ _logger.LogInformation("Got room list with info request for {user} ({hs})", hs.UserId, hs.FullHomeServerDomain);
+ rooms = await hs.GetJoinedRooms();
+ }
+ catch (MxApiMatrixException e) {
+ _logger.LogError(e, "Matrix error");
+ Response.StatusCode = StatusCodes.Status500InternalServerError;
+ Response.ContentType = "application/json";
+
+ await Response.WriteAsJsonAsync(e.GetAsJson());
+ await Response.CompleteAsync();
+ }
+ catch (Exception e) {
+ _logger.LogError(e, "Unhandled error");
+ Response.StatusCode = StatusCodes.Status500InternalServerError;
+ Response.ContentType = "text/plain";
+
+ await Response.WriteAsJsonAsync(e.ToString());
+ await Response.CompleteAsync();
+ }
+
+ if (hs is not null) {
+ Response.ContentType = "application/json";
+ Response.Headers.Add("Cache-Control", "public, max-age=60");
+ Response.Headers.Add("Expires", DateTime.Now.AddMinutes(1).ToString("R"));
+ Response.Headers.Add("Last-Modified", DateTime.Now.ToString("R"));
+ Response.Headers.Add("X-Matrix-Server", hs.FullHomeServerDomain);
+ Response.Headers.Add("X-Matrix-User", hs.UserId);
+ // await Response.StartAsync();
+
+ var cachedRooms = _roomInfoCache
+ .Where(cr => rooms.Any(r => r.RoomId == cr.Key) && cr.Value.ExpiresAt > DateTime.Now)
+ .ToList();
+ rooms.RemoveAll(r => cachedRooms.Any(cr => cr.Key == r.RoomId));
+
+ foreach (var room in cachedRooms) {
+ yield return room.Value;
+ _logger.LogInformation("Sent cached room info for {room} for {user} ({hs})", room.Key, hs.UserId, hs.FullHomeServerDomain);
+ }
+
+ var tasks = rooms.Select(r => GetRoomInfo(hs, r.RoomId)).ToAsyncEnumerable();
+
+ await foreach (var result in tasks) {
+ yield return result;
+ _logger.LogInformation("Sent room info for {room} for {user} ({hs})", result.RoomId, hs.UserId, hs.FullHomeServerDomain);
+ }
+ }
+ }
+
+ private SemaphoreSlim _roomInfoSemaphore = new(100, 100);
+
+ private async Task<RoomInfoEntry> GetRoomInfo(AuthenticatedHomeserverGeneric hs, string roomId) {
+ _logger.LogInformation("Getting room info for {room} for {user} ({hs})", roomId, hs.UserId, hs.FullHomeServerDomain);
+ var room = await hs.GetRoom(roomId);
+ var state = room.GetFullStateAsync();
+ var result = new RoomInfoEntry {
+ RoomId = roomId,
+ RoomState = new(),
+ MemberCounts = new(),
+ StateCount = 0,
+ ExpiresAt = DateTime.Now.AddMinutes(5)
+ };
+
+ await foreach (var @event in state) {
+ // result.ExpiresAt = result.ExpiresAt.AddMilliseconds(100);
+ result.StateCount++;
+ if (@event.Type != "m.room.member") result.RoomState.Add(@event);
+ else {
+ if(!result.MemberCounts.ContainsKey((@event.TypedContent as RoomMemberEventData)?.Membership)) result.MemberCounts.Add((@event.TypedContent as RoomMemberEventData)?.Membership, 0);
+ result.MemberCounts[(@event.TypedContent as RoomMemberEventData)?.Membership]++;
+ }
+ }
+
+ result.ExpiresAt = result.ExpiresAt.AddMilliseconds(100 * result.StateCount);
+
+ _logger.LogInformation("Got room info for {room} for {user} ({hs})", roomId, hs.UserId, hs.FullHomeServerDomain);
+ while (!_roomInfoCache.TryAdd(roomId, result)) {
+ _logger.LogWarning("Failed to add room info for {room} to cache, retrying...", roomId);
+ await Task.Delay(100);
+ if (_roomInfoCache.ContainsKey(roomId)) break;
+ }
+
+ return result;
+ }
+
+ [HttpGet("joined_rooms_with_info_cache")]
+ public async Task<object> GetRoomInfoCache() {
+ var mxid = await _authenticationService.GetMxidFromToken();
+ if(!_config.Admins.Contains(mxid)) {
+ Response.StatusCode = StatusCodes.Status403Forbidden;
+ Response.ContentType = "application/json";
+
+ await Response.WriteAsJsonAsync(new {
+ ErrorCode = "M_FORBIDDEN",
+ Error = "You are not an admin"
+ });
+ await Response.CompleteAsync();
+ return null;
+ }
+
+ return _roomInfoCache.Select(x => new {
+ x.Key,
+ x.Value.ExpiresAt,
+ ExpiresIn = x.Value.ExpiresAt - DateTime.Now,
+ x.Value.MemberCounts,
+ x.Value.StateCount
+ }).OrderByDescending(x => x.ExpiresAt);
+ }
+}
diff --git a/MxApiExtensions/Controllers/Extensions/ProxyConfigurationController.cs b/MxApiExtensions/Controllers/Extensions/ProxyConfigurationController.cs
new file mode 100644
index 0000000..71bf167
--- /dev/null
+++ b/MxApiExtensions/Controllers/Extensions/ProxyConfigurationController.cs
@@ -0,0 +1,43 @@
+using System.Collections.Concurrent;
+using LibMatrix.MxApiExtensions;
+using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Services;
+
+namespace MxApiExtensions.Controllers.Extensions;
+
+[ApiController]
+[Route("/_matrix/client/unstable/gay.rory.mxapiextensions")]
+public class ProxyConfigurationController : ControllerBase {
+ private readonly ILogger _logger;
+ private readonly MxApiExtensionsConfiguration _config;
+ private readonly AuthenticationService _authenticationService;
+
+ private static ConcurrentDictionary<string, RoomInfoEntry> _roomInfoCache = new();
+
+ public ProxyConfigurationController(ILogger<ProxyConfigurationController> logger, MxApiExtensionsConfiguration config, AuthenticationService authenticationService,
+ AuthenticatedHomeserverProviderService authenticatedHomeserverProviderService) {
+ _logger = logger;
+ _config = config;
+ _authenticationService = authenticationService;
+ }
+
+ [HttpGet("proxy_config")]
+ public async Task<MxApiExtensionsConfiguration> GetConfig() {
+ var mxid = await _authenticationService.GetMxidFromToken();
+ if(!_config.Admins.Contains(mxid)) {
+ _logger.LogWarning("Got proxy config request for {user}, but they are not an admin", mxid);
+ Response.StatusCode = StatusCodes.Status403Forbidden;
+ Response.ContentType = "application/json";
+
+ await Response.WriteAsJsonAsync(new {
+ ErrorCode = "M_FORBIDDEN",
+ Error = "You are not an admin"
+ });
+ await Response.CompleteAsync();
+ return null;
+ }
+
+ _logger.LogInformation("Got proxy config request for {user}", mxid);
+ return _config;
+ }
+}
diff --git a/MxApiExtensions/Controllers/GenericProxyController.cs b/MxApiExtensions/Controllers/GenericProxyController.cs
index f0ad4e7..4e27b4a 100644
--- a/MxApiExtensions/Controllers/GenericProxyController.cs
+++ b/MxApiExtensions/Controllers/GenericProxyController.cs
@@ -1,5 +1,6 @@
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Services;
namespace MxApiExtensions.Controllers;
@@ -7,21 +8,75 @@ namespace MxApiExtensions.Controllers;
[Route("/{*_}")]
public class GenericController : ControllerBase {
private readonly ILogger<GenericController> _logger;
- private readonly CacheConfiguration _config;
- private readonly Auth _auth;
+ private readonly MxApiExtensionsConfiguration _config;
+ private readonly AuthenticationService _authenticationService;
+ private readonly AuthenticatedHomeserverProviderService _authenticatedHomeserverProviderService;
private static Dictionary<string, string> _tokenMap = new();
- public GenericController(ILogger<GenericController> logger, CacheConfiguration config, Auth auth) {
+ public GenericController(ILogger<GenericController> logger, MxApiExtensionsConfiguration config, AuthenticationService authenticationService,
+ AuthenticatedHomeserverProviderService authenticatedHomeserverProviderService) {
_logger = logger;
_config = config;
- _auth = auth;
+ _authenticationService = authenticationService;
+ _authenticatedHomeserverProviderService = authenticatedHomeserverProviderService;
}
[HttpGet]
- public async Task Proxy([FromQuery] string? access_token, string _) {
+ public async Task Proxy([FromQuery] string? access_token, string? _) {
try {
- access_token ??= _auth.GetToken(fail: false);
- var mxid = _auth.GetUserId(fail: false);
+ access_token ??= _authenticationService.GetToken(fail: false);
+ var mxid = await _authenticationService.GetMxidFromToken(fail: false);
+ var hs = await _authenticatedHomeserverProviderService.GetHomeserver();
+
+ _logger.LogInformation("Proxying request for {}: {}{}", mxid, Request.Path, Request.QueryString);
+
+ //remove access_token from query string
+ Request.QueryString = new QueryString(
+ Request.QueryString.Value?.Replace("&access_token", "access_token")
+ .Replace($"access_token={access_token}", "")
+ );
+
+ var resp = await hs._httpClient.GetAsync($"{Request.Path}{Request.QueryString}");
+
+ if (resp.Content is null) {
+ throw new MxApiMatrixException {
+ ErrorCode = "M_UNKNOWN",
+ Error = "No content in response"
+ };
+ }
+
+ Response.StatusCode = (int)resp.StatusCode;
+ Response.ContentType = resp.Content.Headers.ContentType?.ToString() ?? "application/json";
+ await Response.StartAsync();
+ await using var stream = await resp.Content.ReadAsStreamAsync();
+ await stream.CopyToAsync(Response.Body);
+ await Response.Body.FlushAsync();
+ await Response.CompleteAsync();
+ }
+ catch (MxApiMatrixException e) {
+ _logger.LogError(e, "Matrix error");
+ Response.StatusCode = StatusCodes.Status500InternalServerError;
+ Response.ContentType = "application/json";
+
+ await Response.WriteAsync(e.GetAsJson());
+ await Response.CompleteAsync();
+ }
+ catch (Exception e) {
+ _logger.LogError(e, "Unhandled error");
+ Response.StatusCode = StatusCodes.Status500InternalServerError;
+ Response.ContentType = "text/plain";
+
+ await Response.WriteAsync(e.ToString());
+ await Response.CompleteAsync();
+ }
+ }
+
+ [HttpPost]
+ public async Task ProxyPost([FromQuery] string? access_token, string _) {
+ try {
+ access_token ??= _authenticationService.GetToken(fail: false);
+ var mxid = await _authenticationService.GetMxidFromToken(fail: false);
+ var hs = await _authenticatedHomeserverProviderService.GetHomeserver();
_logger.LogInformation("Proxying request for {}: {}{}", mxid, Request.Path, Request.QueryString);
@@ -35,10 +90,13 @@ public class GenericController : ControllerBase {
.Replace($"access_token={access_token}", "")
);
- var resp = await hc.GetAsync($"{_config.Homeserver}{Request.Path}{Request.QueryString}");
+ var resp = await hs._httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"{Request.Path}{Request.QueryString}") {
+ Method = HttpMethod.Post,
+ Content = new StreamContent(Request.Body)
+ });
if (resp.Content is null) {
- throw new MatrixException {
+ throw new MxApiMatrixException {
ErrorCode = "M_UNKNOWN",
Error = "No content in response"
};
@@ -51,9 +109,8 @@ public class GenericController : ControllerBase {
await stream.CopyToAsync(Response.Body);
await Response.Body.FlushAsync();
await Response.CompleteAsync();
-
}
- catch (MatrixException e) {
+ catch (MxApiMatrixException e) {
_logger.LogError(e, "Matrix error");
Response.StatusCode = StatusCodes.Status500InternalServerError;
Response.ContentType = "application/json";
@@ -71,11 +128,12 @@ public class GenericController : ControllerBase {
}
}
- [HttpPost]
- public async Task ProxyPost([FromQuery] string? access_token, string _) {
+ [HttpPut]
+ public async Task ProxyPut([FromQuery] string? access_token, string _) {
try {
- access_token ??= _auth.GetToken(fail: false);
- var mxid = _auth.GetUserId(fail: false);
+ access_token ??= _authenticationService.GetToken(fail: false);
+ var mxid = await _authenticationService.GetMxidFromToken(fail: false);
+ var hs = await _authenticatedHomeserverProviderService.GetHomeserver();
_logger.LogInformation("Proxying request for {}: {}{}", mxid, Request.Path, Request.QueryString);
@@ -89,14 +147,13 @@ public class GenericController : ControllerBase {
.Replace($"access_token={access_token}", "")
);
- var resp = await hc.SendAsync(new HttpRequestMessage {
- Method = HttpMethod.Post,
- RequestUri = new Uri($"{_config.Homeserver}{Request.Path}{Request.QueryString}"),
+ var resp = await hs._httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Put, $"{Request.Path}{Request.QueryString}") {
+ Method = HttpMethod.Put,
Content = new StreamContent(Request.Body)
});
if (resp.Content is null) {
- throw new MatrixException {
+ throw new MxApiMatrixException {
ErrorCode = "M_UNKNOWN",
Error = "No content in response"
};
@@ -109,9 +166,8 @@ public class GenericController : ControllerBase {
await stream.CopyToAsync(Response.Body);
await Response.Body.FlushAsync();
await Response.CompleteAsync();
-
}
- catch (MatrixException e) {
+ catch (MxApiMatrixException e) {
_logger.LogError(e, "Matrix error");
Response.StatusCode = StatusCodes.Status500InternalServerError;
Response.ContentType = "application/json";
diff --git a/MxApiExtensions/Controllers/LoginController.cs b/MxApiExtensions/Controllers/LoginController.cs
new file mode 100644
index 0000000..1a7970a
--- /dev/null
+++ b/MxApiExtensions/Controllers/LoginController.cs
@@ -0,0 +1,69 @@
+using System.Net.Http.Headers;
+using LibMatrix;
+using LibMatrix.Extensions;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Services;
+
+namespace MxApiExtensions.Controllers;
+
+[ApiController]
+[Route("/")]
+public class LoginController : ControllerBase {
+ private readonly ILogger _logger;
+ private readonly HomeserverProviderService _hsProvider;
+ private readonly HomeserverResolverService _hsResolver;
+ private readonly AuthenticationService _auth;
+ private readonly MxApiExtensionsConfiguration _conf;
+
+ public LoginController(ILogger<LoginController> logger, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, AuthenticationService auth, MxApiExtensionsConfiguration conf) {
+ _logger = logger;
+ _hsProvider = hsProvider;
+ _hsResolver = hsResolver;
+ _auth = auth;
+ _conf = conf;
+ }
+
+ [HttpPost("/_matrix/client/{_}/login")]
+ public async Task Proxy([FromBody] LoginRequest request, string _) {
+ 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();
+ }
+ var 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) };
+ //hsClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", hsClient.DefaultRequestHeaders.Authorization!.Parameter);
+ 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"
+ }
+ }
+ };
+ }
+}
diff --git a/MxApiExtensions/Controllers/SyncController.cs b/MxApiExtensions/Controllers/SyncController.cs
index d883377..382d670 100644
--- a/MxApiExtensions/Controllers/SyncController.cs
+++ b/MxApiExtensions/Controllers/SyncController.cs
@@ -1,5 +1,19 @@
+using System.Collections.Concurrent;
using System.Net.Http.Headers;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Web;
+using LibMatrix;
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+using LibMatrix.StateEventTypes.Spec;
using Microsoft.AspNetCore.Mvc;
+using MxApiExtensions.Classes;
+using MxApiExtensions.Extensions;
+using MxApiExtensions.Services;
namespace MxApiExtensions.Controllers;
@@ -7,103 +21,255 @@ namespace MxApiExtensions.Controllers;
[Route("/")]
public class SyncController : ControllerBase {
private readonly ILogger<SyncController> _logger;
- private readonly CacheConfiguration _config;
- private readonly Auth _auth;
+ private readonly MxApiExtensionsConfiguration _config;
+ private readonly AuthenticationService _auth;
+ private readonly AuthenticatedHomeserverProviderService _hs;
- public SyncController(ILogger<SyncController> logger, CacheConfiguration config, Auth auth) {
+ private static readonly ConcurrentDictionary<string, SyncState> _syncStates = new();
+
+ public SyncController(ILogger<SyncController> logger, MxApiExtensionsConfiguration config, AuthenticationService auth, AuthenticatedHomeserverProviderService hs) {
_logger = logger;
_config = config;
_auth = auth;
+ _hs = hs;
}
[HttpGet("/_matrix/client/v3/sync")]
- public async Task Sync([FromQuery] string? since, [FromQuery] string? access_token) {
+ public async Task Sync([FromQuery] string? since, [FromQuery] int timeout = 1000) {
+ Task? preloadTask = null;
+ AuthenticatedHomeserverGeneric? hs = null;
+ try {
+ hs = await _hs.GetHomeserver();
+ }
+ catch (Exception e) {
+ Console.WriteLine();
+ }
+ var qs = HttpUtility.ParseQueryString(Request.QueryString.Value!);
+ qs.Remove("access_token");
+
+ if (!_config.FastInitialSync.Enabled) {
+ _logger.LogInformation("Starting sync for {} on {} ({})", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+ var result = await hs._httpClient.GetAsync($"{Request.Path}?{qs}");
+ await Response.WriteHttpResponse(result);
+ return;
+ }
+
try {
- access_token ??= _auth.GetToken();
- var mxid = _auth.GetUserId();
- var cacheFile = GetFilePath(mxid, since);
-
- if (!await TrySendCached(cacheFile)) {
- using var hc = new HttpClient();
- hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
- hc.Timeout = TimeSpan.FromMinutes(10);
- //remove access_token from query string
- Request.QueryString = new QueryString(
- Request.QueryString.Value
- .Replace("&access_token", "access_token")
- .Replace($"access_token={access_token}", "")
- );
-
- var resp = hc.GetAsync($"{_config.Homeserver}{Request.Path}{Request.QueryString}").Result;
- // var resp = await hs._httpClient.GetAsync($"/_matrix/client/v3/sync?since={since}");
-
- if (resp.Content is null) {
- throw new MatrixException {
- ErrorCode = "M_UNKNOWN",
- Error = "No content in response"
- };
+ var syncState = _syncStates.GetOrAdd(hs.AccessToken, _ => {
+ _logger.LogInformation("Started tracking sync state for {} on {} ({})", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+ return new SyncState {
+ IsInitialSync = string.IsNullOrWhiteSpace(since),
+ Homeserver = hs
+ };
+ });
+
+ if (syncState.NextSyncResult is null) {
+ _logger.LogInformation("Starting sync for {} on {} ({})", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+
+ if (syncState.IsInitialSync) {
+ preloadTask = EnqueuePreloadData(syncState);
}
- Response.StatusCode = (int)resp.StatusCode;
+ syncState.NextSyncResultStartedAt = DateTime.Now;
+ syncState.NextSyncResult = Task.Delay(30_000);
+ syncState.NextSyncResult.ContinueWith(x => {
+ _logger.LogInformation("Sync for {} on {} ({}) starting", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+ syncState.NextSyncResult = hs._httpClient.GetAsync($"{Request.Path}?{qs}");
+ });
+ }
+
+ if (syncState.SyncQueue.Count > 0) {
+ _logger.LogInformation("Sync for {} on {} ({}) has {} queued results", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken, syncState.SyncQueue.Count);
+ syncState.SyncQueue.TryDequeue(out var result);
+
+ Response.StatusCode = StatusCodes.Status200OK;
Response.ContentType = "application/json";
await Response.StartAsync();
- await using var stream = await resp.Content.ReadAsStreamAsync();
- await using var target = System.IO.File.OpenWrite(cacheFile);
- var buffer = new byte[1];
-
- int bytesRead;
- while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
- await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead));
- target.Write(buffer, 0, bytesRead);
- }
-
- await target.FlushAsync();
+ await JsonSerializer.SerializeAsync(Response.Body, result, new JsonSerializerOptions {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
await Response.CompleteAsync();
+ return;
+ }
+
+ timeout = Math.Clamp(timeout, 0, 100);
+ _logger.LogInformation("Sync for {} on {} ({}) is still running, waiting for {}ms, {} elapsed", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken, timeout,
+ DateTime.Now.Subtract(syncState.NextSyncResultStartedAt));
+
+ try {
+ await syncState.NextSyncResult.WaitAsync(TimeSpan.FromMilliseconds(timeout));
+ }
+ catch { }
+
+ if (syncState.NextSyncResult is Task<HttpResponseMessage> { IsCompleted: true } response) {
+ _logger.LogInformation("Sync for {} on {} ({}) completed", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+ var resp = await response;
+ await Response.WriteHttpResponse(resp);
+ return;
}
+
+ // await Task.Delay(timeout);
+ _logger.LogInformation("Sync for {} on {} ({}): sending bogus response", hs.WhoAmI.UserId, hs.HomeServerDomain, hs.AccessToken);
+ Response.StatusCode = StatusCodes.Status200OK;
+ Response.ContentType = "application/json";
+ await Response.StartAsync();
+ var syncResult = new SyncResult {
+ // NextBatch = "MxApiExtensions::Next" + Random.Shared.NextInt64(),
+ NextBatch = since ?? "",
+ Presence = new() {
+ Events = new() {
+ await GetStatusMessage(syncState, $"{DateTime.Now.Subtract(syncState.NextSyncResultStartedAt)} {syncState.NextSyncResult.Status}")
+ }
+ },
+ Rooms = new() {
+ Invite = new(),
+ Join = new()
+ }
+ };
+ await JsonSerializer.SerializeAsync(Response.Body, syncResult, new JsonSerializerOptions {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+ await Response.CompleteAsync();
}
- catch (MatrixException e) {
+ catch (MxApiMatrixException e) {
+ _logger.LogError(e, "Error while syncing for {} on {} ({})", _hs.GetHomeserver().Result.WhoAmI.UserId,
+ _hs.GetHomeserver().Result.HomeServerDomain, _hs.GetHomeserver().Result.AccessToken);
+
Response.StatusCode = StatusCodes.Status500InternalServerError;
Response.ContentType = "application/json";
await Response.WriteAsJsonAsync(e.GetAsJson());
await Response.CompleteAsync();
}
+
catch (Exception e) {
+ //catch SSL connection errors and retry
+ if (e.InnerException is HttpRequestException && e.InnerException.Message.Contains("The SSL connection could not be established")) {
+ _logger.LogWarning("Caught SSL connection error, retrying sync for {} on {} ({})", _hs.GetHomeserver().Result.WhoAmI.UserId,
+ _hs.GetHomeserver().Result.HomeServerDomain, _hs.GetHomeserver().Result.AccessToken);
+ await Sync(since, timeout);
+ return;
+ }
+
+ _logger.LogError(e, "Error while syncing for {} on {} ({})", _hs.GetHomeserver().Result.WhoAmI.UserId,
+ _hs.GetHomeserver().Result.HomeServerDomain, _hs.GetHomeserver().Result.AccessToken);
+
Response.StatusCode = StatusCodes.Status500InternalServerError;
Response.ContentType = "text/plain";
await Response.WriteAsync(e.ToString());
await Response.CompleteAsync();
}
+
+ Response.Body.Close();
+ if (preloadTask is not null)
+ await preloadTask;
}
- private async Task<bool> TrySendCached(string cacheFile) {
- if (!System.IO.File.Exists(cacheFile)) return false;
+ private async Task EnqueuePreloadData(SyncState syncState) {
+ var rooms = await syncState.Homeserver.GetJoinedRooms();
+ var dm_rooms = (await syncState.Homeserver.GetAccountData<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.HomeServerDomain, syncState.Homeserver.AccessToken);
- Response.StatusCode = 200;
- Response.ContentType = "application/json";
- await Response.StartAsync();
- await using var stream = System.IO.File.OpenRead(cacheFile);
- await stream.CopyToAsync(Response.Body);
- await Response.CompleteAsync();
- return true;
+ await Task.WhenAll(roomDataTasks);
}
-#region Cache management
+ private SemaphoreSlim _roomDataSemaphore = new(4, 4);
- public string GetFilePath(string mxid, string since) {
- var cacheDir = Path.Join("cache", mxid);
- Directory.CreateDirectory(cacheDir);
- var cacheFile = Path.Join(cacheDir, $"sync-{since}.json");
- if (!Path.GetFullPath(cacheFile).StartsWith(Path.GetFullPath(cacheDir))) {
- throw new MatrixException {
- ErrorCode = "M_UNKNOWN",
- Error = "[Rory&::MxSyncCache] Cache file path is not in cache directory"
- };
+ 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 syncResult = new SyncResult {
+ Rooms = new() {
+ Join = new() {
+ {
+ room.RoomId,
+ new SyncResult.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.NextSyncResultStartedAt)} {syncState.NextSyncResult.Status} {room.RoomId}")
+ }
+ },
+ NextBatch = ""
+ };
+
+ await foreach (var stateEvent in roomState) {
+ syncResult.Rooms.Join[room.RoomId].State.Events.Add(stateEvent);
}
- return cacheFile;
+ var joinRoom = syncResult.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 RoomMemberEventData).Membership == "join"
+ )
+ .Select(x => x.StateKey));
+ joinRoom.Summary.JoinedMemberCount = joinRoom.Summary.Heroes.Count;
+
+ syncState.SyncQueue.Enqueue(syncResult);
+ _roomDataSemaphore.Release();
}
-#endregion
+ private async Task<StateEventResponse> GetStatusMessage(SyncState syncState, string message) {
+ return new StateEventResponse() {
+ TypedContent = new PresenceStateEventData() {
+ 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,
+ UserId = syncState.Homeserver.WhoAmI.UserId,
+ EventId = Guid.NewGuid().ToString(),
+ OriginServerTs = 0
+ };
+ }
}
diff --git a/MxApiExtensions/Controllers/WellKnownController.cs b/MxApiExtensions/Controllers/WellKnownController.cs
new file mode 100644
index 0000000..b27451f
--- /dev/null
+++ b/MxApiExtensions/Controllers/WellKnownController.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Nodes;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MxApiExtensions.Controllers;
+
+[ApiController]
+[Route("/")]
+public class WellKnownController : ControllerBase {
+ private readonly MxApiExtensionsConfiguration _config;
+
+ public WellKnownController(MxApiExtensionsConfiguration config) {
+ _config = config;
+ }
+
+ [HttpGet("/.well-known/matrix/client")]
+ public object GetWellKnown() {
+ var res = new JsonObject();
+ res.Add("m.homeserver", new JsonObject {
+ { "base_url", Request.Scheme + "://" + Request.Host + "/" },
+ });
+ return res;
+ }
+}
|