diff options
Diffstat (limited to 'LibMatrix')
-rw-r--r-- | LibMatrix/Extensions/MatrixHttpClient.Single.cs | 16 | ||||
-rw-r--r-- | LibMatrix/Helpers/MessageBuilder.cs | 12 | ||||
-rw-r--r-- | LibMatrix/Helpers/SyncHelper.cs | 8 | ||||
-rw-r--r-- | LibMatrix/LibMatrix.csproj | 10 | ||||
-rw-r--r-- | LibMatrix/Responses/DeviceKeysUploadRequest.cs | 24 | ||||
-rw-r--r-- | LibMatrix/Responses/SyncResponse.cs | 2 | ||||
-rw-r--r-- | LibMatrix/Responses/UserDirectoryResponse.cs | 30 | ||||
-rw-r--r-- | LibMatrix/RoomTypes/GenericRoom.cs | 54 | ||||
-rw-r--r-- | LibMatrix/Services/ServiceInstaller.cs | 10 | ||||
-rw-r--r-- | LibMatrix/StateEvent.cs | 39 |
10 files changed, 162 insertions, 43 deletions
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs index 39eb7e5..4145a16 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs @@ -2,6 +2,7 @@ // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Http.Headers; using System.Reflection; using System.Security.Cryptography.X509Certificates; @@ -73,12 +74,15 @@ public class MatrixHttpClient { await _rateLimitSemaphore.WaitAsync(cancellationToken); #endif - Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.GetContentLength())})"); if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); - foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value); + foreach (var (key, value) in DefaultRequestHeaders) { + if (request.Headers.Contains(key)) continue; + request.Headers.Add(key, value); + } request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); @@ -106,7 +110,13 @@ public class MatrixHttpClient { public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { var responseMessage = await SendUnhandledAsync(request, cancellationToken); if (responseMessage.IsSuccessStatusCode) return responseMessage; - + + //retry on gateway timeout + if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); + } + //error handling var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); if (content.Length == 0) diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs index d897078..b639e1f 100644 --- a/LibMatrix/Helpers/MessageBuilder.cs +++ b/LibMatrix/Helpers/MessageBuilder.cs @@ -91,6 +91,18 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr return this; } + public MessageBuilder WithMention(string id, string? displayName = null) { + Content.Body += $"@{displayName ?? id}"; + Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; + return this; + } + + public MessageBuilder WithNewline() { + Content.Body += "\n"; + Content.FormattedBody += "<br>"; + return this; + } + public MessageBuilder WithTable(Action<TableBuilder> tableBuilder) { var tb = new TableBuilder(this); this.WithHtmlTag("table", msb => tableBuilder(tb)); diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs index 1833bd0..c9ca85d 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -4,6 +4,7 @@ using ArcaneLibs.Extensions; using LibMatrix.Filters; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Utilities; using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; @@ -42,6 +43,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg _filter = value; _filterIsDirty = true; _filterId = null; + _namedFilterName = null; } } @@ -81,16 +83,16 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}"; if (_filterId is not null) url += $"&filter={_filterId}"; - logger?.LogInformation("SyncHelper: Calling: {}", url); + // logger?.LogInformation("SyncHelper: Calling: {}", url); try { var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None); if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request"); - logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.Content.Headers.ContentLength ?? -1, sw.Elapsed); + logger?.LogTrace("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed); var deserializeSw = Stopwatch.StartNew(); var resp = await httpResp.Content.ReadFromJsonAsync<SyncResponse>(cancellationToken: cancellationToken ?? CancellationToken.None, jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse); - logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.Content.Headers.ContentLength ?? -1, deserializeSw.Elapsed, sw.Elapsed); + logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed); var timeToWait = MinimumDelay.Subtract(sw.Elapsed); if (timeToWait.TotalMilliseconds > 0) await Task.Delay(timeToWait); diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index e037672..f4a20bc 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>preview</LangVersion> @@ -12,8 +12,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> </ItemGroup> <ItemGroup> @@ -23,12 +23,14 @@ Using the NuGet version in development is annoying due to delays between pushing and being able to consume. If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. --> - <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/> + <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview.202*"/> <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/> </ItemGroup> + <!-- <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild"> <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/> </Target> + --> </Project> diff --git a/LibMatrix/Responses/DeviceKeysUploadRequest.cs b/LibMatrix/Responses/DeviceKeysUploadRequest.cs new file mode 100644 index 0000000..c93c4c6 --- /dev/null +++ b/LibMatrix/Responses/DeviceKeysUploadRequest.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class DeviceKeysUploadRequest { + [JsonPropertyName("device_keys")] + public DeviceKeysSchema DeviceKeys { get; set; } + + + [JsonPropertyName("one_time_keys")] + public Dictionary<string, OneTimeKey> OneTimeKeys { get; set; } + + public class DeviceKeysSchema { + [JsonPropertyName("algorithms")] + public List<string> Algorithms { get; set; } + } + public class OneTimeKey { + [JsonPropertyName("key")] + public string Key { get; set; } + + [JsonPropertyName("signatures")] + public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs index e4addb6..b2308c5 100644 --- a/LibMatrix/Responses/SyncResponse.cs +++ b/LibMatrix/Responses/SyncResponse.cs @@ -39,7 +39,7 @@ public class SyncResponse { // supporting classes public class PresenceDataStructure { [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } = new(); + public List<StateEventResponse>? Events { get; set; } } public class RoomsDataStructure { diff --git a/LibMatrix/Responses/UserDirectoryResponse.cs b/LibMatrix/Responses/UserDirectoryResponse.cs new file mode 100644 index 0000000..13235d9 --- /dev/null +++ b/LibMatrix/Responses/UserDirectoryResponse.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class UserDirectoryResponse { + [JsonPropertyName("limited")] + public bool Limited { get; set; } + + [JsonPropertyName("results")] + public List<UserDirectoryResult> Results { get; set; } + + public class UserDirectoryResult { + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("display_name")] + public string? DisplayName { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + } +} + +public class UserDirectoryRequest { + [JsonPropertyName("search_term")] + public string SearchTerm { get; set; } + + [JsonPropertyName("limit")] + public int? Limit { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index b9d98bd..8398ab9 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -106,7 +106,7 @@ public class GenericRoom { Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync"); var sh = new SyncHelper(Homeserver); var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]); - var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]); + var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []); sh.Filter = new() { Presence = emptyFilter, AccountData = emptyFilter, @@ -121,10 +121,11 @@ public class GenericRoom { var sync = await sh.SyncAsync(); var state = sync.Rooms.Join[RoomId].State.Events; var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey); - if (stateEvent is null) throw new LibMatrixException() { - ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, - Error = "State event not found in sync response" - }; + if (stateEvent is null) + throw new LibMatrixException() { + ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND, + Error = "State event not found in sync response" + }; return stateEvent.EventId; } @@ -232,7 +233,7 @@ public class GenericRoom { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -240,7 +241,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); foreach (var resp in result.Chunk) { if (resp?.Type != "m.room.member") continue; @@ -249,14 +250,14 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); } public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(bool joinedOnly = true) { // var sw = Stopwatch.StartNew(); var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); // if (sw.ElapsedMilliseconds > 1000) - // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); // var resText = await res.Content.ReadAsStringAsync(); // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}"); @@ -264,7 +265,7 @@ public class GenericRoom { TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default }); // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}"); // else sw.Restart(); var members = new List<StateEventResponse>(); foreach (var resp in result.Chunk) { @@ -274,7 +275,7 @@ public class GenericRoom { } // if (sw.ElapsedMilliseconds > 100) - // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); + // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}"); return members.ToFrozenSet(); } @@ -320,7 +321,9 @@ public class GenericRoom { [Obsolete("This method will be merged into GetNameAsync() in the future.")] public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) { try { - return await GetNameAsync(); + var name = await GetNameAsync(); + if (!string.IsNullOrEmpty(name)) return name; + throw new(); } catch { try { @@ -376,7 +379,7 @@ public class GenericRoom { public async Task UnbanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", - new UserIdAndReason { UserId = userId, Reason = reason}); + new UserIdAndReason { UserId = userId, Reason = reason }); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null) @@ -428,6 +431,16 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<T>(); } + + public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) { + try { + return await GetRoomAccountDataAsync<T>(key); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_NOT_FOUND") return default; + throw; + } + } public async Task SetRoomAccountDataAsync(string key, object data) { var res = await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data); @@ -440,10 +453,17 @@ public class GenericRoom { public Task<StateEventResponse> GetEventAsync(string eventId) => Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}"); - public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) { + public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) { var data = new { reason }; - return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync( - $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + var url = $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid().ToString()}"; + while (true) { + try { + return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(url, data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + } catch (MatrixException e) { + if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw; + throw; + } + } } #endregion @@ -510,7 +530,7 @@ public class GenericRoom { var uri = new Uri(path, UriKind.Relative); if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir); - else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); + else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir)); if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from); if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString()); if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString()); diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs index 06ea9de..8b7e54b 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -5,23 +5,13 @@ namespace LibMatrix.Services; public static class ServiceInstaller { public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) { - //Check required services - // if (!services.Any(x => x.ServiceType == typeof(TieredStorageService))) - // throw new Exception("[RMUCore/DI] No TieredStorageService has been registered!"); //Add config services.AddSingleton(config ?? new RoryLibMatrixConfiguration()); //Add services services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>())); - - // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) { services.AddSingleton<HomeserverProviderService>(); - // } - // else { - // services.AddScoped<HomeserverProviderService>(); - // } - // services.AddScoped<MatrixHttpClient>(); return services; } } diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 073d26d..cc870e4 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -44,6 +44,7 @@ public class StateEvent { public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type; private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() { + // We need these, NumberHandling covers other number types that we don't want to convert Converters = { new JsonFloatStringConverter(), new JsonDoubleStringConverter(), @@ -55,9 +56,6 @@ public class StateEvent { [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public EventContent? TypedContent { get { - // if (Type == "m.receipt") { - // return null; - // } try { var mappedType = GetStateEventType(Type); if (mappedType == typeof(UnknownEventContent)) @@ -81,6 +79,18 @@ public class StateEvent { } } + public T? ContentAs<T>() { + try { + return RawContent.Deserialize<T>(TypedContentSerializerOptions)!; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null")); + } + + return default; + } + [JsonPropertyName("state_key")] public string? StateKey { get; set; } @@ -156,7 +166,7 @@ public class StateEventResponse : StateEvent { public string? Sender { get; set; } [JsonPropertyName("unsigned")] - public UnsignedData? Unsigned { get; set; } + public JsonObject? Unsigned { get; set; } [JsonPropertyName("event_id")] public string? EventId { get; set; } @@ -254,4 +264,23 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR } */ -#endregion \ No newline at end of file +#endregion + +/* +public class ForgivingObjectConverter<T> : JsonConverter<T> where T : new() { + public override T? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { + try { + var text = JsonDocument.ParseValue(ref reader).RootElement.GetRawText(); + return JsonSerializer.Deserialize<T>(text, options); + } + catch (JsonException ex) { + Console.WriteLine(ex); + return null; + } + } + + public override bool CanConvert(Type typeToConvert) => true; + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => JsonSerializer.Serialize<T>(writer, value, options); +}*/ \ No newline at end of file |