diff options
author | Emma [it/its]@Rory& <root@rory.gay> | 2023-11-23 05:42:33 +0100 |
---|---|---|
committer | Emma [it/its]@Rory& <root@rory.gay> | 2023-11-23 05:42:33 +0100 |
commit | 3e934eee892f69a8f78b94950993000522702769 (patch) | |
tree | 6aa0d3d26c9a07a7a3e097fe28abb785400bfbd6 /LibMatrix | |
parent | Add license retroactively, matching where the code originated from (MatrixRoo... (diff) | |
download | LibMatrix-3e934eee892f69a8f78b94950993000522702769.tar.xz |
Moderation bot work
Diffstat (limited to '')
-rw-r--r-- | LibMatrix.sln | 25 | ||||
-rw-r--r-- | LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs | 18 | ||||
-rw-r--r-- | LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs | 19 | ||||
-rw-r--r-- | LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs | 23 | ||||
-rw-r--r-- | LibMatrix/Extensions/HttpClientExtensions.cs | 9 | ||||
-rw-r--r-- | LibMatrix/Helpers/MessageFormatter.cs | 5 | ||||
-rw-r--r-- | LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs | 20 | ||||
-rw-r--r-- | LibMatrix/Interfaces/EventContent.cs | 33 | ||||
-rw-r--r-- | LibMatrix/LibMatrix.csproj | 4 | ||||
-rw-r--r-- | LibMatrix/Responses/CreateRoomRequest.cs | 41 | ||||
-rw-r--r-- | LibMatrix/RoomTypes/GenericRoom.cs | 88 | ||||
-rw-r--r-- | LibMatrix/Services/HomeserverResolverService.cs | 14 |
12 files changed, 252 insertions, 47 deletions
diff --git a/LibMatrix.sln b/LibMatrix.sln index fd241b3..87398f3 100644 --- a/LibMatrix.sln +++ b/LibMatrix.sln @@ -13,12 +13,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{1B1B2197-61FB-416F-B6C8-845F2E5A0442}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaModeratorPoC", "ExampleBots\MediaModeratorPoC\MediaModeratorPoC.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "ExampleBots\ModerationBot\ModerationBot.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFE16D8E-EFC5-49F6-9854-DB001309B3B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{345934FF-CA81-4A4B-B137-9F198102C65F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDataGenerator", "Tests\TestDataGenerator\TestDataGenerator.csproj", "{0B9B34D1-9362-45A9-9C21-816FD6959110}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,11 +60,26 @@ Global {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.Build.0 = Release|Any CPU + {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.Build.0 = Release|Any CPU + {345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.Build.0 = Release|Any CPU + {0B9B34D1-9362-45A9-9C21-816FD6959110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B9B34D1-9362-45A9-9C21-816FD6959110}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B9B34D1-9362-45A9-9C21-816FD6959110}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B9B34D1-9362-45A9-9C21-816FD6959110}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D} {8F0A820E-F6AE-45A2-970E-7A3759693919} = {840309F0-435B-43A7-8471-8C2BE643889D} {35DF9A1A-D988-4225-AFA3-06BB8EDEB559} = {A6345ECE-4C5E-400F-9130-886E343BF314} {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7} = {A6345ECE-4C5E-400F-9130-886E343BF314} + {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B} = {840309F0-435B-43A7-8471-8C2BE643889D} + {345934FF-CA81-4A4B-B137-9F198102C65F} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} + {0B9B34D1-9362-45A9-9C21-816FD6959110} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4} EndGlobalSection EndGlobal diff --git a/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs index 3e4a5cd..558e4fc 100644 --- a/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs +++ b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs @@ -4,17 +4,17 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.presence")] -public class PresenceEventContent : TimelineEventContent { - [JsonPropertyName("presence")] - public string Presence { get; set; } +public class PresenceEventContent : EventContent { + [JsonPropertyName("presence"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Presence { get; set; } [JsonPropertyName("last_active_ago")] public long LastActiveAgo { get; set; } [JsonPropertyName("currently_active")] public bool CurrentlyActive { get; set; } - [JsonPropertyName("status_msg")] - public string StatusMessage { get; set; } - [JsonPropertyName("avatar_url")] - public string AvatarUrl { get; set; } - [JsonPropertyName("displayname")] - public string DisplayName { get; set; } + [JsonPropertyName("status_msg"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? StatusMessage { get; set; } + [JsonPropertyName("avatar_url"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AvatarUrl { get; set; } + [JsonPropertyName("displayname"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? DisplayName { get; set; } } diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs index 1938b3e..8a22489 100644 --- a/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs +++ b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs @@ -17,16 +17,29 @@ public class RoomMessageEventContent : TimelineEventContent { public string MessageType { get; set; } = "m.notice"; [JsonPropertyName("formatted_body")] - public string FormattedBody { get; set; } + public string? FormattedBody { get; set; } [JsonPropertyName("format")] - public string Format { get; set; } + public string? Format { get; set; } /// <summary> /// Media URI for this message, if any /// </summary> [JsonPropertyName("url")] public string? Url { get; set; } - + public string? FileName { get; set; } + + [JsonPropertyName("info")] + public FileInfoStruct? FileInfo { get; set; } + + public class FileInfoStruct { + [JsonPropertyName("mimetype")] + public string? MimeType { get; set; } + [JsonPropertyName("size")] + public long Size { get; set; } + [JsonPropertyName("thumbnail_url")] + public string? ThumbnailUrl { get; set; } + } + } diff --git a/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs index 63f148c..757a9e9 100644 --- a/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs +++ b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs @@ -3,10 +3,23 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; -[MatrixEvent(EventName = "m.policy.rule.user")] -[MatrixEvent(EventName = "m.policy.rule.server")] -[MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")] -public class PolicyRuleEventContent : TimelineEventContent { +//spec +[MatrixEvent(EventName = "m.policy.rule.server")] //spec +[MatrixEvent(EventName = "m.room.rule.server")] //??? +[MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")] //legacy +public class ServerPolicyRuleEventContent : PolicyRuleEventContent { } + +[MatrixEvent(EventName = "m.policy.rule.user")] //spec +[MatrixEvent(EventName = "m.room.rule.user")] //??? +[MatrixEvent(EventName = "org.matrix.mjolnir.rule.user")] //legacy +public class UserPolicyRuleEventContent : PolicyRuleEventContent { } + +[MatrixEvent(EventName = "m.policy.rule.room")] //spec +[MatrixEvent(EventName = "m.room.rule.room")] //??? +[MatrixEvent(EventName = "org.matrix.mjolnir.rule.room")] //legacy +public class RoomPolicyRuleEventContent : PolicyRuleEventContent { } + +public abstract class PolicyRuleEventContent : EventContent { /// <summary> /// Entity this ban applies to, can use * and ? as globs. /// </summary> @@ -52,4 +65,4 @@ public static class PolicyRecommendationTypes { /// Mute this user /// </summary> public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending -} +} \ No newline at end of file diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index 913864e..6f27f71 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -36,12 +36,11 @@ public class MatrixHttpClient : HttpClient { return options; } - public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { + public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId); - // Console.WriteLine($"Sending request to {request.RequestUri}"); + Console.WriteLine($"Sending request to {request.RequestUri}"); try { var webAssemblyEnableStreamingResponseKey = @@ -60,7 +59,7 @@ public class MatrixHttpClient : HttpClient { catch (Exception e) { typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance) ?.SetValue(request, 0); - await Task.Delay(2500); + await Task.Delay(2500, cancellationToken); return await SendAsync(request, cancellationToken); } @@ -123,7 +122,7 @@ public class MatrixHttpClient : HttpClient { var request = new HttpRequestMessage(HttpMethod.Put, requestUri); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); Console.WriteLine($"Sending PUT {requestUri}"); - Console.WriteLine($"Content: {value.ToJson()}"); + Console.WriteLine($"Content: {JsonSerializer.Serialize(value, value.GetType(), options)}"); Console.WriteLine($"Type: {value.GetType().FullName}"); request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), Encoding.UTF8, "application/json"); diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs index d252e85..3ebc9a2 100644 --- a/LibMatrix/Helpers/MessageFormatter.cs +++ b/LibMatrix/Helpers/MessageFormatter.cs @@ -35,6 +35,11 @@ public static class MessageFormatter { public static string HtmlFormatMention(string id, string? displayName = null) { return $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; } + + public static string HtmlFormatMessageLink(string roomId, string eventId, string[]? servers = null, string? displayName = null) { + if (servers is not { Length: > 0 }) servers = new[] { roomId.Split(':', 2)[1] }; + return $"<a href=\"https://matrix.to/#/{roomId}/{eventId}?via={string.Join("&via=", servers)}\">{displayName ?? eventId}</a>"; + } #region Extension functions diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index e4b7483..288608d 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -3,6 +3,7 @@ using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Web; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Filters; @@ -18,7 +19,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke var instance = Activator.CreateInstance(typeof(T), serverName, accessToken) as T ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}"); HomeserverResolverService.WellKnownUris? urls = null; - if(proxy is null) + if (proxy is null) urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(serverName); instance.ClientHttpClient = new() { @@ -78,7 +79,11 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke } public virtual async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") { - var res = await ClientHttpClient.PostAsync($"/_matrix/media/v3/upload?filename={fileName}", new StreamContent(fileStream)); + var req = new HttpRequestMessage(HttpMethod.Post, $"/_matrix/media/v3/upload?filename={fileName}"); + req.Content = new StreamContent(fileStream); + req.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + var res = await ClientHttpClient.SendAsync(req); + if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); throw new InvalidDataException($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); @@ -283,6 +288,17 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke } } + public async Task<RoomIdResponse> JoinRoomAsync(string roomId, List<string> homeservers = null, string? reason = null) { + var join_url = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(roomId)}"; + Console.WriteLine($"Calling {join_url} with {homeservers?.Count ?? 0} via's..."); + if (homeservers == null || homeservers.Count == 0) homeservers = new() { roomId.Split(':')[1] }; + var fullJoinUrl = $"{join_url}?server_name=" + string.Join("&server_name=", homeservers); + var res = await ClientHttpClient.PostAsJsonAsync(fullJoinUrl, new { + reason + }); + return await res.Content.ReadFromJsonAsync<RoomIdResponse>() ?? throw new Exception("Failed to join room?"); + } + #region Room Profile Utility private async Task<KeyValuePair<string, RoomMemberEventContent>> GetOwnRoomProfileWithIdAsync(GenericRoom room) { diff --git a/LibMatrix/Interfaces/EventContent.cs b/LibMatrix/Interfaces/EventContent.cs index ec09c7e..1fb6974 100644 --- a/LibMatrix/Interfaces/EventContent.cs +++ b/LibMatrix/Interfaces/EventContent.cs @@ -1,21 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace LibMatrix.Interfaces; -public abstract class EventContent { - -} +public abstract class EventContent { } + public abstract class TimelineEventContent : EventContent { [JsonPropertyName("m.relates_to")] public MessageRelatesTo? RelatesTo { get; set; } - // [JsonPropertyName("m.new_content")] - // public TimelineEventContent? NewContent { get; set; } + [JsonPropertyName("m.new_content")] + public JsonObject? NewContent { get; set; } + + public TimelineEventContent SetReplaceRelation(string eventId) { + NewContent = JsonSerializer.SerializeToNode(this, GetType()).AsObject(); + // NewContent = JsonSerializer.Deserialize(jsonText, GetType()); + RelatesTo = new() { + RelationType = "m.replace", + EventId = eventId + }; + return this; + } + + public T SetReplaceRelation<T>(string eventId) where T : TimelineEventContent { + return SetReplaceRelation(eventId) as T ?? throw new InvalidOperationException(); + } public class MessageRelatesTo { [JsonPropertyName("m.in_reply_to")] public EventInReplyTo? InReplyTo { get; set; } + [JsonPropertyName("event_id")] + public string? EventId { get; set; } + + [JsonPropertyName("rel_type")] + public string? RelationType { get; set; } + public class EventInReplyTo { [JsonPropertyName("event_id")] public string EventId { get; set; } @@ -24,4 +45,4 @@ public abstract class TimelineEventContent : EventContent { public string RelType { get; set; } } } -} +} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index bfb3069..690556f 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -11,8 +11,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1"/> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> </ItemGroup> <ItemGroup> diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs index 4267094..85db517 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs @@ -81,9 +81,48 @@ public class CreateRoomRequest { return errors; } + public static CreateRoomRequest CreatePublic(AuthenticatedHomeserverGeneric hs, string? name = null, string? roomAliasName = null) { + var request = new CreateRoomRequest { + Name = name ?? "New public Room", + Visibility = "public", + CreationContent = new(), + PowerLevelContentOverride = new() { + EventsDefault = 0, + UsersDefault = 0, + Kick = 50, + Ban = 50, + Invite = 25, + StateDefault = 10, + Redact = 50, + NotificationsPl = new() { + Room = 10 + }, + Events = new() { + { "m.room.avatar", 50 }, + { "m.room.canonical_alias", 50 }, + { "m.room.encryption", 100 }, + { "m.room.history_visibility", 100 }, + { "m.room.name", 50 }, + { "m.room.power_levels", 100 }, + { "m.room.server_acl", 100 }, + { "m.room.tombstone", 100 } + }, + Users = new() { + { + hs.UserId, + 101 + } + } + }, + RoomAliasName = roomAliasName, + InitialState = new() + }; + + return request; + } public static CreateRoomRequest CreatePrivate(AuthenticatedHomeserverGeneric hs, string? name = null, string? roomAliasName = null) { var request = new CreateRoomRequest { - Name = name ?? "Private Room", + Name = name ?? "New private Room", Visibility = "private", CreationContent = new(), PowerLevelContentOverride = new() { diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index b81713a..d26b1f8 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -29,13 +29,16 @@ public class GenericRoom { public string RoomId { get; set; } public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() { - var result = _httpClient.GetAsyncEnumerableFromJsonAsync<StateEventResponse>( - $"/_matrix/client/v3/rooms/{RoomId}/state"); + var result = _httpClient.GetAsyncEnumerableFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/state"); await foreach (var resp in result) { yield return resp; } } + public async Task<List<StateEventResponse>> GetFullStateAsListAsync() { + return await _httpClient.GetFromJsonAsync<List<StateEventResponse>>($"/_matrix/client/v3/rooms/{RoomId}/state"); + } + public async Task<T?> GetStateAsync<T>(string type, string stateKey = "") { var url = $"/_matrix/client/v3/rooms/{RoomId}/state"; if (!string.IsNullOrEmpty(type)) url += $"/{type}"; @@ -77,17 +80,82 @@ public class GenericRoom { } } - public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b", - string filter = "") { - var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}"; - if (!string.IsNullOrEmpty(filter)) url += $"&filter={filter}"; + public async Task<MessagesResponse> GetMessagesAsync(string from = "", int? limit = null, string dir = "b", string filter = "") { + var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?dir={dir}"; + if (!string.IsNullOrWhiteSpace(from)) url += $"&from={from}"; + if (limit is not null) url += $"&limit={limit}"; + if (!string.IsNullOrWhiteSpace(filter)) url += $"&filter={filter}"; var res = await _httpClient.GetFromJsonAsync<MessagesResponse>(url); return res ?? new MessagesResponse(); } + /// <summary> + /// Same as <see cref="GetMessagesAsync"/>, except keeps fetching more responses until the beginning of the room is found, or the target message limit is reached + /// </summary> + public async IAsyncEnumerable<MessagesResponse> GetManyMessagesAsync(string from = "", int limit = 100, string dir = "b", string filter = "", bool includeState = true, + bool fixForward = false) { + if (dir == "f" && fixForward) { + var concat = new List<MessagesResponse>(); + while (true) { + var resp = await GetMessagesAsync(from, int.MaxValue, "b", filter); + concat.Add(resp); + if (!includeState) + resp.State.Clear(); + from = resp.End; + if (resp.End is null) break; + } + + concat.Reverse(); + foreach (var eventResponse in concat) { + limit -= eventResponse.State.Count + eventResponse.Chunk.Count; + while (limit < 0) { + if (eventResponse.State.Count > 0 && eventResponse.State.Max(x => x.OriginServerTs) > eventResponse.Chunk.Max(x => x.OriginServerTs)) + eventResponse.State.Remove(eventResponse.State.MaxBy(x => x.OriginServerTs)); + else + eventResponse.Chunk.Remove(eventResponse.Chunk.MaxBy(x => x.OriginServerTs)); + + limit++; + } + + eventResponse.Chunk.Reverse(); + eventResponse.State.Reverse(); + yield return eventResponse; + if (limit <= 0) yield break; + } + } + else { + while (limit > 0) { + var resp = await GetMessagesAsync(from, limit, dir, filter); + + if (!includeState) + resp.State.Clear(); + + limit -= resp.Chunk.Count + resp.State.Count; + from = resp.End; + yield return resp; + if (resp.End is null) { + Console.WriteLine("End is null"); + yield break; + } + } + } + + Console.WriteLine("End of GetManyAsync"); + } + public async Task<string?> GetNameAsync() => (await GetStateAsync<RoomNameEventContent>("m.room.name"))?.Name; - public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null) { + public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) { + if (checkIfAlreadyMember) { + try { + var ce = await GetCreateEventAsync(); + return new() { + RoomId = RoomId + }; + } + catch { } //ignore + } + var join_url = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(RoomId)}"; Console.WriteLine($"Calling {join_url} with {homeservers?.Length ?? 0} via's..."); if (homeservers == null || homeservers.Length == 0) homeservers = new[] { RoomId.Split(':')[1] }; @@ -235,13 +303,17 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<EventIdResponse>(); } - public async Task<EventIdResponse?> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") { + public async Task<EventIdResponse?> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file", string contentType = "application/octet-stream") { var url = await Homeserver.UploadFile(fileName, fileStream); var content = new RoomMessageEventContent() { MessageType = messageType, Url = url, Body = fileName, FileName = fileName, + FileInfo = new() { + Size = fileStream.Length, + MimeType = contentType + } }; return await SendTimelineEventAsync("m.room.message", content); } diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index c8b6bb7..556bf86 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -7,7 +7,9 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Services; public class HomeserverResolverService(ILogger<HomeserverResolverService>? logger = null) { - private readonly MatrixHttpClient _httpClient = new(); + 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(); @@ -20,7 +22,7 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge _wellKnownSemaphores[homeserver].Release(); return known; } - + logger?.LogInformation("Resolving homeserver: {}", homeserver); var res = new WellKnownUris { Client = await _tryResolveFromClientWellknown(homeserver), @@ -33,11 +35,12 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge private async Task<string?> _tryResolveFromClientWellknown(string homeserver) { if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; - if (await _httpClient.CheckSuccessStatus($"{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; } + catch { } logger?.LogInformation("No client well-known..."); return null; @@ -45,13 +48,14 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge private async Task<string?> _tryResolveFromServerWellknown(string homeserver) { if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; - if (await _httpClient.CheckSuccessStatus($"{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.StartsWithAnyOf("http://", "https://")) hs = $"https://{hs}"; return hs; } + catch { } // fallback: most servers host these on the same location var clientUrl = await _tryResolveFromClientWellknown(homeserver); @@ -74,4 +78,4 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge public string? Client { get; set; } public string? Server { get; set; } } -} +} \ No newline at end of file |