diff options
author | TheArcaneBrony <myrainbowdash949@gmail.com> | 2023-08-14 04:09:13 +0200 |
---|---|---|
committer | TheArcaneBrony <myrainbowdash949@gmail.com> | 2023-08-14 04:09:13 +0200 |
commit | 0d0511e35d9965fc0ea5190ae3347c3d77c3334c (patch) | |
tree | b4e0c336cbccc37bd14952b447868a577ea15540 /LibMatrix | |
download | LibMatrix-bak-0d0511e35d9965fc0ea5190ae3347c3d77c3334c.tar.xz |
Split LibMatrix into separate repo
Diffstat (limited to 'LibMatrix')
57 files changed, 2282 insertions, 0 deletions
diff --git a/LibMatrix/AuthenticatedHomeServer.cs b/LibMatrix/AuthenticatedHomeServer.cs new file mode 100644 index 0000000..102d448 --- /dev/null +++ b/LibMatrix/AuthenticatedHomeServer.cs @@ -0,0 +1,174 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Filters; +using LibMatrix.Helpers; +using LibMatrix.Interfaces; +using LibMatrix.Responses; +using LibMatrix.Responses.Admin; +using LibMatrix.RoomTypes; +using LibMatrix.Services; + +namespace LibMatrix; + +public class AuthenticatedHomeServer : IHomeServer { + private readonly TieredStorageService _storage; + public readonly HomeserverAdminApi Admin; + public readonly SyncHelper SyncHelper; + + public AuthenticatedHomeServer(TieredStorageService storage, string canonicalHomeServerDomain, string accessToken) { + _storage = storage; + AccessToken = accessToken.Trim(); + HomeServerDomain = canonicalHomeServerDomain.Trim(); + Admin = new HomeserverAdminApi(this); + SyncHelper = new SyncHelper(this, storage); + _httpClient = new MatrixHttpClient(); + } + + public WhoAmIResponse WhoAmI { get; set; } = null!; + public string UserId => WhoAmI.UserId; + public string AccessToken { get; set; } + + + public async Task<GenericRoom> GetRoom(string roomId) => new(this, roomId); + + public async Task<List<GenericRoom>> GetJoinedRooms() { + var rooms = new List<GenericRoom>(); + var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms"); + + var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>(); + foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new GenericRoom(this, room.GetString())); + + Console.WriteLine($"Fetched {rooms.Count} rooms"); + + return rooms; + } + + public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") { + var res = await _httpClient.PostAsync($"/_matrix/media/v3/upload?filename={fileName}", new StreamContent(fileStream)); + if (!res.IsSuccessStatusCode) { + Console.WriteLine($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); + } + + var resJson = await res.Content.ReadFromJsonAsync<JsonElement>(); + return resJson.GetProperty("content_uri").GetString()!; + } + + public async Task<GenericRoom> CreateRoom(CreateRoomRequest creationEvent) { + var res = await _httpClient.PostAsJsonAsync("/_matrix/client/v3/createRoom", creationEvent); + if (!res.IsSuccessStatusCode) { + Console.WriteLine($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); + } + + return await GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString()); + } + + public class HomeserverAdminApi { + private readonly AuthenticatedHomeServer _authenticatedHomeServer; + + public HomeserverAdminApi(AuthenticatedHomeServer authenticatedHomeServer) => _authenticatedHomeServer = authenticatedHomeServer; + + public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) { + AdminRoomListingResult? res = null; + var i = 0; + int? totalRooms = null; + do { + var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 100)}&dir={dir}&order_by={orderBy}"; + if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; + + if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; + + Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); + + res = await _authenticatedHomeServer._httpClient.GetFromJsonAsync<AdminRoomListingResult>(url); + totalRooms ??= res?.TotalRooms; + Console.WriteLine(res.ToJson(false)); + foreach (var room in res.Rooms) { + if (localFilter is not null) { + if (!room.RoomId.Contains(localFilter.RoomIdContains)) { + totalRooms--; + continue; + } + if (!room.Name?.Contains(localFilter.NameContains) == true) { + totalRooms--; + continue; + } + if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) { + totalRooms--; + continue; + } + if (!room.Version.Contains(localFilter.VersionContains)) { + totalRooms--; + continue; + } + if (!room.Creator.Contains(localFilter.CreatorContains)) { + totalRooms--; + continue; + } + if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) { + totalRooms--; + continue; + } + if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) { + totalRooms--; + continue; + } + if(!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) { + totalRooms--; + continue; + } + if(!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) { + totalRooms--; + continue; + } + + if(localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { + totalRooms--; + continue; + } + if(localFilter.CheckPublic && room.Public != localFilter.Public) { + totalRooms--; + continue; + } + + if(room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { + totalRooms--; + continue; + } + if(room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { + totalRooms--; + continue; + } + } + // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) && + // !( + // room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || + // room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || + // room.Creator?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true + // ) + // ) { + // totalRooms--; + // continue; + // } + + i++; + yield return room; + } + } while (i < Math.Min(limit, totalRooms ?? limit)); + } + } +} + +public class WhoAmIResponse { + [JsonPropertyName("user_id")] + public string UserId { get; set; } = null!; + + [JsonPropertyName("device_id")] + public string? DeviceId { get; set; } + [JsonPropertyName("is_guest")] + public bool? IsGuest { get; set; } +} diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/EventIdResponse.cs new file mode 100644 index 0000000..31a95b8 --- /dev/null +++ b/LibMatrix/EventIdResponse.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix; + +public class EventIdResponse { + [JsonPropertyName("event_id")] + public string EventId { get; set; } = null!; +} diff --git a/LibMatrix/Extensions/ClassCollector.cs b/LibMatrix/Extensions/ClassCollector.cs new file mode 100644 index 0000000..f53850a --- /dev/null +++ b/LibMatrix/Extensions/ClassCollector.cs @@ -0,0 +1,22 @@ +using System.Reflection; + +namespace LibMatrix.Extensions; + +public class ClassCollector<T> where T : class { + static ClassCollector() { + if (!typeof(T).IsInterface) + throw new ArgumentException( + $"ClassCollector<T> must be used with an interface type. Passed type: {typeof(T).Name}"); + } + + public List<Type> ResolveFromAllAccessibleAssemblies() => AppDomain.CurrentDomain.GetAssemblies().SelectMany(ResolveFromAssembly).ToList(); + + public List<Type> ResolveFromObjectReference(object obj) => ResolveFromTypeReference(obj.GetType()); + + public List<Type> ResolveFromTypeReference(Type t) => Assembly.GetAssembly(t)?.GetReferencedAssemblies().SelectMany(ResolveFromAssemblyName).ToList() ?? new List<Type>(); + + public List<Type> ResolveFromAssemblyName(AssemblyName assemblyName) => ResolveFromAssembly(Assembly.Load(assemblyName)); + + public List<Type> ResolveFromAssembly(Assembly assembly) => assembly.GetTypes() + .Where(x => x is { IsClass: true, IsAbstract: false } && x.GetInterfaces().Contains(typeof(T))).ToList(); +} diff --git a/LibMatrix/Extensions/DictionaryExtensions.cs b/LibMatrix/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..fbc5cf5 --- /dev/null +++ b/LibMatrix/Extensions/DictionaryExtensions.cs @@ -0,0 +1,33 @@ +namespace LibMatrix.Extensions; + +public static class DictionaryExtensions { + public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, + TKey oldKey, TKey newKey) { + TValue value; + if (!dict.Remove(oldKey, out value)) + return false; + + dict[newKey] = value; // or dict.Add(newKey, value) depending on ur comfort + return true; + } + + public static Y GetOrCreate<X, Y>(this IDictionary<X, Y> dict, X key) where Y : new() { + if (dict.TryGetValue(key, out var value)) { + return value; + } + + value = new Y(); + dict.Add(key, value); + return value; + } + + public static Y GetOrCreate<X, Y>(this IDictionary<X, Y> dict, X key, Func<X, Y> valueFactory) { + if (dict.TryGetValue(key, out var value)) { + return value; + } + + value = valueFactory(key); + dict.Add(key, value); + return value; + } +} diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..797a077 --- /dev/null +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -0,0 +1,76 @@ +using System.Net.Http.Headers; +using System.Reflection; +using System.Text.Json; + +namespace LibMatrix.Extensions; + +public static class HttpClientExtensions { + public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url) { + //cors causes failure, try to catch + try { + var resp = await hc.GetAsync(url); + return resp.IsSuccessStatusCode; + } + catch (Exception e) { + Console.WriteLine($"Failed to check success status: {e.Message}"); + return false; + } + } +} + +public class MatrixHttpClient : HttpClient { + public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) { + Console.WriteLine($"Sending request to {request.RequestUri}"); + try { + HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingResponseKey = + new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"); + request.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + } + catch (Exception e) { + Console.WriteLine("Failed to set browser response streaming:"); + Console.WriteLine(e); + } + + var a = await base.SendAsync(request, cancellationToken); + if (!a.IsSuccessStatusCode) { + var content = await a.Content.ReadAsStringAsync(cancellationToken); + if (content.StartsWith('{')) { + var ex = JsonSerializer.Deserialize<MatrixException>(content); + ex.RawContent = content; + // Console.WriteLine($"Failed to send request: {ex}"); + if (ex?.RetryAfterMs is not null) { + await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); + typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(request, 0); + return await SendAsync(request, cancellationToken); + } + + throw ex!; + } + + throw new InvalidDataException("Encountered invalid data:\n" + content); + } + + return a; + } + + // GetFromJsonAsync + public async Task<T> GetFromJsonAsync<T>(string requestUri, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken); + } + + // GetStreamAsync + public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(cancellationToken); + } +} diff --git a/LibMatrix/Extensions/IEnumerableExtensions.cs b/LibMatrix/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..8124947 --- /dev/null +++ b/LibMatrix/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,7 @@ +namespace LibMatrix.Extensions; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class MatrixEventAttribute : Attribute { + public string EventName { get; set; } + public bool Legacy { get; set; } +} diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs new file mode 100644 index 0000000..caf96e1 --- /dev/null +++ b/LibMatrix/Extensions/JsonElementExtensions.cs @@ -0,0 +1,150 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using LibMatrix.Responses; + +namespace LibMatrix.Extensions; + +public static class JsonElementExtensions { + public static bool FindExtraJsonElementFields(this JsonElement obj, Type objectType, string objectPropertyName) { + if (objectPropertyName == "content" && objectType == typeof(JsonObject)) + objectType = typeof(StateEventResponse); + // if (t == typeof(JsonNode)) + // return false; + + Console.WriteLine($"{objectType.Name} {objectPropertyName}"); + bool unknownPropertyFound = false; + var mappedPropsDict = objectType.GetProperties() + .Where(x => x.GetCustomAttribute<JsonPropertyNameAttribute>() is not null) + .ToDictionary(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()!.Name, x => x); + objectType.GetProperties().Where(x => !mappedPropsDict.ContainsKey(x.Name)) + .ToList().ForEach(x => mappedPropsDict.TryAdd(x.Name, x)); + + foreach (var field in obj.EnumerateObject()) { + if (mappedPropsDict.TryGetValue(field.Name, out var mappedProperty)) { + //dictionary + if (mappedProperty.PropertyType.IsGenericType && + mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { + unknownPropertyFound |= _checkDictionary(field, objectType, mappedProperty.PropertyType); + continue; + } + + if (mappedProperty.PropertyType.IsGenericType && + mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { + unknownPropertyFound |= _checkList(field, objectType, mappedProperty.PropertyType); + continue; + } + + if (field.Name == "content" && (objectType == typeof(StateEventResponse) || objectType == typeof(StateEvent))) { + unknownPropertyFound |= field.FindExtraJsonPropertyFieldsByValueKind( + StateEvent.GetStateEventType(obj.GetProperty("type").GetString()), + mappedProperty.PropertyType); + continue; + } + + unknownPropertyFound |= + field.FindExtraJsonPropertyFieldsByValueKind(objectType, mappedProperty.PropertyType); + continue; + } + + Console.WriteLine($"[!!] Unknown property {field.Name} in {objectType.Name}!"); + unknownPropertyFound = true; + } + + return unknownPropertyFound; + } + + private static bool FindExtraJsonPropertyFieldsByValueKind(this JsonProperty field, Type containerType, + Type propertyType) { + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { + propertyType = propertyType.GetGenericArguments()[0]; + } + + bool switchResult = false; + switch (field.Value.ValueKind) { + case JsonValueKind.Array: + switchResult = field.Value.EnumerateArray().Aggregate(switchResult, + (current, element) => current | element.FindExtraJsonElementFields(propertyType, field.Name)); + break; + case JsonValueKind.Object: + switchResult |= field.Value.FindExtraJsonElementFields(propertyType, field.Name); + break; + case JsonValueKind.True: + case JsonValueKind.False: + return _checkBool(field, containerType, propertyType); + case JsonValueKind.String: + return _checkString(field, containerType, propertyType); + case JsonValueKind.Number: + return _checkNumber(field, containerType, propertyType); + case JsonValueKind.Undefined: + case JsonValueKind.Null: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return switchResult; + } + + private static bool _checkBool(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(bool)) return true; + Console.WriteLine( + $"[!!] Encountered bool for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkString(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(string)) return true; + // ReSharper disable once BuiltInTypeReferenceStyle + if (propertyType == typeof(String)) return true; + Console.WriteLine( + $"[!!] Encountered string for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkNumber(this JsonProperty field, Type containerType, Type propertyType) { + if (propertyType == typeof(int) || + propertyType == typeof(double) || + propertyType == typeof(float) || + propertyType == typeof(decimal) || + propertyType == typeof(long) || + propertyType == typeof(short) || + propertyType == typeof(uint) || + propertyType == typeof(ulong) || + propertyType == typeof(ushort) || + propertyType == typeof(byte) || + propertyType == typeof(sbyte)) + return true; + Console.WriteLine( + $"[!!] Encountered number for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); + return false; + } + + private static bool _checkDictionary(this JsonProperty field, Type containerType, Type propertyType) { + var keyType = propertyType.GetGenericArguments()[0]; + var valueType = propertyType.GetGenericArguments()[1]; + valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; + Console.WriteLine( + $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!"); + + return field.Value.EnumerateObject() + .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) + .Aggregate(false, (current, key) => + current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType) + ); + } + + private static bool _checkList(this JsonProperty field, Type containerType, Type propertyType) { + var valueType = propertyType.GetGenericArguments()[0]; + valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; + Console.WriteLine( + $"Encountered list {field.Name} with value type {valueType.Name}!"); + + return field.Value.EnumerateArray() + .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) + .Aggregate(false, (current, key) => + current | key.FindExtraJsonElementFields(valueType, field.Name) + ); + } +} diff --git a/LibMatrix/Extensions/ObjectExtensions.cs b/LibMatrix/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..085de7d --- /dev/null +++ b/LibMatrix/Extensions/ObjectExtensions.cs @@ -0,0 +1,14 @@ +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace LibMatrix.Extensions; + +public static class ObjectExtensions { + public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false, bool unsafeContent = false) { + var jso = new JsonSerializerOptions(); + if (indent) jso.WriteIndented = true; + if (ignoreNull) jso.IgnoreNullValues = true; + if (unsafeContent) jso.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + return JsonSerializer.Serialize(obj, jso); + } +} diff --git a/LibMatrix/Extensions/StringExtensions.cs b/LibMatrix/Extensions/StringExtensions.cs new file mode 100644 index 0000000..491fa77 --- /dev/null +++ b/LibMatrix/Extensions/StringExtensions.cs @@ -0,0 +1,13 @@ +namespace LibMatrix.Extensions; + +public static class StringExtensions { + // public static async Task<string> GetMediaUrl(this string MxcUrl) + // { + // //MxcUrl: mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg + // //target: https://matrix.rory.gay/_matrix/media/v3/download/rory.gay/ocRVanZoUTCcifcVNwXgbtTg + // + // var server = MxcUrl.Split('/')[2]; + // var mediaId = MxcUrl.Split('/')[3]; + // return $"{(await new RemoteHomeServer(server).Configure()).FullHomeServerDomain}/_matrix/media/v3/download/{server}/{mediaId}"; + // } +} diff --git a/LibMatrix/Filters/LocalRoomQueryFilter.cs b/LibMatrix/Filters/LocalRoomQueryFilter.cs new file mode 100644 index 0000000..668d408 --- /dev/null +++ b/LibMatrix/Filters/LocalRoomQueryFilter.cs @@ -0,0 +1,28 @@ +namespace LibMatrix.Filters; + +public class LocalRoomQueryFilter { + public string RoomIdContains { get; set; } = ""; + public string NameContains { get; set; } = ""; + public string CanonicalAliasContains { get; set; } = ""; + public string VersionContains { get; set; } = ""; + public string CreatorContains { get; set; } = ""; + public string EncryptionContains { get; set; } = ""; + public string JoinRulesContains { get; set; } = ""; + public string GuestAccessContains { get; set; } = ""; + public string HistoryVisibilityContains { get; set; } = ""; + + public bool Federatable { get; set; } = true; + public bool Public { get; set; } = true; + + public int JoinedMembersGreaterThan { get; set; } = 0; + public int JoinedMembersLessThan { get; set; } = int.MaxValue; + + public int JoinedLocalMembersGreaterThan { get; set; } = 0; + public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue; + public int StateEventsGreaterThan { get; set; } = 0; + public int StateEventsLessThan { get; set; } = int.MaxValue; + + + public bool CheckFederation { get; set; } + public bool CheckPublic { get; set; } +} diff --git a/LibMatrix/Filters/SyncFilter.cs b/LibMatrix/Filters/SyncFilter.cs new file mode 100644 index 0000000..c907f6b --- /dev/null +++ b/LibMatrix/Filters/SyncFilter.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Filters; + +public class SyncFilter { + [JsonPropertyName("account_data")] + public EventFilter? AccountData { get; set; } + + [JsonPropertyName("presence")] + public EventFilter? Presence { get; set; } + + [JsonPropertyName("room")] + public RoomFilter? Room { get; set; } + + public class RoomFilter { + [JsonPropertyName("account_data")] + public StateFilter? AccountData { get; set; } + + [JsonPropertyName("ephemeral")] + public StateFilter? Ephemeral { get; set; } + + [JsonPropertyName("state")] + public StateFilter? State { get; set; } + + [JsonPropertyName("timeline")] + public StateFilter? Timeline { get; set; } + + + public class StateFilter : EventFilter { + [JsonPropertyName("contains_url")] + public bool? ContainsUrl { get; set; } + + [JsonPropertyName("include_redundant_members")] + public bool? IncludeRedundantMembers { get; set; } + + [JsonPropertyName("lazy_load_members")] + public bool? LazyLoadMembers { get; set; } + + [JsonPropertyName("rooms")] + public List<string>? Rooms { get; set; } + + [JsonPropertyName("not_rooms")] + public List<string>? NotRooms { get; set; } + + [JsonPropertyName("unread_thread_notifications")] + public bool? UnreadThreadNotifications { get; set; } + } + } + + public class EventFilter { + [JsonPropertyName("limit")] + public int? Limit { get; set; } + + [JsonPropertyName("types")] + public List<string>? Types { get; set; } + + [JsonPropertyName("not_types")] + public List<string>? NotTypes { get; set; } + + [JsonPropertyName("senders")] + public List<string>? Senders { get; set; } + + [JsonPropertyName("not_senders")] + public List<string>? NotSenders { get; set; } + } +} diff --git a/LibMatrix/Helpers/MediaResolver.cs b/LibMatrix/Helpers/MediaResolver.cs new file mode 100644 index 0000000..6ddb221 --- /dev/null +++ b/LibMatrix/Helpers/MediaResolver.cs @@ -0,0 +1,6 @@ +namespace LibMatrix.Helpers; + +public class MediaResolver { + public static string ResolveMediaUri(string homeserver, string mxc) => + mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/"); +} diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs new file mode 100644 index 0000000..2015eaa --- /dev/null +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -0,0 +1,233 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Filters; +using LibMatrix.Responses; +using LibMatrix.Services; + +namespace LibMatrix.Helpers; + +public class SyncHelper { + private readonly AuthenticatedHomeServer _homeServer; + private readonly TieredStorageService _storageService; + + public SyncHelper(AuthenticatedHomeServer homeServer, TieredStorageService storageService) { + _homeServer = homeServer; + _storageService = storageService; + } + + public async Task<SyncResult?> Sync( + string? since = null, + int? timeout = 30000, + string? setPresence = "online", + SyncFilter? filter = null, + CancellationToken? cancellationToken = null) { + var outFileName = "sync-" + + (await _storageService.CacheStorageProvider.GetAllKeysAsync()).Count( + x => x.StartsWith("sync")) + + ".json"; + var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}"; + if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}"; + if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}"; + // else url += "&full_state=true"; + Console.WriteLine("Calling: " + url); + try { + var req = await _homeServer._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None); + + // var res = await JsonSerializer.DeserializeAsync<SyncResult>(await req.Content.ReadAsStreamAsync()); + +#if DEBUG && false + var jsonObj = await req.Content.ReadFromJsonAsync<JsonElement>(); + try { + await _homeServer._httpClient.PostAsJsonAsync( + "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName, jsonObj); + } + catch (Exception e) { + Console.WriteLine("[!!] Checking sync response failed: " + e); + } + + var res = jsonObj.Deserialize<SyncResult>(); + return res; +#else + return await req.Content.ReadFromJsonAsync<SyncResult>(); +#endif + } + catch (TaskCanceledException) { + Console.WriteLine("Sync cancelled!"); + } + catch (Exception e) { + Console.WriteLine(e); + } + + return null; + } + + [SuppressMessage("ReSharper", "FunctionNeverReturns")] + public async Task RunSyncLoop( + bool skipInitialSyncEvents = true, + string? since = null, + int? timeout = 30000, + string? setPresence = "online", + SyncFilter? filter = null, + CancellationToken? cancellationToken = null + ) { + await Task.WhenAll((await _storageService.CacheStorageProvider.GetAllKeysAsync()) + .Where(x => x.StartsWith("sync")) + .ToList() + .Select(x => _storageService.CacheStorageProvider.DeleteObjectAsync(x))); + SyncResult? sync = null; + string? nextBatch = since; + while (cancellationToken is null || !cancellationToken.Value.IsCancellationRequested) { + sync = await Sync(since: nextBatch, timeout: timeout, setPresence: setPresence, filter: filter, + cancellationToken: cancellationToken); + nextBatch = sync?.NextBatch ?? nextBatch; + if (sync is null) continue; + Console.WriteLine($"Got sync, next batch: {nextBatch}!"); + + if (sync.Rooms is { Invite.Count: > 0 }) { + foreach (var roomInvite in sync.Rooms.Invite) { + var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList(); + await Task.WhenAll(tasks); + } + } + + if (sync.AccountData is { Events: { Count: > 0 } }) { + foreach (var accountDataEvent in sync.AccountData.Events) { + var tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList(); + await Task.WhenAll(tasks); + } + } + + // Things that are skipped on the first sync + if (skipInitialSyncEvents) { + skipInitialSyncEvents = false; + continue; + } + + if (sync.Rooms is { Join.Count: > 0 }) { + foreach (var updatedRoom in sync.Rooms.Join) { + foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) { + stateEventResponse.RoomId = updatedRoom.Key; + var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList(); + await Task.WhenAll(tasks); + } + } + } + } + } + + /// <summary> + /// Event fired when a room invite is received + /// </summary> + public List<Func<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>, Task>> + InviteReceivedHandlers { get; } = new(); + + public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new(); + public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); +} + +public class SyncResult { + [JsonPropertyName("next_batch")] + public string NextBatch { get; set; } + + [JsonPropertyName("account_data")] + public EventList? AccountData { get; set; } + + [JsonPropertyName("presence")] + public PresenceDataStructure? Presence { get; set; } + + [JsonPropertyName("device_one_time_keys_count")] + public Dictionary<string, int> DeviceOneTimeKeysCount { get; set; } + + [JsonPropertyName("rooms")] + public RoomsDataStructure? Rooms { get; set; } + + [JsonPropertyName("to_device")] + public EventList? ToDevice { get; set; } + + [JsonPropertyName("device_lists")] + public DeviceListsDataStructure? DeviceLists { get; set; } + + public class DeviceListsDataStructure { + [JsonPropertyName("changed")] + public List<string>? Changed { get; set; } + + [JsonPropertyName("left")] + public List<string>? Left { get; set; } + } + + // supporting classes + public class PresenceDataStructure { + [JsonPropertyName("events")] + public List<StateEventResponse> Events { get; set; } + } + + public class RoomsDataStructure { + [JsonPropertyName("join")] + public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; } + + [JsonPropertyName("invite")] + public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; } + + public class JoinedRoomDataStructure { + [JsonPropertyName("timeline")] + public TimelineDataStructure Timeline { get; set; } + + [JsonPropertyName("state")] + public EventList State { get; set; } + + [JsonPropertyName("account_data")] + public EventList AccountData { get; set; } + + [JsonPropertyName("ephemeral")] + public EventList Ephemeral { get; set; } + + [JsonPropertyName("unread_notifications")] + public UnreadNotificationsDataStructure UnreadNotifications { get; set; } + + [JsonPropertyName("summary")] + public SummaryDataStructure Summary { get; set; } + + public class TimelineDataStructure { + [JsonPropertyName("events")] + public List<StateEventResponse> Events { get; set; } + + [JsonPropertyName("prev_batch")] + public string PrevBatch { get; set; } + + [JsonPropertyName("limited")] + public bool Limited { get; set; } + } + + public class UnreadNotificationsDataStructure { + [JsonPropertyName("notification_count")] + public int NotificationCount { get; set; } + + [JsonPropertyName("highlight_count")] + public int HighlightCount { get; set; } + } + + public class SummaryDataStructure { + [JsonPropertyName("m.heroes")] + public List<string> Heroes { get; set; } + + [JsonPropertyName("m.invited_member_count")] + public int InvitedMemberCount { get; set; } + + [JsonPropertyName("m.joined_member_count")] + public int JoinedMemberCount { get; set; } + } + } + + public class InvitedRoomDataStructure { + [JsonPropertyName("invite_state")] + public EventList InviteState { get; set; } + } + } +} + +public class EventList { + [JsonPropertyName("events")] + public List<StateEventResponse> Events { get; set; } +} diff --git a/LibMatrix/Interfaces/IHomeServer.cs b/LibMatrix/Interfaces/IHomeServer.cs new file mode 100644 index 0000000..5e7e374 --- /dev/null +++ b/LibMatrix/Interfaces/IHomeServer.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Json; +using LibMatrix.Extensions; +using LibMatrix.StateEventTypes.Spec; + +namespace LibMatrix.Interfaces; + +public class IHomeServer { + private readonly Dictionary<string, object> _profileCache = new(); + public string HomeServerDomain { get; set; } + public string FullHomeServerDomain { get; set; } + + public MatrixHttpClient _httpClient { get; set; } = new(); + + public async Task<ProfileResponseEventData> GetProfile(string mxid) { + if(mxid is null) throw new ArgumentNullException(nameof(mxid)); + if (_profileCache.ContainsKey(mxid)) { + if (_profileCache[mxid] is SemaphoreSlim s) await s.WaitAsync(); + if (_profileCache[mxid] is ProfileResponseEventData p) return p; + } + _profileCache[mxid] = new SemaphoreSlim(1); + + var resp = await _httpClient.GetAsync($"/_matrix/client/v3/profile/{mxid}"); + var data = await resp.Content.ReadFromJsonAsync<ProfileResponseEventData>(); + if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); + _profileCache[mxid] = data; + + return data; + } +} diff --git a/LibMatrix/Interfaces/IStateEventType.cs b/LibMatrix/Interfaces/IStateEventType.cs new file mode 100644 index 0000000..d80f22d --- /dev/null +++ b/LibMatrix/Interfaces/IStateEventType.cs @@ -0,0 +1,5 @@ +namespace LibMatrix.Interfaces; + +public interface IStateEventType { + +} diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs new file mode 100644 index 0000000..519d8ed --- /dev/null +++ b/LibMatrix/Interfaces/Services/IStorageProvider.cs @@ -0,0 +1,58 @@ +namespace LibMatrix.Interfaces.Services; + +public interface IStorageProvider { + // save all children of a type with reflection + public Task SaveAllChildrenAsync<T>(string key, T value) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren<T>(key, value)!"); + throw new NotImplementedException(); + } + + // load all children of a type with reflection + public Task<T?> LoadAllChildrenAsync<T>(string key) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren<T>(key)!"); + throw new NotImplementedException(); + } + + + public Task SaveObjectAsync<T>(string key, T value) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!"); + throw new NotImplementedException(); + } + + // load + public Task<T?> LoadObjectAsync<T>(string key) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject<T>(key)!"); + throw new NotImplementedException(); + } + + // check if exists + public Task<bool> ObjectExistsAsync(string key) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!"); + throw new NotImplementedException(); + } + + // get all keys + public Task<List<string>> GetAllKeysAsync() { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!"); + throw new NotImplementedException(); + } + + + // delete + public Task DeleteObjectAsync(string key) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!"); + throw new NotImplementedException(); + } + + // save stream + public Task SaveStreamAsync(string key, Stream stream) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveStream(key, stream)!"); + throw new NotImplementedException(); + } + + // load stream + public Task<Stream?> LoadStreamAsync(string key) { + Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!"); + throw new NotImplementedException(); + } +} diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj new file mode 100644 index 0000000..3571eab --- /dev/null +++ b/LibMatrix/LibMatrix.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> + </ItemGroup> + +</Project> diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs new file mode 100644 index 0000000..99bacb5 --- /dev/null +++ b/LibMatrix/MatrixException.cs @@ -0,0 +1,59 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; + +namespace LibMatrix; + +public class MatrixException : Exception { + [JsonPropertyName("errcode")] + public string ErrorCode { get; set; } + + [JsonPropertyName("error")] + public string Error { get; set; } + + [JsonPropertyName("soft_logout")] + public bool? SoftLogout { get; set; } + + [JsonPropertyName("retry_after_ms")] + public int? RetryAfterMs { get; set; } + + public string RawContent { get; set; } + + public override string Message => + $"{ErrorCode}: {ErrorCode switch { + // common + "M_FORBIDDEN" => $"You do not have permission to perform this action: {Error}", + "M_UNKNOWN_TOKEN" => $"The access token specified was not recognised: {Error}{(SoftLogout == true ? " (soft logout)" : "")}", + "M_MISSING_TOKEN" => $"No access token was specified: {Error}", + "M_BAD_JSON" => $"Request contained valid JSON, but it was malformed in some way: {Error}", + "M_NOT_JSON" => $"Request did not contain valid JSON: {Error}", + "M_NOT_FOUND" => $"The requested resource was not found: {Error}", + "M_LIMIT_EXCEEDED" => $"Too many requests have been sent in a short period of time. Wait a while then try again: {Error}", + "M_UNRECOGNISED" => $"The server did not recognise the request: {Error}", + "M_UNKOWN" => $"The server encountered an unexpected error: {Error}", + // endpoint specific + "M_UNAUTHORIZED" => $"The request did not contain valid authentication information for the target of the request: {Error}", + "M_USER_DEACTIVATED" => $"The user ID associated with the request has been deactivated: {Error}", + "M_USER_IN_USE" => $"The user ID associated with the request is already in use: {Error}", + "M_INVALID_USERNAME" => $"The requested user ID is not valid: {Error}", + "M_ROOM_IN_USE" => $"The room alias requested is already taken: {Error}", + "M_INVALID_ROOM_STATE" => $"The room associated with the request is not in a valid state to perform the request: {Error}", + "M_THREEPID_IN_USE" => $"The threepid requested is already associated with a user ID on this server: {Error}", + "M_THREEPID_NOT_FOUND" => $"The threepid requested is not associated with any user ID: {Error}", + "M_THREEPID_AUTH_FAILED" => $"The provided threepid and/or token was invalid: {Error}", + "M_THREEPID_DENIED" => $"The homeserver does not permit the third party identifier in question: {Error}", + "M_SERVER_NOT_TRUSTED" => $"The homeserver does not trust the identity server: {Error}", + "M_UNSUPPORTED_ROOM_VERSION" => $"The room version is not supported: {Error}", + "M_INCOMPATIBLE_ROOM_VERSION" => $"The room version is incompatible: {Error}", + "M_BAD_STATE" => $"The request was invalid because the state was invalid: {Error}", + "M_GUEST_ACCESS_FORBIDDEN" => $"Guest access is forbidden: {Error}", + "M_CAPTCHA_NEEDED" => $"Captcha needed: {Error}", + "M_CAPTCHA_INVALID" => $"Captcha invalid: {Error}", + "M_MISSING_PARAM" => $"Missing parameter: {Error}", + "M_INVALID_PARAM" => $"Invalid parameter: {Error}", + "M_TOO_LARGE" => $"The request or entity was too large: {Error}", + "M_EXCLUSIVE" => $"The resource being requested is reserved by an application service, or the application service making the request has not created the resource: {Error}", + "M_RESOURCE_LIMIT_EXCEEDED" => $"Exceeded resource limit: {Error}", + "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" => $"Cannot leave server notice room: {Error}", + _ => $"Unknown error: {new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true)}" + }}"; +} diff --git a/LibMatrix/MessagesResponse.cs b/LibMatrix/MessagesResponse.cs new file mode 100644 index 0000000..f09d136 --- /dev/null +++ b/LibMatrix/MessagesResponse.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using LibMatrix.Responses; + +namespace LibMatrix; + +public class MessagesResponse { + [JsonPropertyName("start")] + public string Start { get; set; } + + [JsonPropertyName("end")] + public string? End { get; set; } + + [JsonPropertyName("chunk")] + public List<StateEventResponse> Chunk { get; set; } = new(); + + [JsonPropertyName("state")] + public List<StateEventResponse> State { get; set; } = new(); +} diff --git a/LibMatrix/RemoteHomeServer.cs b/LibMatrix/RemoteHomeServer.cs new file mode 100644 index 0000000..81ef9a7 --- /dev/null +++ b/LibMatrix/RemoteHomeServer.cs @@ -0,0 +1,13 @@ +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix; + +public class RemoteHomeServer : IHomeServer { + public RemoteHomeServer(string canonicalHomeServerDomain) { + HomeServerDomain = canonicalHomeServerDomain; + _httpClient = new MatrixHttpClient(); + _httpClient.Timeout = TimeSpan.FromSeconds(5); + } + +} diff --git a/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs b/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs new file mode 100644 index 0000000..f22c8d2 --- /dev/null +++ b/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses.Admin; + +public class AdminRoomDeleteRequest { + [JsonPropertyName("new_room_user_id")] + public string? NewRoomUserId { get; set; } + [JsonPropertyName("room_name")] + public string? RoomName { get; set; } + [JsonPropertyName("block")] + public bool Block { get; set; } + [JsonPropertyName("purge")] + public bool Purge { get; set; } + [JsonPropertyName("message")] + public string? Message { get; set; } + [JsonPropertyName("force_purge")] + public bool ForcePurge { get; set; } +} diff --git a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs new file mode 100644 index 0000000..f035184 --- /dev/null +++ b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses.Admin; + +public class AdminRoomListingResult { + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("total_rooms")] + public int TotalRooms { get; set; } + + [JsonPropertyName("next_batch")] + public int? NextBatch { get; set; } + + [JsonPropertyName("prev_batch")] + public int? PrevBatch { get; set; } + + [JsonPropertyName("rooms")] + public List<AdminRoomListingResultRoom> Rooms { get; set; } = new(); + + public class AdminRoomListingResultRoom { + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("canonical_alias")] + public string? CanonicalAlias { get; set; } + + [JsonPropertyName("joined_members")] + public int JoinedMembers { get; set; } + + [JsonPropertyName("joined_local_members")] + public int JoinedLocalMembers { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("creator")] + public string Creator { get; set; } + + [JsonPropertyName("encryption")] + public string? Encryption { get; set; } + + [JsonPropertyName("federatable")] + public bool Federatable { get; set; } + + [JsonPropertyName("public")] + public bool Public { get; set; } + + [JsonPropertyName("join_rules")] + public string? JoinRules { get; set; } + + [JsonPropertyName("guest_access")] + public string? GuestAccess { get; set; } + + [JsonPropertyName("history_visibility")] + public string? HistoryVisibility { get; set; } + + [JsonPropertyName("state_events")] + public int StateEvents { get; set; } + } +} diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs new file mode 100644 index 0000000..d59e6fd --- /dev/null +++ b/LibMatrix/Responses/CreateRoomRequest.cs @@ -0,0 +1,74 @@ +using System.Reflection; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using LibMatrix.Extensions; +using LibMatrix.StateEventTypes.Spec; + +namespace LibMatrix.Responses; + +public class CreateRoomRequest { + [JsonIgnore] public CreationContentBaseType _creationContentBaseType; + + public CreateRoomRequest() => _creationContentBaseType = new CreationContentBaseType(this); + + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + [JsonPropertyName("room_alias_name")] + public string RoomAliasName { get; set; } = null!; + + //we dont want to use this, we want more control + // [JsonPropertyName("preset")] + // public string Preset { get; set; } = null!; + + [JsonPropertyName("initial_state")] + public List<StateEvent> InitialState { get; set; } = null!; + + [JsonPropertyName("visibility")] + public string Visibility { get; set; } = null!; + + [JsonPropertyName("power_level_content_override")] + public RoomPowerLevelEventData PowerLevelContentOverride { get; set; } = null!; + + [JsonPropertyName("creation_content")] + public JsonObject CreationContent { get; set; } = new(); + + /// <summary> + /// For use only when you can't use the CreationContent property + /// </summary> + + public StateEvent this[string event_type, string event_key = ""] { + get { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key); + if (stateEvent == null) { + InitialState.Add(stateEvent = new StateEvent { + Type = event_type, + StateKey = event_key, + TypedContent = Activator.CreateInstance( + StateEvent.KnownStateEventTypes.FirstOrDefault(x => + x.GetCustomAttributes<MatrixEventAttribute>()? + .Any(y => y.EventName == event_type) ?? false) ?? typeof(object) + ) + }); + } + return stateEvent; + } + set { + var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key); + if (stateEvent == null) + InitialState.Add(value); + else + InitialState[InitialState.IndexOf(stateEvent)] = value; + } + } + + public Dictionary<string, string> Validate() { + Dictionary<string, string> errors = new(); + if (!Regex.IsMatch(RoomAliasName, @"[a-zA-Z0-9_\-]+$")) + errors.Add("room_alias_name", + "Room alias name must only contain letters, numbers, underscores, and hyphens."); + + return errors; + } +} diff --git a/LibMatrix/Responses/CreationContentBaseType.cs b/LibMatrix/Responses/CreationContentBaseType.cs new file mode 100644 index 0000000..ba3ce5e --- /dev/null +++ b/LibMatrix/Responses/CreationContentBaseType.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class CreationContentBaseType { + private readonly CreateRoomRequest createRoomRequest; + + public CreationContentBaseType(CreateRoomRequest createRoomRequest) => this.createRoomRequest = createRoomRequest; + + [JsonPropertyName("type")] + public string Type { + get => (string)createRoomRequest.CreationContent["type"]; + set { + if (value is "null" or "") createRoomRequest.CreationContent.Remove("type"); + else createRoomRequest.CreationContent["type"] = value; + } + } +} diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs new file mode 100644 index 0000000..2800a9c --- /dev/null +++ b/LibMatrix/Responses/LoginResponse.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class LoginResponse { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("device_id")] + public string DeviceId { get; set; } + + [JsonPropertyName("home_server")] + public string Homeserver { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } +} diff --git a/LibMatrix/Responses/StateEventResponse.cs b/LibMatrix/Responses/StateEventResponse.cs new file mode 100644 index 0000000..b3d5b96 --- /dev/null +++ b/LibMatrix/Responses/StateEventResponse.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses; + +public class StateEventResponse : StateEvent { + [JsonPropertyName("origin_server_ts")] + public ulong OriginServerTs { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("unsigned")] + public UnsignedData? Unsigned { get; set; } + + [JsonPropertyName("event_id")] + public string EventId { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonPropertyName("replaces_state")] + public string ReplacesState { get; set; } + + public class UnsignedData { + [JsonPropertyName("age")] + public ulong? Age { get; set; } + + [JsonPropertyName("redacted_because")] + public object? RedactedBecause { get; set; } + + [JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } + + [JsonPropertyName("prev_sender")] + public string? PrevSender { get; set; } + + [JsonPropertyName("prev_content")] + public JsonObject? PrevContent { get; set; } + } +} diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs new file mode 100644 index 0000000..b935b9d --- /dev/null +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -0,0 +1,186 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Web; +using LibMatrix.Extensions; +using LibMatrix.Responses; +using LibMatrix.StateEventTypes.Spec; + +namespace LibMatrix.RoomTypes; + +public class GenericRoom { + internal readonly AuthenticatedHomeServer _homeServer; + internal readonly MatrixHttpClient _httpClient; + + public GenericRoom(AuthenticatedHomeServer homeServer, string roomId) { + _homeServer = homeServer; + _httpClient = homeServer._httpClient; + RoomId = roomId; + if (GetType() != typeof(SpaceRoom)) + AsSpace = new SpaceRoom(homeServer, RoomId); + } + + public string RoomId { get; set; } + + [Obsolete("", true)] + public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "") { + var url = $"/_matrix/client/v3/rooms/{RoomId}/state"; + if (!string.IsNullOrEmpty(type)) url += $"/{type}"; + if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}"; + return await _httpClient.GetFromJsonAsync<JsonElement>(url); + } + + public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() { + var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/state"); + var result = + JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(await res.Content.ReadAsStreamAsync()); + await foreach (var resp in result) { + yield return resp; + } + } + + public async Task<T?> GetStateAsync<T>(string type, string stateKey = "") { + var url = $"/_matrix/client/v3/rooms/{RoomId}/state"; + if (!string.IsNullOrEmpty(type)) url += $"/{type}"; + if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}"; + try { +#if DEBUG && false + var resp = await _httpClient.GetFromJsonAsync<JsonObject>(url); + try { + _homeServer._httpClient.PostAsJsonAsync( + "http://localhost:5116/validate/" + typeof(T).AssemblyQualifiedName, resp); + } + catch (Exception e) { + Console.WriteLine("[!!] Checking state response failed: " + e); + } + + return resp.Deserialize<T>(); +#else + var resp = await _httpClient.GetFromJsonAsync<T>(url); + return resp; +#endif + } + catch (MatrixException e) { + if (e is not { ErrorCode: "M_NOT_FOUND" }) { + throw; + } + + Console.WriteLine(e); + return default; + } + } + + 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}"; + var res = await _httpClient.GetFromJsonAsync<MessagesResponse>(url); + return res ?? new MessagesResponse(); + } + + public async Task<string> GetNameAsync() { + try { + var res = await GetStateAsync<RoomNameEventData>("m.room.name"); + return res?.Name ?? RoomId; + } + catch (MatrixException e) { + return $"{RoomId} ({e.ErrorCode})"; + } + } + + public async Task JoinAsync(string[]? homeservers = null, string? reason = null) { + 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] }; + var fullJoinUrl = $"{join_url}?server_name=" + string.Join("&server_name=", homeservers); + var res = await _httpClient.PostAsJsonAsync(fullJoinUrl, new { + reason + }); + } + + public async IAsyncEnumerable<StateEventResponse> GetMembersAsync(bool joinedOnly = true) { + var res = GetFullStateAsync(); + await foreach (var member in res) { + if (member.Type != "m.room.member") continue; + if (joinedOnly && (member.TypedContent as RoomMemberEventData).Membership is not "join") continue; + yield return member; + } + } + + public async Task<List<string>> GetAliasesAsync() { + var res = await GetStateAsync<RoomAliasEventData>("m.room.aliases"); + return res.Aliases; + } + + public async Task<CanonicalAliasEventData?> GetCanonicalAliasAsync() => + await GetStateAsync<CanonicalAliasEventData>("m.room.canonical_alias"); + + public async Task<RoomTopicEventData?> GetTopicAsync() => + await GetStateAsync<RoomTopicEventData>("m.room.topic"); + + public async Task<RoomAvatarEventData?> GetAvatarUrlAsync() => + await GetStateAsync<RoomAvatarEventData>("m.room.avatar"); + + public async Task<JoinRulesEventData> GetJoinRuleAsync() => + await GetStateAsync<JoinRulesEventData>("m.room.join_rules"); + + public async Task<HistoryVisibilityEventData?> GetHistoryVisibilityAsync() => + await GetStateAsync<HistoryVisibilityEventData>("m.room.history_visibility"); + + public async Task<GuestAccessEventData?> GetGuestAccessAsync() => + await GetStateAsync<GuestAccessEventData>("m.room.guest_access"); + + public async Task<RoomCreateEventData> GetCreateEventAsync() => + await GetStateAsync<RoomCreateEventData>("m.room.create"); + + public async Task<string?> GetRoomType() { + var res = await GetStateAsync<RoomCreateEventData>("m.room.create"); + return res.Type; + } + + public async Task ForgetAsync() => + await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null); + + public async Task LeaveAsync(string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new { + reason + }); + + public async Task KickAsync(string userId, string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", + new UserIdAndReason() { UserId = userId, Reason = reason }); + + public async Task BanAsync(string userId, string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", + new UserIdAndReason() { UserId = userId, Reason = reason }); + + public async Task UnbanAsync(string userId) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", + new UserIdAndReason() { UserId = userId }); + + public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) => + await (await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content)) + .Content.ReadFromJsonAsync<EventIdResponse>(); + + public async Task<EventIdResponse> SendMessageEventAsync(string eventType, RoomMessageEventData content) { + var res = await _httpClient.PutAsJsonAsync( + $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content); + var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>(); + return resu; + } + + public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) { + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(fileStream), "file", fileName); + var res = await + ( + await _httpClient.PutAsync( + $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), + content + ) + ) + .Content.ReadFromJsonAsync<EventIdResponse>(); + return res; + } + + public readonly SpaceRoom AsSpace; +} diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs new file mode 100644 index 0000000..ff2c228 --- /dev/null +++ b/LibMatrix/RoomTypes/SpaceRoom.cs @@ -0,0 +1,25 @@ +using LibMatrix.Extensions; + +namespace LibMatrix.RoomTypes; + +public class SpaceRoom : GenericRoom { + private readonly AuthenticatedHomeServer _homeServer; + private readonly GenericRoom _room; + + public SpaceRoom(AuthenticatedHomeServer homeServer, string roomId) : base(homeServer, roomId) { + _homeServer = homeServer; + } + + private static SemaphoreSlim _semaphore = new(1, 1); + public async IAsyncEnumerable<GenericRoom> GetRoomsAsync(bool includeRemoved = false) { + await _semaphore.WaitAsync(); + var rooms = new List<GenericRoom>(); + var state = GetFullStateAsync(); + await foreach (var stateEvent in state) { + if (stateEvent.Type != "m.space.child") continue; + if (stateEvent.RawContent.ToJson() != "{}" || includeRemoved) + yield return await _homeServer.GetRoom(stateEvent.StateKey); + } + _semaphore.Release(); + } +} diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs new file mode 100644 index 0000000..61c449a --- /dev/null +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -0,0 +1,96 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Responses; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Services; + +public class HomeserverProviderService { + private readonly TieredStorageService _tieredStorageService; + private readonly ILogger<HomeserverProviderService> _logger; + private readonly HomeserverResolverService _homeserverResolverService; + + public HomeserverProviderService(TieredStorageService tieredStorageService, + ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) { + Console.WriteLine("Homeserver provider service instantiated!"); + _tieredStorageService = tieredStorageService; + _logger = logger; + _homeserverResolverService = homeserverResolverService; + logger.LogDebug( + $"New HomeserverProviderService created with TieredStorageService<{string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name))}>!"); + } + + private static Dictionary<string, SemaphoreSlim> _authenticatedHomeserverSemaphore = new(); + private static Dictionary<string, AuthenticatedHomeServer> _authenticatedHomeServerCache = new(); + + public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken, + string? overrideFullDomain = null) { + SemaphoreSlim sem = _authenticatedHomeserverSemaphore.GetOrCreate(homeserver+accessToken, _ => new SemaphoreSlim(1, 1)); + await sem.WaitAsync(); + if (_authenticatedHomeServerCache.ContainsKey(homeserver+accessToken)) { + sem.Release(); + return _authenticatedHomeServerCache[homeserver+accessToken]; + } + + var hs = new AuthenticatedHomeServer(_tieredStorageService, homeserver, accessToken); + hs.FullHomeServerDomain = overrideFullDomain ?? + await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver); + hs._httpClient.Dispose(); + hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) }; + hs._httpClient.Timeout = TimeSpan.FromSeconds(120); + hs._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!; + + _authenticatedHomeServerCache[homeserver+accessToken] = hs; + sem.Release(); + + return hs; + } + + public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? overrideFullDomain = null) { + var hs = new RemoteHomeServer(homeserver); + hs.FullHomeServerDomain = overrideFullDomain ?? + await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver); + hs._httpClient.Dispose(); + hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) }; + hs._httpClient.Timeout = TimeSpan.FromSeconds(120); + return hs; + } + + public async Task<LoginResponse> Login(string homeserver, string user, string password, + string? overrideFullDomain = null) { + var hs = await GetRemoteHomeserver(homeserver, overrideFullDomain); + var payload = new LoginRequest { + Identifier = new() { User = user }, + Password = password + }; + var resp = await hs._httpClient.PostAsJsonAsync("/_matrix/client/v3/login", payload); + var data = await resp.Content.ReadFromJsonAsync<LoginResponse>(); + return data!; + } + + private class LoginRequest { + [JsonPropertyName("type")] + public string Type { get; set; } = "m.login.password"; + + [JsonPropertyName("identifier")] + public LoginIdentifier Identifier { get; set; } = new(); + + [JsonPropertyName("password")] + public string Password { get; set; } = ""; + + [JsonPropertyName("initial_device_display_name")] + public string InitialDeviceDisplayName { get; set; } = "Rory&::LibMatrix"; + + public class LoginIdentifier { + [JsonPropertyName("type")] + public string Type { get; set; } = "m.id.user"; + + [JsonPropertyName("user")] + public string User { get; set; } = ""; + } + } +} diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs new file mode 100644 index 0000000..4d3bc46 --- /dev/null +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using LibMatrix.Extensions; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Services; + +public class HomeserverResolverService { + private readonly MatrixHttpClient _httpClient = new(); + private readonly ILogger<HomeserverResolverService> _logger; + + private static readonly Dictionary<string, string> _wellKnownCache = new(); + private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new(); + + public HomeserverResolverService(ILogger<HomeserverResolverService> logger) { + _logger = logger; + } + + public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) { + var res = await _resolveHomeserverFromWellKnown(homeserver); + if (!res.StartsWith("http")) res = "https://" + res; + if (res.EndsWith(":443")) res = res.Substring(0, res.Length - 4); + return res; + } + + private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) { + if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); + SemaphoreSlim sem = _wellKnownSemaphores.GetOrCreate(homeserver, _ => new SemaphoreSlim(1, 1)); + await sem.WaitAsync(); + if (_wellKnownCache.ContainsKey(homeserver)) { + sem.Release(); + return _wellKnownCache[homeserver]; + } + + string? result = null; + _logger.LogInformation($"Attempting to resolve homeserver: {homeserver}"); + result ??= await _tryResolveFromClientWellknown(homeserver); + result ??= await _tryResolveFromServerWellknown(homeserver); + result ??= await _tryCheckIfDomainHasHomeserver(homeserver); + + if (result is not null) { + _logger.LogInformation($"Resolved homeserver: {homeserver} -> {result}"); + _wellKnownCache[homeserver] = result; + sem.Release(); + return result; + } + + throw new InvalidDataException($"Failed to resolve homeserver for {homeserver}! Is it online and configured correctly?"); + } + + private async Task<string?> _tryResolveFromClientWellknown(string homeserver) { + if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; + if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/client")) { + var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client"); + var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString(); + return hs; + } + + _logger.LogInformation("No client well-known..."); + return null; + } + + private async Task<string?> _tryResolveFromServerWellknown(string homeserver) { + if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; + if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/server")) { + var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/server"); + var hs = resp.GetProperty("m.server").GetString(); + return hs; + } + + _logger.LogInformation("No server well-known..."); + return null; + } + + private async Task<string?> _tryCheckIfDomainHasHomeserver(string homeserver) { + _logger.LogInformation($"Checking if {homeserver} hosts a homeserver..."); + if (await _httpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) + return homeserver; + _logger.LogInformation("No homeserver on shortname..."); + return null; + } + + private async Task<string?> _tryCheckIfSubDomainHasHomeserver(string homeserver, string subdomain) { + homeserver = homeserver.Replace("https://", $"https://{subdomain}."); + return await _tryCheckIfDomainHasHomeserver(homeserver); + } +} diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs new file mode 100644 index 0000000..96a1963 --- /dev/null +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; + +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("[MRUCore/DI] No TieredStorageService has been registered!"); + //Add config + if(config is not null) + services.AddSingleton(config); + else { + services.AddSingleton(new RoryLibMatrixConfiguration()); + } + //Add services + services.AddSingleton<HomeserverProviderService>(); + services.AddSingleton<HomeserverResolverService>(); + // services.AddScoped<MatrixHttpClient>(); + return services; + } + + +} + +public class RoryLibMatrixConfiguration { + public string AppName { get; set; } = "Rory&::LibMatrix"; +} diff --git a/LibMatrix/Services/TieredStorageService.cs b/LibMatrix/Services/TieredStorageService.cs new file mode 100644 index 0000000..954a2ce --- /dev/null +++ b/LibMatrix/Services/TieredStorageService.cs @@ -0,0 +1,13 @@ +using LibMatrix.Interfaces.Services; + +namespace LibMatrix.Services; + +public class TieredStorageService { + public IStorageProvider CacheStorageProvider { get; } + public IStorageProvider DataStorageProvider { get; } + + public TieredStorageService(IStorageProvider cacheStorageProvider, IStorageProvider dataStorageProvider) { + CacheStorageProvider = cacheStorageProvider; + DataStorageProvider = dataStorageProvider; + } +} diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs new file mode 100644 index 0000000..5efeaf5 --- /dev/null +++ b/LibMatrix/StateEvent.cs @@ -0,0 +1,117 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix; + +public class StateEvent { + public static List<Type> KnownStateEventTypes = + new ClassCollector<IStateEventType>().ResolveFromAllAccessibleAssemblies(); + + public static Type GetStateEventType(string type) { + if (type == "m.receipt") { + return typeof(Dictionary<string, JsonObject>); + } + + var eventType = KnownStateEventTypes.FirstOrDefault(x => + x.GetCustomAttributes<MatrixEventAttribute>()?.Any(y => y.EventName == type) ?? false); + + return eventType ?? typeof(object); + } + + public object TypedContent { + get { + try { + return RawContent.Deserialize(GetType)!; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + ObjectExtensions.ToJson(RawContent)); + } + + return null; + } + set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value)); + } + + [JsonPropertyName("state_key")] + public string StateKey { get; set; } = ""; + + private string _type; + + [JsonPropertyName("type")] + public string Type { + get => _type; + set { + _type = value; + // if (RawContent is not null && this is StateEventResponse stateEventResponse) { + // if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return; + // var x = GetType.Name; + // } + } + } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } + + private JsonObject? _rawContent; + + [JsonPropertyName("content")] + public JsonObject? RawContent { + get => _rawContent; + set { + _rawContent = value; + // if (Type is not null && this is StateEventResponse stateEventResponse) { + // if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return; + // var x = GetType.Name; + // } + } + } + + [JsonIgnore] + public Type GetType { + get { + var type = GetStateEventType(Type); + + //special handling for some types + // if (type == typeof(RoomEmotesEventData)) { + // RawContent["emote"] = RawContent["emote"]?.AsObject() ?? new JsonObject(); + // } + // + // if (this is StateEventResponse stateEventResponse) { + // if (type == null || type == typeof(object)) { + // Console.WriteLine($"Warning: unknown event type '{Type}'!"); + // Console.WriteLine(RawContent.ToJson()); + // Directory.CreateDirectory($"unknown_state_events/{Type}"); + // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json", + // RawContent.ToJson()); + // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json"); + // } + // else if (RawContent is not null && RawContent.FindExtraJsonObjectFields(type)) { + // Directory.CreateDirectory($"unknown_state_events/{Type}"); + // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json", + // RawContent.ToJson()); + // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json"); + // } + // } + + return type; + } + } + + //debug + public string dtype { + get { + var res = GetType().Name switch { + "StateEvent`1" => $"StateEvent", + _ => GetType().Name + }; + return res; + } + } + + public string cdtype => TypedContent.GetType().Name; +} diff --git a/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs new file mode 100644 index 0000000..808a0db --- /dev/null +++ b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Common; + +[MatrixEvent(EventName = "org.matrix.mjolnir.shortcode")] +public class MjolnirShortcodeEventData : IStateEventType { + [JsonPropertyName("shortcode")] + public string? Shortcode { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs new file mode 100644 index 0000000..af1c09e --- /dev/null +++ b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Common; + +[MatrixEvent(EventName = "im.ponies.room_emotes")] +public class RoomEmotesEventData : IStateEventType { + [JsonPropertyName("emoticons")] + public Dictionary<string, EmoticonData>? Emoticons { get; set; } + + [JsonPropertyName("images")] + public Dictionary<string, EmoticonData>? Images { get; set; } + + [JsonPropertyName("pack")] + public PackInfo? Pack { get; set; } + + public class EmoticonData { + [JsonPropertyName("url")] + public string? Url { get; set; } + } + + public class PackInfo { + + } +} diff --git a/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs new file mode 100644 index 0000000..36cc90e --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.canonical_alias")] +public class CanonicalAliasEventData : IStateEventType { + [JsonPropertyName("alias")] + public string? Alias { get; set; } + [JsonPropertyName("alt_aliases")] + public string[]? AltAliases { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs new file mode 100644 index 0000000..b6ddd93 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.guest_access")] +public class GuestAccessEventData : IStateEventType { + [JsonPropertyName("guest_access")] + public string GuestAccess { get; set; } + + public bool IsGuestAccessEnabled { + get => GuestAccess == "can_join"; + set => GuestAccess = value ? "can_join" : "forbidden"; + } +} diff --git a/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs new file mode 100644 index 0000000..8836fc0 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.history_visibility")] +public class HistoryVisibilityEventData : IStateEventType { + [JsonPropertyName("history_visibility")] + public string HistoryVisibility { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs new file mode 100644 index 0000000..0393395 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.join_rules")] +public class JoinRulesEventData : IStateEventType { + private static string Public = "public"; + private static string Invite = "invite"; + private static string Knock = "knock"; + + [JsonPropertyName("join_rule")] + public string JoinRule { get; set; } + + [JsonPropertyName("allow")] + public List<string> Allow { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs new file mode 100644 index 0000000..963864f --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.policy.rule.user")] +[MatrixEvent(EventName = "m.policy.rule.server")] +[MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")] +public class PolicyRuleStateEventData : IStateEventType { + /// <summary> + /// Entity this ban applies to, can use * and ? as globs. + /// </summary> + [JsonPropertyName("entity")] + public string Entity { get; set; } + + /// <summary> + /// Reason this user is banned + /// </summary> + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// <summary> + /// Suggested action to take + /// </summary> + [JsonPropertyName("recommendation")] + public string? Recommendation { get; set; } + + /// <summary> + /// Expiry time in milliseconds since the unix epoch, or null if the ban has no expiry. + /// </summary> + [JsonPropertyName("support.feline.policy.expiry.rev.2")] //stable prefix: expiry, msc pending + public long? Expiry { get; set; } + + //utils + /// <summary> + /// Readable expiry time, provided for easy interaction + /// </summary> + [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")] + public DateTime? ExpiryDateTime { + get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime; + set => Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); + } +} + +public static class PolicyRecommendationTypes { + /// <summary> + /// Ban this user + /// </summary> + public static string Ban = "m.ban"; + + /// <summary> + /// Mute this user + /// </summary> + public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending +} diff --git a/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs new file mode 100644 index 0000000..fa75a88 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.presence")] +public class PresenceStateEventData : IStateEventType { + [JsonPropertyName("presence")] + 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; } +} diff --git a/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs b/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs new file mode 100644 index 0000000..d2340f5 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +public class ProfileResponseEventData : IStateEventType { + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } = ""; + + [JsonPropertyName("displayname")] + public string? DisplayName { get; set; } = ""; +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs new file mode 100644 index 0000000..8d921b2 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.alias")] +public class RoomAliasEventData : IStateEventType { + [JsonPropertyName("aliases")] + public List<string>? Aliases { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs new file mode 100644 index 0000000..cbe41dd --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.avatar")] +public class RoomAvatarEventData : IStateEventType { + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("info")] + public RoomAvatarInfo? Info { get; set; } + + public class RoomAvatarInfo { + [JsonPropertyName("h")] + public int? Height { get; set; } + + [JsonPropertyName("w")] + public int? Width { get; set; } + + [JsonPropertyName("mimetype")] + public string? MimeType { get; set; } + + [JsonPropertyName("size")] + public int? Size { get; set; } + } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs new file mode 100644 index 0000000..b96c31e --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.create")] +public class RoomCreateEventData : IStateEventType { + [JsonPropertyName("room_version")] + public string? RoomVersion { get; set; } + [JsonPropertyName("creator")] + public string? Creator { get; set; } + [JsonPropertyName("m.federate")] + public bool? Federate { get; set; } + [JsonPropertyName("predecessor")] + public RoomCreatePredecessor? Predecessor { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } + + public class RoomCreatePredecessor { + [JsonPropertyName("room_id")] + public string? RoomId { get; set; } + + [JsonPropertyName("event_id")] + public string? EventId { get; set; } + } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs new file mode 100644 index 0000000..e16716e --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.encryption")] +public class RoomEncryptionEventData : IStateEventType { + [JsonPropertyName("algorithm")] + public string? Algorithm { get; set; } + [JsonPropertyName("rotation_period_ms")] + public ulong? RotationPeriodMs { get; set; } + [JsonPropertyName("rotation_period_msgs")] + public ulong? RotationPeriodMsgs { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs new file mode 100644 index 0000000..623c43c --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.member")] +public class RoomMemberEventData : IStateEventType { + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("membership")] + public string Membership { get; set; } = null!; + + [JsonPropertyName("displayname")] + public string? Displayname { get; set; } + + [JsonPropertyName("is_direct")] + public bool? IsDirect { get; set; } + + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("kind")] + public string? Kind { get; set; } + + [JsonPropertyName("join_authorised_via_users_server")] + public string? JoinAuthorisedViaUsersServer { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs new file mode 100644 index 0000000..14dd67a --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.message")] +public class RoomMessageEventData : IStateEventType { + [JsonPropertyName("body")] + public string Body { get; set; } + [JsonPropertyName("msgtype")] + public string MessageType { get; set; } = "m.notice"; + + [JsonPropertyName("formatted_body")] + public string FormattedBody { get; set; } + + [JsonPropertyName("format")] + public string Format { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs new file mode 100644 index 0000000..9d13513 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.name")] +public class RoomNameEventData : IStateEventType { + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs new file mode 100644 index 0000000..c7d29fa --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.pinned_events")] +public class RoomPinnedEventData : IStateEventType { + [JsonPropertyName("pinned")] + public string[]? PinnedEvents { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs new file mode 100644 index 0000000..c5dda78 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.power_levels")] +public class RoomPowerLevelEventData : IStateEventType { + [JsonPropertyName("ban")] + public int Ban { get; set; } // = 50; + + [JsonPropertyName("events_default")] + public int EventsDefault { get; set; } // = 0; + + [JsonPropertyName("events")] + public Dictionary<string, int> Events { get; set; } // = null!; + + [JsonPropertyName("invite")] + public int Invite { get; set; } // = 50; + + [JsonPropertyName("kick")] + public int Kick { get; set; } // = 50; + + [JsonPropertyName("notifications")] + public NotificationsPL NotificationsPl { get; set; } // = null!; + + [JsonPropertyName("redact")] + public int Redact { get; set; } // = 50; + + [JsonPropertyName("state_default")] + public int StateDefault { get; set; } // = 50; + + [JsonPropertyName("users")] + public Dictionary<string, int> Users { get; set; } // = null!; + + [JsonPropertyName("users_default")] + public int UsersDefault { get; set; } // = 0; + + [Obsolete("Historical was a key related to MSC2716, a spec change on backfill that was dropped!", true)] + [JsonIgnore] + [JsonPropertyName("historical")] + public int Historical { get; set; } // = 50; + + public class NotificationsPL { + [JsonPropertyName("room")] + public int Room { get; set; } = 50; + } + + public bool IsUserAdmin(string userId) { + return Users.TryGetValue(userId, out var level) && level >= Events.Max(x=>x.Value); + } + + public bool UserHasPermission(string userId, string eventType) { + return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault); + } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs new file mode 100644 index 0000000..0fd0df6 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.topic")] +[MatrixEvent(EventName = "org.matrix.msc3765.topic", Legacy = true)] +public class RoomTopicEventData : IStateEventType { + [JsonPropertyName("topic")] + public string? Topic { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs new file mode 100644 index 0000000..857338c --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.typing")] +public class RoomTypingEventData : IStateEventType { + [JsonPropertyName("user_ids")] + public string[]? UserIds { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs new file mode 100644 index 0000000..68bbe6b --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.room.server_acl")] +public class ServerACLEventData : IStateEventType { + [JsonPropertyName("allow")] + public List<string> Allow { get; set; } // = null!; + + [JsonPropertyName("deny")] + public List<string> Deny { get; set; } // = null!; + + [JsonPropertyName("allow_ip_literals")] + public bool AllowIpLiterals { get; set; } // = false; +} diff --git a/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs new file mode 100644 index 0000000..a55e941 --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.space.child")] +public class SpaceChildEventData : IStateEventType { + [JsonPropertyName("auto_join")] + public bool? AutoJoin { get; set; } + [JsonPropertyName("via")] + public string[]? Via { get; set; } + [JsonPropertyName("suggested")] + public bool? Suggested { get; set; } +} diff --git a/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs new file mode 100644 index 0000000..7dc7f4c --- /dev/null +++ b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Interfaces; + +namespace LibMatrix.StateEventTypes.Spec; + +[MatrixEvent(EventName = "m.space.parent")] +public class SpaceParentEventData : IStateEventType { + [JsonPropertyName("via")] + public string[]? Via { get; set; } + + [JsonPropertyName("canonical")] + public bool? Canonical { get; set; } +} diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/UserIdAndReason.cs new file mode 100644 index 0000000..a0c2acd --- /dev/null +++ b/LibMatrix/UserIdAndReason.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix; + +internal class UserIdAndReason { + [JsonPropertyName("user_id")] + public string UserId { get; set; } + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} |