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="
}
]
|