about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs10
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs2
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs48
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs35
-rw-r--r--LibMatrix/LibMatrix.csproj6
-rw-r--r--LibMatrix/LibMatrixException.cs7
-rw-r--r--LibMatrix/Responses/Federation/SignedObject.cs3
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs16
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs1
-rw-r--r--LibMatrix/Utilities/CommonSyncFilters.cs10
-rw-r--r--LibMatrix/deps.json13
11 files changed, 110 insertions, 41 deletions
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs

index cd82071..ae18b2d 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -1,5 +1,6 @@ #define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; @@ -70,7 +71,7 @@ public class MatrixHttpClient { public int MaxRetryIntervalMs { get; set; } = DefaultMaxRetryIntervalMs; public int MaxRetries { get; set; } = DefaultMaxRetries; - private Dictionary<HttpRequestMessage, int> _retries = []; + private readonly ConcurrentDictionary<HttpRequestMessage, int> _retries = []; // default headers, not bound to client public HttpRequestHeaders DefaultRequestHeaders { get; set; } = @@ -224,7 +225,12 @@ public class MatrixHttpClient { } if (responseMessage.IsSuccessStatusCode) { - _retries.Remove(request); + while (!_retries.TryRemove(request, out _)) { + Console.WriteLine("[MatrixHttpClient] Race - failed to remove retries entry, retrying..."); + // ReSharper disable once MethodSupportsCancellation - this shouldn't be cancellable as it would be a memory leak + await Task.Delay(5); // hopefully helps resolve contention? + } + return responseMessage; } diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index f753bf7..c6f8ea1 100644 --- a/LibMatrix/Helpers/MessageBuilder.cs +++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -96,7 +96,7 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr } public MessageBuilder WithMention(string id, string? displayName = null, string[]? vias = null, bool useIdInPlainText = false, bool useLinkInPlainText = false) { - if (!useLinkInPlainText) Content.Body += $"@{(useIdInPlainText ? id : displayName ?? id)}"; + if (!useLinkInPlainText) Content.Body += $"{(useIdInPlainText ? id : displayName ?? id)}"; else { Content.Body += $"https://matrix.to/#/{id}"; if (vias is { Length: > 0 }) Content.Body += $"?via={string.Join("&via=", vias)}"; diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index 1e33bb5..ed47eb2 100644 --- a/LibMatrix/Helpers/RoomBuilder.cs +++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -207,7 +207,7 @@ public class RoomBuilder { private async Task SetStatesAsync(GenericRoom room, List<MatrixEvent> state) { if (state.Count == 0) return; Console.WriteLine($"Setting {state.Count} state events for {room.RoomId}..."); - await room.BulkSendEventsAsync(state); + // await room.BulkSendEventsAsync(state); // We chunk this up to try to avoid hitting reverse proxy timeouts // foreach (var group in state.Chunk(chunkSize)) { // var sw = Stopwatch.StartNew(); @@ -217,27 +217,31 @@ public class RoomBuilder { // Console.WriteLine($"Warning: Sending {group.Length} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}."); // } // } - // int chunkSize = 50; - // for (int i = 0; i < state.Count; i += chunkSize) { - // var chunk = state.Skip(i).Take(chunkSize).ToList(); - // if (chunk.Count == 0) continue; - // - // var sw = Stopwatch.StartNew(); - // await room.BulkSendEventsAsync(chunk, forceSyncInterval: chunk.Count + 1); - // Console.WriteLine($"Sent {chunk.Count} state events in {sw.ElapsedMilliseconds}ms. {state.Count - (i + chunk.Count)} remaining."); - // // if (sw.ElapsedMilliseconds > 45000) { - // // chunkSize = Math.Max(chunkSize / 3, 1); - // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is dangerously long. Reducing chunk size to {chunkSize}."); - // // } - // // else if (sw.ElapsedMilliseconds > 30000) { - // // chunkSize = Math.Max(chunkSize / 2, 1); - // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}."); - // // } - // // else if (sw.ElapsedMilliseconds < 10000) { - // // chunkSize = Math.Min((int)(chunkSize * 1.2), 1000); - // // Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}."); - // // } - // } + int chunkSize = 767; + for (int i = 0; i < state.Count; i += chunkSize) { + var chunk = state.Skip(i).Take(chunkSize).ToList(); + if (chunk.Count == 0) continue; + + var sw = Stopwatch.StartNew(); + await room.BulkSendEventsAsync(chunk, forceSyncInterval: chunk.Count + 1); + Console.WriteLine($"Sent {chunk.Count} state events in {sw.ElapsedMilliseconds}ms. {state.Count - (i + chunk.Count)} remaining."); + if (sw.ElapsedMilliseconds > 50000) { + chunkSize = Math.Max((int)(chunkSize / 1.2), 1); + Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is dangerously long. Reducing chunk size to {chunkSize}."); + } + // else if (sw.ElapsedMilliseconds > 30000) { + // chunkSize = Math.Max(chunkSize / 2, 1); + // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}."); + // } + else if (sw.ElapsedMilliseconds < 5000) { + chunkSize = Math.Min((int)(chunkSize * 1.5), 1000); + Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}."); + } + else if (sw.ElapsedMilliseconds < 10000) { + chunkSize = Math.Min((int)(chunkSize * 1.2), 1000); + Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}."); + } + } } private async Task SetBasicRoomInfoAsync(GenericRoom room) { diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index b453d87..b1e655d 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -186,6 +186,34 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { } } + public virtual async Task<T> GetRoomAccountDataAsync<T>(string roomId, string key) => + // var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}"); + // if (!res.IsSuccessStatusCode) { + // Console.WriteLine($"Failed to get account data: {await res.Content.ReadAsStringAsync()}"); + // throw new InvalidDataException($"Failed to get account data: {await res.Content.ReadAsStringAsync()}"); + // } + // + // return await res.Content.ReadFromJsonAsync<T>(); + await ClientHttpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{WhoAmI.UserId}/rooms/{HttpUtility.UrlEncode(roomId)}/account_data/{key}"); + + public virtual async Task<T?> GetRoomAccountDataOrNullAsync<T>(string roomId, string key) { + try { + return await GetRoomAccountDataAsync<T>(roomId, key); + } + catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return default; + throw; + } + } + + public virtual async Task SetRoomAccountDataAsync(string roomId, string key, object data) { + var res = await ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{WhoAmI.UserId}/rooms/{HttpUtility.UrlEncode(roomId)}/account_data/{key}", data); + if (!res.IsSuccessStatusCode) { + Console.WriteLine($"Failed to set account data: {await res.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to set account data: {await res.Content.ReadAsStringAsync()}"); + } + } + #endregion #region MSC 4133 @@ -378,11 +406,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { /// <b>Warning</b>: This uses /sync! /// </summary> /// <param name="includeGlobal">Include non-room account data</param> + /// <param name="rooms">Scope sync request to given room ID(s)</param> /// <returns>Dictionary of room IDs and their account data.</returns> /// <exception cref="Exception"></exception> - public async Task<Dictionary<string, EventList?>> EnumerateAccountDataPerRoom(bool includeGlobal = false) { + public async Task<Dictionary<string, EventList?>> EnumerateAccountDataPerRoom(bool includeGlobal = false, List<string>? rooms = null) { var syncHelper = new SyncHelper(this); - syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountDataWithRooms); + syncHelper.FilterId = rooms == null + ? await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountDataWithRooms) + : (await UploadFilterAsync(CommonSyncFilters.GetAccountDataForRoomsFilter(rooms))).FilterId; var resp = await syncHelper.SyncAsync(); if (resp is null) throw new Exception("Sync failed"); var perRoomAccountData = new Dictionary<string, EventList?>(); diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index f9e5ce3..2179817 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj
@@ -19,14 +19,14 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0"/> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" /> <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/> </ItemGroup> <ItemGroup> <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.2025*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.2026*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> </ItemGroup> </Project> diff --git a/LibMatrix/LibMatrixException.cs b/LibMatrix/LibMatrixException.cs
index 27cfc2a..e066d6c 100644 --- a/LibMatrix/LibMatrixException.cs +++ b/LibMatrix/LibMatrixException.cs
@@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; + // ReSharper disable MemberCanBePrivate.Global namespace LibMatrix; @@ -12,13 +13,15 @@ public class LibMatrixException : Exception { [JsonPropertyName("error")] public required string Error { get; set; } - public object GetAsObject() => new { errcode = ErrorCode, error = Error }; public string GetAsJson() => GetAsObject().ToJson(ignoreNull: true); public override string Message => $"{ErrorCode}: {ErrorCode switch { + "M_NOT_FOUND" => "The specified entity could not be found", "M_UNSUPPORTED" => "The requested feature is not supported", + "RLM_NO_CLIENT_URL" => "Could not resolve client URL", + "RLM_NO_SERVER_URL" => "Could not resolve server URL", _ => $"Unknown error: {GetAsObject().ToJson(ignoreNull: true)}" }}\nError: {Error}"; @@ -26,5 +29,7 @@ public class LibMatrixException : Exception { public static class ErrorCodes { public const string M_NOT_FOUND = "M_NOT_FOUND"; public const string M_UNSUPPORTED = "M_UNSUPPORTED"; + public const string RLM_NO_CLIENT_URL = "RLM_NO_CLIENT_URL"; + public const string RLM_NO_SERVER_URL = "RLM_NO_SERVER_URL"; } } \ No newline at end of file diff --git a/LibMatrix/Responses/Federation/SignedObject.cs b/LibMatrix/Responses/Federation/SignedObject.cs
index 3f6ffd6..517bb1f 100644 --- a/LibMatrix/Responses/Federation/SignedObject.cs +++ b/LibMatrix/Responses/Federation/SignedObject.cs
@@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -19,7 +20,7 @@ public class SignedObject<T> { } [JsonExtensionData] - public required JsonObject Content { get; set; } + public JsonObject Content { get; set; } = null!; [JsonIgnore] public T TypedContent { diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index ed1d2e3..700cfbb 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -37,16 +37,22 @@ public class HomeserverResolverService { var res = new WellKnownUris(); if (client != null) - res.Client = (await client)?.TrimEnd('/') ?? throw new Exception($"Could not resolve client URL for {homeserver}."); + res.Client = (await client)?.TrimEnd('/') ?? throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.RLM_NO_CLIENT_URL, + Error = $"Could not resolve client URL for {homeserver}." + }; if (server != null) - res.Server = (await server)?.TrimEnd('/') ?? throw new Exception($"Could not resolve server URL for {homeserver}."); + res.Server = (await server)?.TrimEnd('/') ?? throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.RLM_NO_SERVER_URL, + Error = $"Could not resolve server URL for {homeserver}." + }; _logger.LogInformation("Resolved well-knowns for {hs}: {json}", homeserver, res.ToJson(indent: false)); return res; }); } - + private async Task<T?> GetFromJsonAsync<T>(string url) { try { return await _httpClient.GetFromJsonAsync<T>(url); @@ -56,7 +62,7 @@ public class HomeserverResolverService { return default; } } - + private async Task<string?> _tryResolveClientEndpoint(string homeserver) { ArgumentNullException.ThrowIfNull(homeserver); _logger.LogTrace("Resolving client well-known: {homeserver}", homeserver); @@ -71,7 +77,7 @@ public class HomeserverResolverService { } else if (homeserver.StartsWith("http://")) { clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client"); - + if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) return homeserver; } diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
index 5ffd43a..7f15cd2 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -13,6 +13,7 @@ public static class ServiceInstaller { services.AddSingleton<ClientWellKnownResolver>(); services.AddSingleton<ServerWellKnownResolver>(); services.AddSingleton<SupportWellKnownResolver>(); + services.AddSingleton<PolicyServerWellKnownResolver>(); if (!services.Any(x => x.ServiceType == typeof(WellKnownResolverConfiguration))) services.AddSingleton<WellKnownResolverConfiguration>(); services.AddSingleton<WellKnownResolverService>(); diff --git a/LibMatrix/Utilities/CommonSyncFilters.cs b/LibMatrix/Utilities/CommonSyncFilters.cs
index 503cc1f..ec52e72 100644 --- a/LibMatrix/Utilities/CommonSyncFilters.cs +++ b/LibMatrix/Utilities/CommonSyncFilters.cs
@@ -91,4 +91,14 @@ public static class CommonSyncFilters { [GetSpaceRelations] = GetSpaceRelationsFilter, [GetOwnMemberEvents] = GetOwnMemberEventsFilter }.ToFrozenDictionary(); + + public static SyncFilter GetAccountDataForRoomsFilter(List<string> rooms) => new() { + Presence = new SyncFilter.EventFilter(notTypes: ["*"]), + Room = new SyncFilter.RoomFilter() { + State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Timeline = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + AccountData = new SyncFilter.RoomFilter.StateFilter(rooms: rooms) + } + }; } \ No newline at end of file diff --git a/LibMatrix/deps.json b/LibMatrix/deps.json
index ce0dfbf..6989072 100644 --- a/LibMatrix/deps.json +++ b/LibMatrix/deps.json
@@ -1,12 +1,17 @@ [ { + "pname": "ArcaneLibs", + "version": "1.0.1-preview.20260619-035441", + "hash": "sha256-jNEGd8Ccgkk4A8ZMaJO4QDExmsf9q7TeuhiNESos3Q4=" + }, + { "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", - "version": "10.0.0", - "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps=" + "version": "10.0.9", + "hash": "sha256-YzQpGAsrjLU18s016LmM7DMJKml0MzKl1bPPYJ/erEk=" }, { "pname": "Microsoft.Extensions.Logging.Abstractions", - "version": "10.0.0", - "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk=" + "version": "10.0.9", + "hash": "sha256-su2q/OZuG7nB3wnUTVcimy3oHSdetFh4hhmjI3f/ym8=" } ]