From 3e934eee892f69a8f78b94950993000522702769 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Thu, 23 Nov 2023 05:42:33 +0100 Subject: Moderation bot work --- .../Spec/Ephemeral/PresenceStateEventContent.cs | 18 ++--- .../EventTypes/Spec/RoomMessageEventContent.cs | 19 ++++- .../State/Policy/PolicyRuleStateEventContent.cs | 23 ++++-- LibMatrix/Extensions/HttpClientExtensions.cs | 9 +-- LibMatrix/Helpers/MessageFormatter.cs | 5 ++ .../Homeservers/AuthenticatedHomeserverGeneric.cs | 20 ++++- LibMatrix/Interfaces/EventContent.cs | 33 ++++++-- LibMatrix/LibMatrix.csproj | 4 +- LibMatrix/Responses/CreateRoomRequest.cs | 41 +++++++++- LibMatrix/RoomTypes/GenericRoom.cs | 88 ++++++++++++++++++++-- LibMatrix/Services/HomeserverResolverService.cs | 14 ++-- 11 files changed, 228 insertions(+), 46 deletions(-) (limited to 'LibMatrix') 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; } /// /// Media URI for this message, if any /// [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 { /// /// Entity this ban applies to, can use * and ? as globs. /// @@ -52,4 +65,4 @@ public static class PolicyRecommendationTypes { /// Mute this user /// 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 SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) { + public override async Task 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 $"{displayName ?? id}"; } + + 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 $"{displayName ?? eventId}"; + } #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 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 JoinRoomAsync(string roomId, List 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() ?? throw new Exception("Failed to join room?"); + } + #region Room Profile Utility private async Task> 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(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 @@ - - + + 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 GetFullStateAsync() { - var result = _httpClient.GetAsyncEnumerableFromJsonAsync( - $"/_matrix/client/v3/rooms/{RoomId}/state"); + var result = _httpClient.GetAsyncEnumerableFromJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state"); await foreach (var resp in result) { yield return resp; } } + public async Task> GetFullStateAsListAsync() { + return await _httpClient.GetFromJsonAsync>($"/_matrix/client/v3/rooms/{RoomId}/state"); + } + public async Task GetStateAsync(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 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 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(url); return res ?? new MessagesResponse(); } + /// + /// Same as , except keeps fetching more responses until the beginning of the room is found, or the target message limit is reached + /// + public async IAsyncEnumerable 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(); + 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 GetNameAsync() => (await GetStateAsync("m.room.name"))?.Name; - public async Task JoinAsync(string[]? homeservers = null, string? reason = null) { + public async Task 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(); } - public async Task SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") { + public async Task 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? logger = null) { - private readonly MatrixHttpClient _httpClient = new(); + private readonly MatrixHttpClient _httpClient = new() { + Timeout = TimeSpan.FromMilliseconds(10000) + }; private static readonly ConcurrentDictionary _wellKnownCache = new(); private static readonly ConcurrentDictionary _wellKnownSemaphores = new(); @@ -20,7 +22,7 @@ public class HomeserverResolverService(ILogger? 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? logge private async Task _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($"{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? logge private async Task _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($"{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? logge public string? Client { get; set; } public string? Server { get; set; } } -} +} \ No newline at end of file -- cgit 1.4.1