diff options
Diffstat (limited to 'LibMatrix')
15 files changed, 341 insertions, 337 deletions
diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index 60b1fc1..598f8e5 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using ArcaneLibs; using ArcaneLibs.Extensions; namespace LibMatrix.Extensions; @@ -38,6 +39,7 @@ public class MatrixHttpClient : HttpClient { } public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); // if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId); @@ -97,6 +99,16 @@ public class MatrixHttpClient : HttpClient { SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); // GetFromJsonAsync + public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + try { + return await GetFromJsonAsync<T>(requestUri, options, cancellationToken); + } + catch (HttpRequestException e) { + Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); + return default; + } + } + public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { options = GetJsonSerializerOptions(options); // Console.WriteLine($"GetFromJsonAsync called for {requestUri} with json options {options?.ToJson(ignoreNull:true)} and cancellation token {cancellationToken}"); diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index 727c0ea..afa6a6c 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -14,49 +14,35 @@ using LibMatrix.Responses; using LibMatrix.RoomTypes; using LibMatrix.Services; using LibMatrix.Utilities; +using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Homeservers; -public class AuthenticatedHomeserverGeneric(string serverName, string accessToken) : RemoteHomeserver(serverName) { - public static async Task<T> Create<T>(string serverName, string accessToken, string? proxy = null) where T : AuthenticatedHomeserverGeneric => - await Create(typeof(T), serverName, accessToken, proxy) as T ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}"); - - public static async Task<AuthenticatedHomeserverGeneric> Create(Type type, string serverName, string accessToken, string? proxy = null) { - if (string.IsNullOrWhiteSpace(proxy)) - proxy = null; - if (!type.IsAssignableTo(typeof(AuthenticatedHomeserverGeneric))) throw new ArgumentException("Type must be a subclass of AuthenticatedHomeserverGeneric", nameof(type)); - var instance = Activator.CreateInstance(type, serverName, accessToken) as AuthenticatedHomeserverGeneric - ?? throw new InvalidOperationException($"Failed to create instance of {type.Name}"); - - instance.ClientHttpClient = new MatrixHttpClient { - Timeout = TimeSpan.FromMinutes(15), - DefaultRequestHeaders = { - Authorization = new AuthenticationHeaderValue("Bearer", accessToken) - } - }; - instance.FederationClient = await FederationClient.TryCreate(serverName, proxy); +public class AuthenticatedHomeserverGeneric : RemoteHomeserver { + public AuthenticatedHomeserverGeneric(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, ref string? proxy, string accessToken) : base(serverName, + wellKnownUris, ref proxy) { + AccessToken = accessToken; + ClientHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - if (string.IsNullOrWhiteSpace(proxy)) { - var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(serverName); - instance.ClientHttpClient.BaseAddress = new Uri(urls?.Client ?? throw new InvalidOperationException("Failed to resolve homeserver")); - } - else { - instance.ClientHttpClient.BaseAddress = new Uri(proxy); - instance.ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", serverName); - } + NamedCaches = new HsNamedCaches(this); + } + + public async Task Initialise() { + WhoAmI = await ClientHttpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"); + } - instance.WhoAmI = await instance.ClientHttpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"); - instance.NamedCaches = new HsNamedCaches(instance); + private WhoAmIResponse? _whoAmI; - return instance; + public WhoAmIResponse WhoAmI { + get => _whoAmI ?? throw new Exception("Initialise was not called or awaited, WhoAmI is null!"); + private set => _whoAmI = value; } - public WhoAmIResponse WhoAmI { get; set; } public string UserId => WhoAmI.UserId; public string UserLocalpart => UserId.Split(":")[0][1..]; public string ServerName => UserId.Split(":", 2)[1]; - public string AccessToken { get; set; } = accessToken; + public string AccessToken { get; set; } public HsNamedCaches NamedCaches { get; set; } = null!; diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs index f0b5675..f55acb8 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs @@ -1,3 +1,6 @@ +using LibMatrix.Services; + namespace LibMatrix.Homeservers; -public class AuthenticatedHomeserverMxApiExtended(string baseUrl, string accessToken) : AuthenticatedHomeserverGeneric(baseUrl, accessToken); \ No newline at end of file +public class AuthenticatedHomeserverMxApiExtended(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, ref string? proxy, string accessToken) + : AuthenticatedHomeserverGeneric(serverName, wellKnownUris, ref proxy, accessToken); \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs index 8df0c5b..307a226 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs @@ -1,113 +1,17 @@ using ArcaneLibs.Extensions; using LibMatrix.Filters; +using LibMatrix.Homeservers.ImplementationDetails.Synapse; using LibMatrix.Responses.Admin; +using LibMatrix.Services; namespace LibMatrix.Homeservers; public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric { - public readonly SynapseAdminApi Admin; + public readonly SynapseAdminApiClient Admin; - public class SynapseAdminApi(AuthenticatedHomeserverSynapse authenticatedHomeserver) { - public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", - string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) { - AdminRoomListingResult? res = null; - var i = 0; - int? totalRooms = null; - do { - var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 100)}&dir={dir}&order_by={orderBy}"; - if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; - - if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; - - Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); - - res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<AdminRoomListingResult>(url); - totalRooms ??= res.TotalRooms; - Console.WriteLine(res.ToJson(false)); - foreach (var room in res.Rooms) { - if (localFilter is not null) { - if (!room.RoomId.Contains(localFilter.RoomIdContains)) { - totalRooms--; - continue; - } - - if (!room.Name?.Contains(localFilter.NameContains) == true) { - totalRooms--; - continue; - } - - if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) { - totalRooms--; - continue; - } - - if (!room.Version.Contains(localFilter.VersionContains)) { - totalRooms--; - continue; - } - - if (!room.Creator.Contains(localFilter.CreatorContains)) { - totalRooms--; - continue; - } - - if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) { - totalRooms--; - continue; - } - - if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) { - totalRooms--; - continue; - } - - if (!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) { - totalRooms--; - continue; - } - - if (!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) { - totalRooms--; - continue; - } - - if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { - totalRooms--; - continue; - } - - if (localFilter.CheckPublic && room.Public != localFilter.Public) { - totalRooms--; - continue; - } - - if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { - totalRooms--; - continue; - } - - if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { - totalRooms--; - continue; - } - } - // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) && - // !( - // room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || - // room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || - // room.Creator?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true - // ) - // ) { - // totalRooms--; - // continue; - // } - - i++; - yield return room; - } - } while (i < Math.Min(limit, totalRooms ?? limit)); - } + public AuthenticatedHomeserverSynapse(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, ref string? proxy, string accessToken) : base(serverName, + wellKnownUris, ref proxy, accessToken) { + Admin = new(this); } - public AuthenticatedHomeserverSynapse(string serverName, string accessToken) : base(serverName, accessToken) => Admin = new SynapseAdminApi(this); } \ No newline at end of file diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs index 3926b29..dc0d1f6 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs @@ -1,41 +1,19 @@ using System.Text.Json.Serialization; using LibMatrix.Extensions; using LibMatrix.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Homeservers; -public class FederationClient(string baseUrl) { - public static async Task<FederationClient?> TryCreate(string baseUrl, string? proxy = null) { - try { - return await Create(baseUrl, proxy); - } - catch (Exception e) { - Console.WriteLine($"Failed to create homeserver {baseUrl}: {e.Message}"); - return null; - } +public class FederationClient { + public FederationClient(string federationEndpoint, string? proxy = null) { + HttpClient = new MatrixHttpClient { + BaseAddress = new Uri(proxy?.TrimEnd('/') ?? federationEndpoint.TrimEnd('/')), + Timeout = TimeSpan.FromSeconds(120) + }; + if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint); } - public static async Task<FederationClient> Create(string baseUrl, string? proxy = null) { - var homeserver = new FederationClient(baseUrl); - homeserver.WellKnownUris = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl); - if (string.IsNullOrWhiteSpace(proxy) && string.IsNullOrWhiteSpace(homeserver.WellKnownUris.Client)) - Console.WriteLine($"Failed to resolve homeserver client URI for {baseUrl}"); - if (string.IsNullOrWhiteSpace(proxy) && string.IsNullOrWhiteSpace(homeserver.WellKnownUris.Server)) - Console.WriteLine($"Failed to resolve homeserver server URI for {baseUrl}"); - - if (!string.IsNullOrWhiteSpace(homeserver.WellKnownUris.Server)) - homeserver.HttpClient = new MatrixHttpClient { - BaseAddress = new Uri(proxy ?? homeserver.WellKnownUris.Server ?? throw new InvalidOperationException($"Failed to resolve homeserver server URI for {baseUrl}")), - Timeout = TimeSpan.FromSeconds(120) - }; - - if (proxy is not null) homeserver.HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); - - return homeserver; - } - - public string BaseUrl { get; } = baseUrl; - public MatrixHttpClient HttpClient { get; set; } = null!; public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } = null!; @@ -46,7 +24,6 @@ public class ServerVersionResponse { [JsonPropertyName("server")] public required ServerInfo Server { get; set; } - // ReSharper disable once ClassNeverInstantiated.Global public class ServerInfo { [JsonPropertyName("name")] public string Name { get; set; } diff --git a/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs index ceb1b3f..f4c927a 100644 --- a/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LibMatrix.Responses.Admin; +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; public class AdminRoomDeleteRequest { [JsonPropertyName("new_room_user_id")] diff --git a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs index 7ab96ac..7ab96ac 100644 --- a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs new file mode 100644 index 0000000..5cc055d --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs @@ -0,0 +1,107 @@ +using ArcaneLibs.Extensions; +using LibMatrix.Filters; +using LibMatrix.Responses.Admin; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; + +public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedHomeserver) { + public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", + string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) { + AdminRoomListingResult? res = null; + var i = 0; + int? totalRooms = null; + do { + var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 100)}&dir={dir}&order_by={orderBy}"; + if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; + + if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; + + Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); + + res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<AdminRoomListingResult>(url); + totalRooms ??= res.TotalRooms; + Console.WriteLine(res.ToJson(false)); + foreach (var room in res.Rooms) { + if (localFilter is not null) { + if (!room.RoomId.Contains(localFilter.RoomIdContains)) { + totalRooms--; + continue; + } + + if (!room.Name?.Contains(localFilter.NameContains) == true) { + totalRooms--; + continue; + } + + if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) { + totalRooms--; + continue; + } + + if (!room.Version.Contains(localFilter.VersionContains)) { + totalRooms--; + continue; + } + + if (!room.Creator.Contains(localFilter.CreatorContains)) { + totalRooms--; + continue; + } + + if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) { + totalRooms--; + continue; + } + + if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) { + totalRooms--; + continue; + } + + if (!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) { + totalRooms--; + continue; + } + + if (!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) { + totalRooms--; + continue; + } + + if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { + totalRooms--; + continue; + } + + if (localFilter.CheckPublic && room.Public != localFilter.Public) { + totalRooms--; + continue; + } + + if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { + totalRooms--; + continue; + } + + if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { + totalRooms--; + continue; + } + } + // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) && + // !( + // room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || + // room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || + // room.Creator?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true + // ) + // ) { + // totalRooms--; + // continue; + // } + + i++; + yield return room; + } + } while (i < Math.Min(limit, totalRooms ?? limit)); + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index 8cd7ad7..e6d58b1 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -6,50 +6,32 @@ using ArcaneLibs.Extensions; using LibMatrix.Extensions; using LibMatrix.Responses; using LibMatrix.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Homeservers; -public class RemoteHomeserver(string baseUrl) { - public static async Task<RemoteHomeserver?> TryCreate(string baseUrl, string? proxy = null) { - try { - return await Create(baseUrl, proxy); - } - catch (Exception e) { - Console.WriteLine($"Failed to create homeserver {baseUrl}: {e.Message}"); - return null; - } - } - - public static async Task<RemoteHomeserver> Create(string baseUrl, string? proxy = null) { +public class RemoteHomeserver { + public RemoteHomeserver(string baseUrl, HomeserverResolverService.WellKnownUris wellKnownUris, ref string? proxy) { if (string.IsNullOrWhiteSpace(proxy)) proxy = null; - var homeserver = new RemoteHomeserver(baseUrl); - homeserver.WellKnownUris = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl); - if (string.IsNullOrWhiteSpace(homeserver.WellKnownUris.Client)) - Console.WriteLine($"Failed to resolve homeserver client URI for {baseUrl}"); - if (string.IsNullOrWhiteSpace(homeserver.WellKnownUris.Server)) - Console.WriteLine($"Failed to resolve homeserver server URI for {baseUrl}"); - - Console.WriteLine(homeserver.WellKnownUris.ToJson(ignoreNull: false)); - - homeserver.ClientHttpClient = new MatrixHttpClient { - BaseAddress = new Uri(proxy ?? homeserver.WellKnownUris.Client ?? throw new InvalidOperationException($"Failed to resolve homeserver client URI for {baseUrl}")), + BaseUrl = baseUrl; + WellKnownUris = wellKnownUris; + ClientHttpClient = new MatrixHttpClient { + BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")), Timeout = TimeSpan.FromSeconds(300) }; - homeserver.FederationClient = await FederationClient.TryCreate(baseUrl, proxy); - - if (proxy is not null) homeserver.ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); - - return homeserver; + if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); + if (!string.IsNullOrWhiteSpace(wellKnownUris.Server)) + FederationClient = new FederationClient(WellKnownUris.Server!, proxy); } private Dictionary<string, object> _profileCache { get; set; } = new(); - public string BaseUrl { get; } = baseUrl; + public string BaseUrl { get; } - public MatrixHttpClient ClientHttpClient { get; set; } = null!; + public MatrixHttpClient ClientHttpClient { get; set; } public FederationClient? FederationClient { get; set; } - public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } = null!; + public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } public async Task<UserProfileResponse> GetProfileAsync(string mxid, bool useCache = false) { if (mxid is null) throw new ArgumentNullException(nameof(mxid)); diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs index 3962fa6..28fb245 100644 --- a/LibMatrix/Responses/LoginResponse.cs +++ b/LibMatrix/Responses/LoginResponse.cs @@ -21,9 +21,10 @@ public class LoginResponse { [JsonPropertyName("user_id")] public string UserId { get; set; } = null!; - public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) => + // public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) { // var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver); - await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(Homeserver, AccessToken, proxy); + // await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(Homeserver, AccessToken, proxy); + // } } public class LoginRequest { diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index cf32df8..36abadc 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -12,6 +12,7 @@ using LibMatrix.EventTypes.Spec.State; using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers; using LibMatrix.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.RoomTypes; @@ -313,7 +314,7 @@ public class GenericRoom { if (useOriginHomeserver) try { var hs = avatar.Url.Split('/', 3)[1]; - return await new HomeserverResolverService().ResolveMediaUri(hs, avatar.Url); + return await new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance).ResolveMediaUri(hs, avatar.Url); } catch (Exception e) { Console.WriteLine(e); diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs index 8e2e15b..3995a26 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -1,4 +1,5 @@ using System.Net.Http.Json; +using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; @@ -6,99 +7,62 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Services; -public class HomeserverProviderService(ILogger<HomeserverProviderService> logger) { - private static readonly Dictionary<string, SemaphoreSlim> AuthenticatedHomeserverSemaphore = new(); - private static readonly Dictionary<string, AuthenticatedHomeserverGeneric> AuthenticatedHomeserverCache = new(); - - private static readonly Dictionary<string, SemaphoreSlim> RemoteHomeserverSemaphore = new(); - private static readonly Dictionary<string, RemoteHomeserver> RemoteHomeserverCache = new(); +public class HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService hsResolver) { + private static SemaphoreCache<AuthenticatedHomeserverGeneric> AuthenticatedHomeserverCache = new(); + private static SemaphoreCache<RemoteHomeserver> RemoteHomeserverCache = new(); public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedWithToken(string homeserver, string accessToken, string? proxy = null, string? impersonatedMxid = null) { - var cacheKey = homeserver + accessToken + proxy + impersonatedMxid; - var sem = AuthenticatedHomeserverSemaphore.GetOrCreate(cacheKey, _ => new SemaphoreSlim(1, 1)); - await sem.WaitAsync(); - AuthenticatedHomeserverGeneric? hs; - lock (AuthenticatedHomeserverCache) { - if (AuthenticatedHomeserverCache.TryGetValue(cacheKey, out hs)) { - sem.Release(); - return hs; - } - } - - var rhs = await RemoteHomeserver.Create(homeserver, proxy); - ClientVersionsResponse clientVersions = new(); - try { - clientVersions = await rhs.GetClientVersionsAsync(); - } - catch (Exception e) { - logger.LogError(e, "Failed to get client versions for {homeserver}", homeserver); - } - - if (proxy is not null) - logger.LogInformation("Homeserver {homeserver} proxied via {proxy}...", homeserver, proxy); - logger.LogInformation("{homeserver}: {clientVersions}", homeserver, clientVersions.ToJson()); + return await AuthenticatedHomeserverCache.GetOrAdd($"{homeserver}{accessToken}{proxy}{impersonatedMxid}", async () => { + var wellKnownUris = await hsResolver.ResolveHomeserverFromWellKnown(homeserver); + var rhs = new RemoteHomeserver(homeserver, wellKnownUris, ref proxy); - ServerVersionResponse serverVersion; - try { - serverVersion = serverVersion = await (rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)!); - } - catch (Exception e) { - logger.LogWarning(e, "Failed to get server version for {homeserver}", homeserver); - sem.Release(); - throw; - } - - try { - if (clientVersions.UnstableFeatures.TryGetValue("gay.rory.mxapiextensions.v0", out var a) && a) - hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverMxApiExtended>(homeserver, accessToken, proxy); - else { - if (serverVersion is { Server.Name: "Synapse" }) - hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverSynapse>(homeserver, accessToken, proxy); - else - hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(homeserver, accessToken, proxy); + ClientVersionsResponse? clientVersions = new(); + try { + clientVersions = await rhs.GetClientVersionsAsync(); + } + catch (Exception e) { + logger.LogError(e, "Failed to get client versions for {homeserver}", homeserver); } - } - catch (Exception e) { - logger.LogError(e, "Failed to create authenticated homeserver for {homeserver}", homeserver); - sem.Release(); - throw; - } - - if (impersonatedMxid is not null) - await hs.SetImpersonate(impersonatedMxid); - - lock (AuthenticatedHomeserverCache) { - AuthenticatedHomeserverCache[cacheKey] = hs; - } - - sem.Release(); - - return hs; - } - public async Task<RemoteHomeserver> GetRemoteHomeserver(string homeserver, string? proxy = null) { - var cacheKey = homeserver + proxy; - var sem = RemoteHomeserverSemaphore.GetOrCreate(cacheKey, _ => new SemaphoreSlim(1, 1)); - await sem.WaitAsync(); - RemoteHomeserver? hs; - lock (RemoteHomeserverCache) { - if (RemoteHomeserverCache.TryGetValue(cacheKey, out hs)) { - sem.Release(); - return hs; + ServerVersionResponse? serverVersion; + try { + serverVersion = await (rhs.FederationClient?.GetServerVersionAsync() ?? Task.FromResult<ServerVersionResponse?>(null)!); + } + catch (Exception e) { + logger.LogWarning(e, "Failed to get server version for {homeserver}", homeserver); + throw; } - } - hs = await RemoteHomeserver.Create(homeserver, proxy); + AuthenticatedHomeserverGeneric hs; + try { + if (clientVersions.UnstableFeatures.TryGetValue("gay.rory.mxapiextensions.v0", out var a) && a) + hs = new AuthenticatedHomeserverMxApiExtended(homeserver, wellKnownUris, ref proxy, accessToken); + else { + if (serverVersion is { Server.Name: "Synapse" }) + hs = new AuthenticatedHomeserverSynapse(homeserver, wellKnownUris, ref proxy, accessToken); + else + hs = new AuthenticatedHomeserverGeneric(homeserver, wellKnownUris, ref proxy, accessToken); + } + } + catch (Exception e) { + logger.LogError(e, "Failed to create authenticated homeserver for {homeserver}", homeserver); + throw; + } - lock (RemoteHomeserverCache) { - RemoteHomeserverCache[cacheKey] = hs; - } + await hs.Initialise(); - sem.Release(); + if (impersonatedMxid is not null) + await hs.SetImpersonate(impersonatedMxid); - return hs; + return hs; + }); } + public async Task<RemoteHomeserver> GetRemoteHomeserver(string homeserver, string? proxy = null) => + await RemoteHomeserverCache.GetOrAdd($"{homeserver}{proxy}", async () => { + return new RemoteHomeserver(homeserver, await hsResolver.ResolveHomeserverFromWellKnown(homeserver), ref proxy); + }); + public async Task<LoginResponse> Login(string homeserver, string user, string password, string? proxy = null) { var hs = await GetRemoteHomeserver(homeserver, proxy); var payload = new LoginRequest { diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index bcef541..42ad0a1 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -1,87 +1,135 @@ using System.Collections.Concurrent; +using System.Diagnostics; +using System.Net.Http.Json; using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace LibMatrix.Services; -public class HomeserverResolverService(ILogger<HomeserverResolverService>? logger = null) { +public class HomeserverResolverService { private readonly MatrixHttpClient _httpClient = new() { Timeout = TimeSpan.FromMilliseconds(10000) }; - private static readonly ConcurrentDictionary<string, WellKnownUris> WellKnownCache = new(); - private static readonly ConcurrentDictionary<string, SemaphoreSlim> WellKnownSemaphores = new(); + private static readonly SemaphoreCache<WellKnownUris> WellKnownCache = new(); - public async Task<WellKnownUris> ResolveHomeserverFromWellKnown(string homeserver) { - if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); - WellKnownSemaphores.TryAdd(homeserver, new SemaphoreSlim(1, 1)); - await WellKnownSemaphores[homeserver].WaitAsync(); - if (WellKnownCache.TryGetValue(homeserver, out var known)) { - WellKnownSemaphores[homeserver].Release(); - return known; + private readonly ILogger<HomeserverResolverService> _logger; + + public HomeserverResolverService(ILogger<HomeserverResolverService> logger) { + _logger = logger; + if (logger is NullLogger<HomeserverResolverService>) { + var stackFrame = new StackTrace(true).GetFrame(1); + Console.WriteLine( + $"WARN | Null logger provided to HomeserverResolverService!\n{stackFrame.GetMethod().DeclaringType} at {stackFrame.GetFileName()}:{stackFrame.GetFileLineNumber()}"); } + } + + private static SemaphoreSlim _wellKnownSemaphore = new(1, 1); + + public async Task<WellKnownUris> ResolveHomeserverFromWellKnown(string homeserver) { + ArgumentNullException.ThrowIfNull(homeserver); - logger?.LogInformation("Resolving homeserver: {}", homeserver); - var res = new WellKnownUris { - Client = await _tryResolveFromClientWellknown(homeserver), - Server = await _tryResolveFromServerWellknown(homeserver) - }; - WellKnownCache.TryAdd(homeserver, res); - WellKnownSemaphores[homeserver].Release(); - return res; + return await WellKnownCache.GetOrAdd(homeserver, async () => { + await _wellKnownSemaphore.WaitAsync(); + _logger.LogTrace($"Resolving homeserver well-knowns: {homeserver}"); + var client = _tryResolveClientEndpoint(homeserver); + + var res = new WellKnownUris(); + + // try { + res.Client = await client ?? throw new Exception("Could not resolve client URL."); + // } + // catch (Exception e) { + // _logger.LogError(e, "Error resolving client well-known for {hs}", homeserver); + // } + + var server = _tryResolveServerEndpoint(homeserver); + + // try { + res.Server = await server ?? throw new Exception("Could not resolve server URL."); + // } + // catch (Exception e) { + // _logger.LogError(e, "Error resolving server well-known for {hs}", homeserver); + // } + + _logger.LogInformation("Resolved well-knowns for {hs}: {json}", homeserver, res.ToJson(indent: false)); + _wellKnownSemaphore.Release(); + return res; + }); } - private async Task<string?> _tryResolveFromClientWellknown(string homeserver) { - if (!homeserver.StartsWith("http")) { - if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/.well-known/matrix/client")) - homeserver = "https://" + homeserver; - else if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/.well-known/matrix/client")) { - homeserver = "http://" + homeserver; - } + private async Task<string?> _tryResolveClientEndpoint(string homeserver) { + ArgumentNullException.ThrowIfNull(homeserver); + _logger.LogTrace("Resolving client well-known: {homeserver}", homeserver); + ClientWellKnown? clientWellKnown = null; + // check if homeserver has a client well-known + if (homeserver.StartsWith("https://")) { + clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); } - - try { - var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client"); - var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString(); - return hs; + else if (homeserver.StartsWith("http://")) { + clientWellKnown = await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); } - catch { - // ignored + else { + clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"https://{homeserver}/.well-known/matrix/client"); + clientWellKnown ??= await _httpClient.TryGetFromJsonAsync<ClientWellKnown>($"http://{homeserver}/.well-known/matrix/client"); + + if (clientWellKnown is null) { + if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/_matrix/client/versions")) + return $"https://{homeserver}"; + if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/_matrix/client/versions")) + return $"http://{homeserver}"; + } } - logger?.LogInformation("No client well-known..."); + if (!string.IsNullOrWhiteSpace(clientWellKnown?.Homeserver.BaseUrl)) + return clientWellKnown.Homeserver.BaseUrl; + + _logger.LogInformation("No client well-known..."); return null; } - private async Task<string?> _tryResolveFromServerWellknown(string homeserver) { - if (!homeserver.StartsWith("http")) { - if (await _httpClient.CheckSuccessStatus($"https://{homeserver}/.well-known/matrix/server")) - homeserver = "https://" + homeserver; - else if (await _httpClient.CheckSuccessStatus($"http://{homeserver}/.well-known/matrix/server")) { - homeserver = "http://" + homeserver; - } + private async Task<string?> _tryResolveServerEndpoint(string homeserver) { + // TODO: implement SRV delegation via DoH: https://developers.google.com/speed/public-dns/docs/doh/json + ArgumentNullException.ThrowIfNull(homeserver); + _logger.LogTrace($"Resolving server well-known: {homeserver}"); + ServerWellKnown? serverWellKnown = null; + // check if homeserver has a server well-known + if (homeserver.StartsWith("https://")) { + serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); } - - try { - var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/server"); - var hs = resp.GetProperty("m.server").GetString(); - if (hs is null) throw new InvalidDataException("m.server is null"); - if (!hs.StartsWithAnyOf("http://", "https://")) - hs = $"https://{hs}"; - return hs; + else if (homeserver.StartsWith("http://")) { + serverWellKnown = await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"{homeserver}/.well-known/matrix/server"); + } + else { + serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"https://{homeserver}/.well-known/matrix/server"); + serverWellKnown ??= await _httpClient.TryGetFromJsonAsync<ServerWellKnown>($"http://{homeserver}/.well-known/matrix/server"); } - catch { - // ignored + + _logger.LogInformation("Server well-known for {hs}: {json}", homeserver, serverWellKnown?.ToJson() ?? "null"); + + if (!string.IsNullOrWhiteSpace(serverWellKnown?.Homeserver)) { + var resolved = serverWellKnown.Homeserver; + if (resolved.StartsWith("https://") || resolved.StartsWith("http://")) + return resolved; + if (await _httpClient.CheckSuccessStatus($"https://{resolved}/_matrix/federation/v1/version")) + return $"https://{resolved}"; + if (await _httpClient.CheckSuccessStatus($"http://{resolved}/_matrix/federation/v1/version")) + return $"http://{resolved}"; + _logger.LogWarning("Server well-known points to invalid server: {resolved}", resolved); } - // fallback: most servers host these on the same location - var clientUrl = await _tryResolveFromClientWellknown(homeserver); + // fallback: most servers host C2S and S2S on the same domain + var clientUrl = await _tryResolveClientEndpoint(homeserver); if (clientUrl is not null && await _httpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version")) return clientUrl; - logger?.LogInformation("No server well-known..."); + _logger.LogInformation("No server well-known..."); return null; } @@ -97,4 +145,19 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge public string? Client { get; set; } public string? Server { get; set; } } + + public class ClientWellKnown { + [JsonPropertyName("m.homeserver")] + public WellKnownHomeserver Homeserver { get; set; } + + public class WellKnownHomeserver { + [JsonPropertyName("base_url")] + public string BaseUrl { get; set; } + } + } + + public class ServerWellKnown { + [JsonPropertyName("m.server")] + public string Homeserver { get; set; } + } } \ No newline at end of file diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs index 0f07b61..06ea9de 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace LibMatrix.Services; @@ -11,7 +12,7 @@ public static class ServiceInstaller { services.AddSingleton(config ?? new RoryLibMatrixConfiguration()); //Add services - services.AddSingleton<HomeserverResolverService>(); + services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>())); // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) { services.AddSingleton<HomeserverProviderService>(); diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 9800c27..26c6a5f 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -24,7 +24,8 @@ public class StateEvent { return dict; }).ToFrozenDictionary(); - public static Type GetStateEventType(string? type) => string.IsNullOrWhiteSpace(type) ? typeof(UnknownEventContent) : KnownStateEventTypesByName.GetValueOrDefault(type) ?? typeof(UnknownEventContent); + public static Type GetStateEventType(string? type) => + string.IsNullOrWhiteSpace(type) ? typeof(UnknownEventContent) : KnownStateEventTypesByName.GetValueOrDefault(type) ?? typeof(UnknownEventContent); [JsonIgnore] public Type MappedType => GetStateEventType(Type); @@ -67,7 +68,9 @@ public class StateEvent { set { if (value is null) RawContent?.Clear(); - else RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value, value.GetType())); + else + RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value, value.GetType(), + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); } } |