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; }
+}
|