diff options
Diffstat (limited to 'MatrixRoomUtils.Core')
21 files changed, 453 insertions, 521 deletions
diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs index 368aa20..ee6be72 100644 --- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs +++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs @@ -9,63 +9,47 @@ using MatrixRoomUtils.Core.Responses.Admin; namespace MatrixRoomUtils.Core; -public class AuthenticatedHomeServer : IHomeServer -{ - public string UserId { get; set; } - public string AccessToken { get; set; } +public class AuthenticatedHomeServer : IHomeServer { public readonly HomeserverAdminApi Admin; - public AuthenticatedHomeServer(string userId, string accessToken, string canonicalHomeServerDomain) - { + public AuthenticatedHomeServer(string userId, string accessToken, string canonicalHomeServerDomain) { UserId = userId; AccessToken = accessToken; HomeServerDomain = canonicalHomeServerDomain; Admin = new HomeserverAdminApi(this); - _httpClient = new HttpClient(); + _httpClient = new MatrixHttpClient(); } - public async Task<AuthenticatedHomeServer> Configure() - { + public string UserId { get; set; } + public string AccessToken { get; set; } + + public async Task<AuthenticatedHomeServer> Configure() { FullHomeServerDomain = await ResolveHomeserverFromWellKnown(HomeServerDomain); _httpClient.Dispose(); - _httpClient = new HttpClient { BaseAddress = new Uri(FullHomeServerDomain) }; + _httpClient = new MatrixHttpClient { BaseAddress = new Uri(FullHomeServerDomain) }; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); Console.WriteLine("[AHS] Finished setting up http client"); return this; } - public async Task<Room> GetRoom(string roomId) - { - return new Room(_httpClient, roomId); - } + public async Task<Room> GetRoom(string roomId) => new Room(_httpClient, roomId); - public async Task<List<Room>> GetJoinedRooms() - { + public async Task<List<Room>> GetJoinedRooms() { var rooms = new List<Room>(); var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms"); - if (!roomQuery.IsSuccessStatusCode) - { - Console.WriteLine($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}"); - throw new InvalidDataException($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}"); - } var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>(); - foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) - { - rooms.Add(new Room(_httpClient, room.GetString())); - } + foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new Room(_httpClient, room.GetString())); Console.WriteLine($"Fetched {rooms.Count} rooms"); return rooms; } - public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") - { + public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") { var res = await _httpClient.PostAsync($"/_matrix/media/r0/upload?filename={fileName}", new StreamContent(fileStream)); - if (!res.IsSuccessStatusCode) - { + if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); throw new InvalidDataException($"Failed to upload file: {await res.Content.ReadAsStringAsync()}"); } @@ -74,11 +58,9 @@ public class AuthenticatedHomeServer : IHomeServer return resJson.GetProperty("content_uri").GetString()!; } - public async Task<Room> CreateRoom(CreateRoomRequest creationEvent) - { + public async Task<Room> CreateRoom(CreateRoomRequest creationEvent) { var res = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/createRoom", creationEvent); - if (!res.IsSuccessStatusCode) - { + if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); throw new InvalidDataException($"Failed to create room: {await res.Content.ReadAsStringAsync()}"); } @@ -86,47 +68,34 @@ public class AuthenticatedHomeServer : IHomeServer return await GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString()); } - public class HomeserverAdminApi - { + public class HomeserverAdminApi { private readonly AuthenticatedHomeServer _authenticatedHomeServer; - public HomeserverAdminApi(AuthenticatedHomeServer authenticatedHomeServer) - { - _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, string? contentSearch = null) - { + public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", string? searchTerm = null, string? contentSearch = null) { AdminRoomListingResult? res = null; - int i = 0; + var i = 0; int? totalRooms = null; - do - { + 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 (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; + + if (res?.NextBatch != null) url += $"&from={res.NextBatch}"; - if (res?.NextBatch != 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(indent:false)); - foreach (var room in res.Rooms) - { + Console.WriteLine(res.ToJson(false)); + foreach (var room in res.Rooms) { if (contentSearch != 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; } diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs index b4b8d19..0f9eb58 100644 --- a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs +++ b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs @@ -5,35 +5,31 @@ using MatrixRoomUtils.Core.Responses; namespace MatrixRoomUtils.Core.Authentication; -public class MatrixAuth -{ - public static async Task<LoginResponse> Login(string homeserver, string username, string password) - { +public class MatrixAuth { + public static async Task<LoginResponse> Login(string homeserver, string username, string password) { Console.WriteLine($"Logging in to {homeserver} as {username}..."); homeserver = (await new RemoteHomeServer(homeserver).Configure()).FullHomeServerDomain; var hc = new HttpClient(); - var payload = new - { + var payload = new { type = "m.login.password", - identifier = new - { + identifier = new { type = "m.id.user", user = username }, - password = password, + password, initial_device_display_name = "Rory&::MatrixRoomUtils" }; Console.WriteLine($"Sending login request to {homeserver}..."); var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/r0/login", payload); Console.WriteLine($"Login: {resp.StatusCode}"); var data = await resp.Content.ReadFromJsonAsync<JsonElement>(); - if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToString()); - if (data.TryGetProperty("retry_after_ms", out var retryAfter)) - { + if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data); + if (data.TryGetProperty("retry_after_ms", out var retryAfter)) { Console.WriteLine($"Login: Waiting {retryAfter.GetInt32()}ms before retrying"); await Task.Delay(retryAfter.GetInt32()); return await Login(homeserver, username, password); } + Console.WriteLine($"Login: {data.ToJson()}"); return data.Deserialize<LoginResponse>(); //var token = data.GetProperty("access_token").GetString(); @@ -41,20 +37,16 @@ public class MatrixAuth } [Obsolete("Migrate to IHomeServer instance")] - public static async Task<ProfileResponse> GetProfile(string homeserver, string mxid) => - await (await new RemoteHomeServer(homeserver).Configure()).GetProfile(mxid); + public static async Task<ProfileResponse> GetProfile(string homeserver, string mxid) => await (await new RemoteHomeServer(homeserver).Configure()).GetProfile(mxid); - private static async Task<bool> CheckSuccessStatus(string url) - { + private static async Task<bool> CheckSuccessStatus(string url) { //cors causes failure, try to catch - try - { + try { using var hc = new HttpClient(); var resp = await hc.GetAsync(url); return resp.IsSuccessStatusCode; } - catch (Exception e) - { + catch (Exception e) { Console.WriteLine($"Failed to check success status: {e.Message}"); return false; } diff --git a/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs b/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs index cce71dd..c51baec 100644 --- a/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/DictionaryExtensions.cs @@ -1,15 +1,13 @@ namespace MatrixRoomUtils.Core.Extensions; -public static class DictionaryExtensions -{ - public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, - TKey oldKey, TKey newKey) - { +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 + dict[newKey] = value; // or dict.Add(newKey, value) depending on ur comfort return true; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs index 8eb0226..47b3121 100644 --- a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs @@ -1,19 +1,40 @@ +using System.Text.Json; + namespace MatrixRoomUtils.Core.Extensions; -public static class HttpClientExtensions -{ - public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url) - { +public static class HttpClientExtensions { + public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url) { //cors causes failure, try to catch - try - { + try { var resp = await hc.GetAsync(url); return resp.IsSuccessStatusCode; } - catch (Exception e) - { + 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) { + var a = await base.SendAsync(request, cancellationToken); + if (!a.IsSuccessStatusCode) { + Console.WriteLine($"Failed to send request: {a.StatusCode}"); + var content = await a.Content.ReadAsStringAsync(cancellationToken); + if (content.StartsWith('{')) { + var ex = JsonSerializer.Deserialize<MatrixException>(content); + if (ex?.RetryAfterMs != null) { + await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); + return await SendAsync(request, cancellationToken); + } + + throw ex!; + } + + throw new InvalidDataException("Encountered invalid data:\n" + content); + } + + return a; + } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs index 725c832..b007136 100644 --- a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs @@ -5,18 +5,16 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core.Extensions; -public static class JsonElementExtensions -{ - public static void FindExtraJsonFields([DisallowNull] this JsonElement? res, Type t) - { +public static class JsonElementExtensions { + public static void FindExtraJsonFields([DisallowNull] this JsonElement? res, Type t) { var props = t.GetProperties(); var unknownPropertyFound = false; - foreach (var field in res.Value.EnumerateObject()) - { + foreach (var field in res.Value.EnumerateObject()) { if (props.Any(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name == field.Name)) continue; Console.WriteLine($"[!!] Unknown property {field.Name} in {t.Name}!"); unknownPropertyFound = true; } - if(unknownPropertyFound) Console.WriteLine(res.Value.ToJson()); + + if (unknownPropertyFound) Console.WriteLine(res.Value.ToJson()); } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs index 5aa9645..812c81c 100644 --- a/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs @@ -1,15 +1,14 @@ +using System.Text.Encodings.Web; using System.Text.Json; namespace MatrixRoomUtils.Core.Extensions; -public static class ObjectExtensions -{ - public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false, bool unsafeContent = false) - { +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 = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + if (indent) jso.WriteIndented = true; + if (ignoreNull) jso.IgnoreNullValues = true; + if (unsafeContent) jso.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; return JsonSerializer.Serialize(obj, jso); } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Extensions/StringExtensions.cs b/MatrixRoomUtils.Core/Extensions/StringExtensions.cs index 8fadc6d..b81d59f 100644 --- a/MatrixRoomUtils.Core/Extensions/StringExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/StringExtensions.cs @@ -1,7 +1,6 @@ namespace MatrixRoomUtils.Core.Extensions; -public static class StringExtensions -{ +public static class StringExtensions { // public static async Task<string> GetMediaUrl(this string MxcUrl) // { // //MxcUrl: mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg @@ -11,5 +10,4 @@ public static class StringExtensions // var mediaId = MxcUrl.Split('/')[3]; // return $"{(await new RemoteHomeServer(server).Configure()).FullHomeServerDomain}/_matrix/media/v3/download/{server}/{mediaId}"; // } - } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs index 3ae1355..9a9ba7a 100644 --- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs +++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs @@ -5,35 +5,32 @@ using MatrixRoomUtils.Core.Responses; namespace MatrixRoomUtils.Core.Interfaces; -public class IHomeServer -{ - private Dictionary<string, ProfileResponse?> _profileCache = new(); +public class IHomeServer { + private readonly Dictionary<string, ProfileResponse?> _profileCache = new(); public string HomeServerDomain { get; set; } public string FullHomeServerDomain { get; set; } - private protected HttpClient _httpClient { get; set; } = new(); + private protected MatrixHttpClient _httpClient { get; set; } = new(); - public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) - { + 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); + 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 (RuntimeCache.HomeserverResolutionCache.Count == 0) - { + + private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) { + if (RuntimeCache.HomeserverResolutionCache.Count == 0) { Console.WriteLine("No cached homeservers, resolving..."); await Task.Delay(Random.Shared.Next(1000, 5000)); - } - if (RuntimeCache.HomeserverResolutionCache.ContainsKey(homeserver)) - { - if (RuntimeCache.HomeserverResolutionCache[homeserver].ResolutionTime < DateTime.Now.AddHours(1)) - { + } + + if (RuntimeCache.HomeserverResolutionCache.ContainsKey(homeserver)) { + if (RuntimeCache.HomeserverResolutionCache[homeserver].ResolutionTime < DateTime.Now.AddHours(1)) { Console.WriteLine($"Found cached homeserver: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}"); return RuntimeCache.HomeserverResolutionCache[homeserver].Result; } + Console.WriteLine($"Cached homeserver expired, removing: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}"); RuntimeCache.HomeserverResolutionCache.Remove(homeserver); } @@ -42,29 +39,25 @@ public class IHomeServer string result = null; Console.WriteLine($"Resolving homeserver: {homeserver}"); if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; - if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/client")) - { - Console.WriteLine($"Got successful response for client well-known..."); + if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/client")) { + Console.WriteLine("Got successful response for client well-known..."); var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client"); Console.WriteLine($"Response: {resp.ToString()}"); var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString(); result = hs; } - else - { - Console.WriteLine($"No client well-known..."); - if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/server")) - { + else { + Console.WriteLine("No client well-known..."); + 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(); result = hs; } - else - { - Console.WriteLine($"No server well-known..."); - if (await _httpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) result = homeserver; - else - { + else { + Console.WriteLine("No server well-known..."); + if (await _httpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) + result = homeserver; + else { Console.WriteLine("No homeserver on shortname..."); if (await _httpClient.CheckSuccessStatus($"{homeserver.Replace("//", "//matrix.")}/_matrix/client/versions")) result = homeserver.Replace("//", "//matrix."); else Console.WriteLine($"Failed to resolve homeserver, not on {homeserver}, nor do client or server well-knowns exist!"); @@ -72,30 +65,27 @@ public class IHomeServer } } - if (result != null) - { + if (result != null) { Console.WriteLine($"Resolved homeserver: {homeserver} -> {result}"); - RuntimeCache.HomeserverResolutionCache.TryAdd(homeserver, new() - { + RuntimeCache.HomeserverResolutionCache.TryAdd(homeserver, new HomeServerResolutionResult { Result = result, ResolutionTime = DateTime.Now }); return result; } + throw new InvalidDataException($"Failed to resolve homeserver, not on {homeserver}, nor do client or server well-knowns exist!"); } - public async Task<ProfileResponse> GetProfile(string mxid, bool debounce = false, bool cache = true) - { - if (cache) - { - if(debounce) await Task.Delay(Random.Shared.Next(100, 500)); - if (_profileCache.ContainsKey(mxid)) - { - while (_profileCache[mxid] == null) - { + + public async Task<ProfileResponse> GetProfile(string mxid, bool debounce = false, bool cache = true) { + if (cache) { + if (debounce) await Task.Delay(Random.Shared.Next(100, 500)); + if (_profileCache.ContainsKey(mxid)) { + while (_profileCache[mxid] == null) { Console.WriteLine($"Waiting for profile cache for {mxid}, currently {_profileCache[mxid]?.ToJson() ?? "null"} within {_profileCache.Count} profiles..."); await Task.Delay(Random.Shared.Next(50, 500)); } + return _profileCache[mxid]; } } @@ -103,13 +93,11 @@ public class IHomeServer _profileCache.Add(mxid, null); var resp = await _httpClient.GetAsync($"/_matrix/client/r0/profile/{mxid}"); var data = await resp.Content.ReadFromJsonAsync<JsonElement>(); - if(!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data.ToString()); + if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); var profile = data.Deserialize<ProfileResponse>(); _profileCache[mxid] = profile; return profile; } - public string? ResolveMediaUri(string mxc) - { - return mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/"); - } + + public string? ResolveMediaUri(string mxc) => mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/"); } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/MatrixException.cs b/MatrixRoomUtils.Core/MatrixException.cs new file mode 100644 index 0000000..3df70e1 --- /dev/null +++ b/MatrixRoomUtils.Core/MatrixException.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; +using MatrixRoomUtils.Core.Extensions; + +namespace MatrixRoomUtils.Core; + +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 override string Message => + 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() + }; +} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/RatelimitedHttpClient.cs b/MatrixRoomUtils.Core/RatelimitedHttpClient.cs deleted file mode 100644 index 61eab07..0000000 --- a/MatrixRoomUtils.Core/RatelimitedHttpClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MatrixRoomUtils.Core; - -public class RatelimitedHttpClient : HttpClient -{ - - -} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/RemoteHomeServer.cs b/MatrixRoomUtils.Core/RemoteHomeServer.cs index 942f873..3f50d2e 100644 --- a/MatrixRoomUtils.Core/RemoteHomeServer.cs +++ b/MatrixRoomUtils.Core/RemoteHomeServer.cs @@ -1,50 +1,39 @@ using System.Net.Http.Json; using System.Text.Json; +using MatrixRoomUtils.Core.Extensions; using MatrixRoomUtils.Core.Interfaces; namespace MatrixRoomUtils.Core; -public class RemoteHomeServer : IHomeServer -{ - - - public RemoteHomeServer(string canonicalHomeServerDomain) - { +public class RemoteHomeServer : IHomeServer { + public RemoteHomeServer(string canonicalHomeServerDomain) { HomeServerDomain = canonicalHomeServerDomain; - _httpClient = new HttpClient(); + _httpClient = new MatrixHttpClient(); _httpClient.Timeout = TimeSpan.FromSeconds(5); } - public async Task<RemoteHomeServer> Configure() - { + + public async Task<RemoteHomeServer> Configure() { FullHomeServerDomain = await ResolveHomeserverFromWellKnown(HomeServerDomain); _httpClient.Dispose(); - _httpClient = new HttpClient { BaseAddress = new Uri(FullHomeServerDomain) }; + _httpClient = new MatrixHttpClient { BaseAddress = new Uri(FullHomeServerDomain) }; _httpClient.Timeout = TimeSpan.FromSeconds(5); Console.WriteLine("[RHS] Finished setting up http client"); return this; } - - public async Task<Room> GetRoom(string roomId) - { - return new Room(_httpClient, roomId); - } - public async Task<List<Room>> GetJoinedRooms() - { + public async Task<Room> GetRoom(string roomId) => new Room(_httpClient, roomId); + + public async Task<List<Room>> GetJoinedRooms() { var rooms = new List<Room>(); var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms"); - if (!roomQuery.IsSuccessStatusCode) - { + if (!roomQuery.IsSuccessStatusCode) { Console.WriteLine($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}"); throw new InvalidDataException($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}"); } var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>(); - foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) - { - rooms.Add(new Room(_httpClient, room.GetString())); - } + foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new Room(_httpClient, room.GetString())); return rooms; } diff --git a/MatrixRoomUtils.Core/Responses/Admin/AdminRoomListingResult.cs b/MatrixRoomUtils.Core/Responses/Admin/AdminRoomListingResult.cs index 8ec0e4f..37bb3ba 100644 --- a/MatrixRoomUtils.Core/Responses/Admin/AdminRoomListingResult.cs +++ b/MatrixRoomUtils.Core/Responses/Admin/AdminRoomListingResult.cs @@ -2,8 +2,7 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core.Responses.Admin; -public class AdminRoomListingResult -{ +public class AdminRoomListingResult { [JsonPropertyName("offset")] public int Offset { get; set; } @@ -19,8 +18,7 @@ public class AdminRoomListingResult [JsonPropertyName("rooms")] public List<AdminRoomListingResultRoom> Rooms { get; set; } = new(); - public class AdminRoomListingResultRoom - { + public class AdminRoomListingResultRoom { [JsonPropertyName("room_id")] public string RoomId { get; set; } diff --git a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs index 5df99f7..da7d569 100644 --- a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs +++ b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs @@ -6,58 +6,56 @@ using MatrixRoomUtils.Core.Extensions; namespace MatrixRoomUtils.Core.Responses; -public class CreateRoomRequest -{ - [JsonPropertyName("name")] public string Name { get; set; } = null!; +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!; + [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("initial_state")] + public List<StateEvent> InitialState { get; set; } = null!; + + [JsonPropertyName("visibility")] + public string Visibility { get; set; } = null!; [JsonPropertyName("power_level_content_override")] public PowerLevelEvent PowerLevelContentOverride { get; set; } = null!; - [JsonPropertyName("creation_content")] public JsonObject CreationContent { get; set; } = new(); + [JsonPropertyName("creation_content")] + public JsonObject CreationContent { get; set; } = new(); /// <summary> - /// For use only when you can't use the CreationContent property + /// For use only when you can't use the CreationContent property /// </summary> - public StateEvent this[string event_type, string event_key = ""] - { + public StateEvent this[string event_type, string event_key = ""] { get => InitialState.First(x => x.Type == event_type && x.StateKey == event_key); - set - { + 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; - } } } //extra properties [JsonIgnore] - public string HistoryVisibility - { - get - { + public string HistoryVisibility { + get { var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility"); - if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + if (stateEvent == null) { + InitialState.Add(new StateEvent { Type = "m.room.history_visibility", - Content = new JsonObject() - { + Content = new JsonObject { ["history_visibility"] = "shared" } }); @@ -66,22 +64,16 @@ public class CreateRoomRequest return stateEvent.ContentAsJsonNode["history_visibility"].GetValue<string>(); } - set - { + set { var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility"); if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + InitialState.Add(new StateEvent { Type = "m.room.history_visibility", - Content = new JsonObject() - { + Content = new JsonObject { ["history_visibility"] = value } }); - } - else - { + else { var v = stateEvent.ContentAsJsonNode; v["history_visibility"] = value; stateEvent.ContentAsJsonNode = v; @@ -90,18 +82,13 @@ public class CreateRoomRequest } [JsonIgnore] - public string RoomIcon - { - get - { + public string RoomIcon { + get { var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar"); - if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + if (stateEvent == null) { + InitialState.Add(new StateEvent { Type = "m.room.avatar", - Content = new JsonObject() - { + Content = new JsonObject { ["url"] = "" } }); @@ -110,22 +97,16 @@ public class CreateRoomRequest return stateEvent.ContentAsJsonNode["url"].GetValue<string>(); } - set - { + set { var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar"); if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + InitialState.Add(new StateEvent { Type = "m.room.avatar", - Content = new JsonObject() - { + Content = new JsonObject { ["url"] = value } }); - } - else - { + else { var v = stateEvent.ContentAsJsonNode; v["url"] = value; stateEvent.ContentAsJsonNode = v; @@ -177,64 +158,50 @@ public class CreateRoomRequest // } // } - public ServerACL ServerACLs - { - get - { + public ServerACL ServerACLs { + get { var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl"); - if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + if (stateEvent == null) { + InitialState.Add(new StateEvent { Type = "m.room.server_acl", - Content = new JsonObject() - { - ["allow"] = new JsonArray() - { + Content = new JsonObject { + ["allow"] = new JsonArray { "*" }, ["deny"] = new JsonArray() } }); - return new ServerACL() - { - Allow = new List<string>() - { + return new ServerACL { + Allow = new List<string> { "*" }, Deny = new List<string>(), AllowIpLiterals = true }; } - return new ServerACL() - { - Allow = JsonSerializer.Deserialize<List<string>>(stateEvent.ContentAsJsonNode["allow"]), - Deny = JsonSerializer.Deserialize<List<string>>(stateEvent.ContentAsJsonNode["deny"]), + + return new ServerACL { + Allow = stateEvent.ContentAsJsonNode["allow"].Deserialize<List<string>>(), + Deny = stateEvent.ContentAsJsonNode["deny"].Deserialize<List<string>>(), AllowIpLiterals = true }; } - set - { + set { Console.WriteLine($"Setting server acl to {value.ToJson()}"); var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl"); if (stateEvent == null) - { - InitialState.Add(new StateEvent() - { + InitialState.Add(new StateEvent { Type = "m.room.server_acl", - Content = new JsonObject() - { - ["allow"] = JsonArray.Parse(JsonSerializer.Serialize(value.Allow)), - ["deny"] = JsonArray.Parse(JsonSerializer.Serialize(value.Deny)) - ["allow_ip_literals"] = value.AllowIpLiterals + Content = new JsonObject { + ["allow"] = JsonNode.Parse(JsonSerializer.Serialize(value.Allow)), + ["deny"] = JsonNode.Parse(JsonSerializer.Serialize(value.Deny)) + ["allow_ip_literals"] = value.AllowIpLiterals } }); - } - else - { + else { var v = stateEvent.ContentAsJsonNode; - v["allow"] = JsonArray.Parse(JsonSerializer.Serialize(value.Allow)); - v["deny"] = JsonArray.Parse(JsonSerializer.Serialize(value.Deny)); + v["allow"] = JsonNode.Parse(JsonSerializer.Serialize(value.Allow)); + v["deny"] = JsonNode.Parse(JsonSerializer.Serialize(value.Deny)); v["allow_ip_literals"] = value.AllowIpLiterals; stateEvent.ContentAsJsonNode = v; Console.WriteLine($"v={v.ToJson()}"); @@ -243,14 +210,7 @@ public class CreateRoomRequest } } - - [JsonIgnore] public CreationContentBaseType _creationContentBaseType; - - public CreateRoomRequest() => _creationContentBaseType = new(this); - - - public Dictionary<string, string> Validate() - { + 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."); @@ -259,49 +219,65 @@ public class CreateRoomRequest } } -public class CreationContentBaseType -{ +public class CreationContentBaseType { private readonly CreateRoomRequest createRoomRequest; - public CreationContentBaseType(CreateRoomRequest createRoomRequest) - { - this.createRoomRequest = createRoomRequest; - } + public CreationContentBaseType(CreateRoomRequest createRoomRequest) => this.createRoomRequest = createRoomRequest; [JsonPropertyName("type")] - public string Type - { + public string Type { get => (string)createRoomRequest.CreationContent["type"]; - set - { + set { if (value is "null" or "") createRoomRequest.CreationContent.Remove("type"); else createRoomRequest.CreationContent["type"] = value; } } } -public class PowerLevelEvent -{ - [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; +public class PowerLevelEvent { + [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; } -public class NotificationsPL -{ - [JsonPropertyName("room")] public int Room { get; set; } = 50; +public class NotificationsPL { + [JsonPropertyName("room")] + public int Room { get; set; } = 50; } -public class ServerACL -{ - [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; +public class ServerACL { + [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; } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Responses/LoginResponse.cs b/MatrixRoomUtils.Core/Responses/LoginResponse.cs index 34b42d1..3259e44 100644 --- a/MatrixRoomUtils.Core/Responses/LoginResponse.cs +++ b/MatrixRoomUtils.Core/Responses/LoginResponse.cs @@ -4,27 +4,26 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core.Responses; -public class LoginResponse -{ +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; } - - public async Task<ProfileResponse> GetProfile() - { + + public async Task<ProfileResponse> GetProfile() { var hc = new HttpClient(); var resp = await hc.GetAsync($"{HomeServer}/_matrix/client/r0/profile/{UserId}"); var data = await resp.Content.ReadFromJsonAsync<JsonElement>(); - if(!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data.ToString()); + if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); return data.Deserialize<ProfileResponse>(); } - public async Task<string> GetCanonicalHomeserverUrl() - { - return (await new RemoteHomeServer(HomeServer).Configure()).FullHomeServerDomain; - } + + public async Task<string> GetCanonicalHomeserverUrl() => (await new RemoteHomeServer(HomeServer).Configure()).FullHomeServerDomain; } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Responses/ProfileResponse.cs b/MatrixRoomUtils.Core/Responses/ProfileResponse.cs index 2c0b679..db72386 100644 --- a/MatrixRoomUtils.Core/Responses/ProfileResponse.cs +++ b/MatrixRoomUtils.Core/Responses/ProfileResponse.cs @@ -2,10 +2,10 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core.Responses; -public class ProfileResponse -{ +public class ProfileResponse { [JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } = ""; + [JsonPropertyName("displayname")] public string? DisplayName { get; set; } = ""; } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs index 670c121..36f0a36 100644 --- a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs +++ b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs @@ -2,43 +2,47 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core; -public class StateEventResponse : StateEvent -{ +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; } + [JsonPropertyName("prev_content")] public dynamic PrevContent { get; set; } - - - public class UnsignedData - { + + public class UnsignedData { [JsonPropertyName("age")] public ulong Age { get; set; } + [JsonPropertyName("prev_content")] public dynamic? PrevContent { get; set; } + [JsonPropertyName("redacted_because")] public dynamic? RedactedBecause { get; set; } + [JsonPropertyName("transaction_id")] public string? TransactionId { get; set; } - } } -public class StateEventResponse<T> : StateEventResponse where T : class -{ - +public class StateEventResponse<T> : StateEventResponse where T : class { [JsonPropertyName("content")] public T Content { get; set; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Room.cs b/MatrixRoomUtils.Core/Room.cs index f228271..a867c0c 100644 --- a/MatrixRoomUtils.Core/Room.cs +++ b/MatrixRoomUtils.Core/Room.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; using System.Net.Http.Json; -using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Web; @@ -8,26 +6,23 @@ using MatrixRoomUtils.Core.Extensions; namespace MatrixRoomUtils.Core; -public class Room -{ +public class Room { private readonly HttpClient _httpClient; - public string RoomId { get; set; } - public Room(HttpClient httpClient, string roomId) - { + public Room(HttpClient httpClient, string roomId) { _httpClient = httpClient; RoomId = roomId; } - public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "", bool logOnFailure = true) - { + public string RoomId { get; set; } + + public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "", bool logOnFailure = true) { var url = $"/_matrix/client/v3/rooms/{RoomId}/state"; if (!string.IsNullOrEmpty(type)) url += $"/{type}"; if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}"; var res = await _httpClient.GetAsync(url); - if (!res.IsSuccessStatusCode) - { + if (!res.IsSuccessStatusCode) { if (logOnFailure) Console.WriteLine($"{RoomId}/{stateKey}/{type} - got status: {res.StatusCode}"); return null; } @@ -36,20 +31,17 @@ public class Room return result; } - public async Task<T?> GetStateAsync<T>(string type, string stateKey = "", bool logOnFailure = false) - { + public async Task<T?> GetStateAsync<T>(string type, string stateKey = "", bool logOnFailure = false) { var res = await GetStateAsync(type, stateKey, logOnFailure); if (res == null) return default; return res.Value.Deserialize<T>(); } - - public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b", string filter = "") - { + + public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b", string filter = "") { var url = $"/_matrix/client/r0/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}"; if (!string.IsNullOrEmpty(filter)) url += $"&filter={filter}"; var res = await _httpClient.GetAsync(url); - if (!res.IsSuccessStatusCode) - { + if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to get messages for {RoomId} - got status: {res.StatusCode}"); throw new Exception($"Failed to get messages for {RoomId} - got status: {res.StatusCode}"); } @@ -58,11 +50,9 @@ public class Room return result ?? new MessagesResponse(); } - public async Task<string> GetNameAsync() - { + public async Task<string> GetNameAsync() { var res = await GetStateAsync("m.room.name"); - if (!res.HasValue) - { + if (!res.HasValue) { Console.WriteLine($"Room {RoomId} has no name!"); return RoomId; } @@ -72,22 +62,19 @@ public class Room return resn; } - public async Task JoinAsync(string[]? homeservers = null) - { - string join_url = $"/_matrix/client/r0/join/{HttpUtility.UrlEncode(RoomId)}"; + public async Task JoinAsync(string[]? homeservers = null) { + var join_url = $"/_matrix/client/r0/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.PostAsync(fullJoinUrl, null); } - public async Task<List<string>> GetMembersAsync(bool joinedOnly = true) - { + public async Task<List<string>> GetMembersAsync(bool joinedOnly = true) { var res = await GetStateAsync(""); if (!res.HasValue) return new List<string>(); var members = new List<string>(); - foreach (var member in res.Value.EnumerateArray()) - { + foreach (var member in res.Value.EnumerateArray()) { if (member.GetProperty("type").GetString() != "m.room.member") continue; if (joinedOnly && member.GetProperty("content").GetProperty("membership").GetString() != "join") continue; var memberId = member.GetProperty("state_key").GetString(); @@ -97,63 +84,52 @@ public class Room return members; } - public async Task<List<string>> GetAliasesAsync() - { + public async Task<List<string>> GetAliasesAsync() { var res = await GetStateAsync("m.room.aliases"); if (!res.HasValue) return new List<string>(); var aliases = new List<string>(); - foreach (var alias in res.Value.GetProperty("aliases").EnumerateArray()) - { - aliases.Add(alias.GetString() ?? ""); - } + foreach (var alias in res.Value.GetProperty("aliases").EnumerateArray()) aliases.Add(alias.GetString() ?? ""); return aliases; } - public async Task<string> GetCanonicalAliasAsync() - { + public async Task<string> GetCanonicalAliasAsync() { var res = await GetStateAsync("m.room.canonical_alias"); if (!res.HasValue) return ""; return res.Value.GetProperty("alias").GetString() ?? ""; } - public async Task<string> GetTopicAsync() - { + public async Task<string> GetTopicAsync() { var res = await GetStateAsync("m.room.topic"); if (!res.HasValue) return ""; return res.Value.GetProperty("topic").GetString() ?? ""; } - public async Task<string> GetAvatarUrlAsync() - { + public async Task<string> GetAvatarUrlAsync() { var res = await GetStateAsync("m.room.avatar"); if (!res.HasValue) return ""; return res.Value.GetProperty("url").GetString() ?? ""; } - public async Task<JoinRules> GetJoinRuleAsync() - { + public async Task<JoinRules> GetJoinRuleAsync() { var res = await GetStateAsync("m.room.join_rules"); if (!res.HasValue) return new JoinRules(); return res.Value.Deserialize<JoinRules>() ?? new JoinRules(); } - public async Task<string> GetHistoryVisibilityAsync() - { + public async Task<string> GetHistoryVisibilityAsync() { var res = await GetStateAsync("m.room.history_visibility"); if (!res.HasValue) return ""; return res.Value.GetProperty("history_visibility").GetString() ?? ""; } - public async Task<string> GetGuestAccessAsync() - { + public async Task<string> GetGuestAccessAsync() { var res = await GetStateAsync("m.room.guest_access"); if (!res.HasValue) return ""; return res.Value.GetProperty("guest_access").GetString() ?? ""; } - public async Task<CreateEvent> GetCreateEventAsync() - { + public async Task<CreateEvent> GetCreateEventAsync() { var res = await GetStateAsync("m.room.create"); if (!res.HasValue) return new CreateEvent(); @@ -163,33 +139,45 @@ public class Room } } -public class MessagesResponse -{ +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(); } -public class CreateEvent -{ - [JsonPropertyName("creator")] public string Creator { get; set; } - [JsonPropertyName("room_version")] public string RoomVersion { get; set; } - [JsonPropertyName("type")] public string? Type { get; set; } - [JsonPropertyName("predecessor")] public object? Predecessor { get; set; } - [JsonPropertyName("m.federate")] public bool Federate { get; set; } +public class CreateEvent { + [JsonPropertyName("creator")] + public string Creator { get; set; } + + [JsonPropertyName("room_version")] + public string RoomVersion { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("predecessor")] + public object? Predecessor { get; set; } + + [JsonPropertyName("m.federate")] + public bool Federate { get; set; } } -public class JoinRules -{ +public class JoinRules { private const string Public = "public"; private const string Invite = "invite"; private const string Knock = "knock"; - [JsonPropertyName("join_rule")] public string JoinRule { get; set; } - [JsonPropertyName("allow")] public List<string> Allow { get; set; } + [JsonPropertyName("join_rule")] + public string JoinRule { get; set; } + + [JsonPropertyName("allow")] + public List<string> Allow { get; set; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/RuntimeCache.cs b/MatrixRoomUtils.Core/RuntimeCache.cs index 4f73341..a2fcf40 100644 --- a/MatrixRoomUtils.Core/RuntimeCache.cs +++ b/MatrixRoomUtils.Core/RuntimeCache.cs @@ -3,9 +3,21 @@ using MatrixRoomUtils.Core.Responses; namespace MatrixRoomUtils.Core; -public class RuntimeCache -{ +public class RuntimeCache { public static bool WasLoaded = false; + + static RuntimeCache() => + Task.Run(async () => { + while (true) { + await Task.Delay(1000); + foreach (var (key, value) in GenericResponseCache) + if (value.Cache.Any()) + SaveObject("rory.matrixroomutils.generic_cache:" + key, value); + else + RemoveObject("rory.matrixroomutils.generic_cache:" + key); + } + }); + public static string? LastUsedToken { get; set; } public static AuthenticatedHomeServer CurrentHomeServer { get; set; } public static Dictionary<string, UserInfo> LoginSessions { get; set; } = new(); @@ -18,110 +30,69 @@ public class RuntimeCache public static Action Save { get; set; } = () => { Console.WriteLine("RuntimeCache.Save() was called, but no callback was set!"); }; public static Action<string, object> SaveObject { get; set; } = (key, value) => { Console.WriteLine($"RuntimeCache.SaveObject({key}, {value}) was called, but no callback was set!"); }; public static Action<string> RemoveObject { get; set; } = key => { Console.WriteLine($"RuntimeCache.RemoveObject({key}) was called, but no callback was set!"); }; - - static RuntimeCache() - { - Task.Run(async () => - { - while(true) - { - await Task.Delay(1000); - foreach (var (key, value) in GenericResponseCache) - { - if (value.Cache.Any()) - SaveObject("rory.matrixroomutils.generic_cache:" + key, value); - else - { - RemoveObject("rory.matrixroomutils.generic_cache:" + key); - } - } - } - }); - } } -public class UserInfo -{ +public class UserInfo { public ProfileResponse Profile { get; set; } = new(); public LoginResponse LoginResponse { get; set; } - public string AccessToken - { - get => LoginResponse.AccessToken; - } + public string AccessToken => LoginResponse.AccessToken; } -public class HomeServerResolutionResult -{ +public class HomeServerResolutionResult { public string Result { get; set; } public DateTime ResolutionTime { get; set; } } -public class ObjectCache<T> where T : class -{ +public class ObjectCache<T> where T : class { + public ObjectCache() => + //expiry timer + Task.Run(async () => { + while (Cache.Any()) { + await Task.Delay(1000); + foreach (var x in Cache.Where(x => x.Value.ExpiryTime < DateTime.Now).OrderBy(x => x.Value.ExpiryTime).Take(15).ToList()) + // Console.WriteLine($"Removing {x.Key} from cache"); + Cache.Remove(x.Key); + //RuntimeCache.SaveObject("rory.matrixroomutils.generic_cache:" + Name, this); + } + }); + public Dictionary<string, GenericResult<T>> Cache { get; set; } = new(); public string Name { get; set; } = null!; - public GenericResult<T> this[string key] - { - get - { - if (Cache.ContainsKey(key)) - { + public GenericResult<T> this[string key] { + get { + if (Cache.ContainsKey(key)) { // Console.WriteLine($"cache.get({key}): hit"); // Console.WriteLine($"Found item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); if (Cache[key].ExpiryTime < DateTime.Now) - Console.WriteLine($"WARNING: item {key} in cache {Name} expired at {Cache[key].ExpiryTime}:\n{Cache[key].Result.ToJson(indent: false)}"); + Console.WriteLine($"WARNING: item {key} in cache {Name} expired at {Cache[key].ExpiryTime}:\n{Cache[key].Result.ToJson(false)}"); return Cache[key]; } Console.WriteLine($"cache.get({key}): miss"); return null; } - set - { - Cache[key] = value; - // Console.WriteLine($"set({key}) = {Cache[key].Result.ToJson(indent:false)}"); - // Console.WriteLine($"new_state: {this.ToJson(indent:false)}"); - // Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); - // Console.Error.WriteLine("Full cache: " + Cache.ToJson()); - } - } - - public ObjectCache() - { - //expiry timer - Task.Run(async () => - { - while (Cache.Any()) - { - await Task.Delay(1000); - foreach (var x in Cache.Where(x => x.Value.ExpiryTime < DateTime.Now).OrderBy(x => x.Value.ExpiryTime).Take(15).ToList()) - { - // Console.WriteLine($"Removing {x.Key} from cache"); - Cache.Remove(x.Key); - } - //RuntimeCache.SaveObject("rory.matrixroomutils.generic_cache:" + Name, this); - } - }); + set => Cache[key] = value; + // Console.WriteLine($"set({key}) = {Cache[key].Result.ToJson(indent:false)}"); + // Console.WriteLine($"new_state: {this.ToJson(indent:false)}"); + // Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}"); + // Console.Error.WriteLine("Full cache: " + Cache.ToJson()); } public bool ContainsKey(string key) => Cache.ContainsKey(key); } -public class GenericResult<T> -{ - public T? Result { get; set; } - public DateTime? ExpiryTime { get; set; } = DateTime.Now; - - public GenericResult() - { +public class GenericResult<T> { + public GenericResult() { //expiry timer } - public GenericResult(T? result, DateTime? expiryTime = null) : this() - { + public GenericResult(T? result, DateTime? expiryTime = null) : this() { Result = result; ExpiryTime = expiryTime; } + + public T? Result { get; set; } + public DateTime? ExpiryTime { get; set; } = DateTime.Now; } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs index 6321fb6..a8c1fac 100644 --- a/MatrixRoomUtils.Core/StateEvent.cs +++ b/MatrixRoomUtils.Core/StateEvent.cs @@ -4,52 +4,48 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core; -public class StateEvent -{ - [JsonPropertyName("content")] public dynamic Content { get; set; } = new { }; +public class StateEvent { + [JsonPropertyName("content")] + public dynamic Content { get; set; } = new { }; + + [JsonPropertyName("state_key")] + public string StateKey { get; set; } = ""; - [JsonPropertyName("state_key")] public string StateKey { get; set; } = ""; - [JsonPropertyName("type")] public string Type { get; set; } - [JsonPropertyName("replaces_state")] public string? ReplacesState { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } //extra properties [JsonIgnore] - public JsonNode ContentAsJsonNode - { + public JsonNode ContentAsJsonNode { get => JsonSerializer.SerializeToNode(Content); set => Content = value; } - public StateEvent<T> As<T>() where T : class - { - return (StateEvent<T>)this; - } - - public string dtype - { - get - { - string res = GetType().Name switch - { + public string dtype { + get { + var res = GetType().Name switch { "StateEvent`1" => $"StateEvent<{Content.GetType().Name}>", _ => GetType().Name }; return res; } } + + public StateEvent<T> As<T>() where T : class => (StateEvent<T>)this; } -public class StateEvent<T> : StateEvent where T : class -{ - public StateEvent() - { +public class StateEvent<T> : StateEvent where T : class { + public StateEvent() { //import base content if not an empty object - if (base.Content.GetType() == typeof(T)) - { + if (base.Content.GetType() == typeof(T)) { Console.WriteLine($"StateEvent<{typeof(T)}> created with base content of type {base.Content.GetType()}. Importing base content."); Content = base.Content; } } + [JsonPropertyName("content")] public new T Content { get; set; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/StateEventStruct.cs b/MatrixRoomUtils.Core/StateEventStruct.cs index bfda594..cd301ac 100644 --- a/MatrixRoomUtils.Core/StateEventStruct.cs +++ b/MatrixRoomUtils.Core/StateEventStruct.cs @@ -1,7 +1,6 @@ namespace MatrixRoomUtils.Core; -public struct StateEventStruct -{ +public struct StateEventStruct { public object content { get; set; } public long origin_server_ts { get; set; } public string sender { get; set; } diff --git a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs index a927ace..6f6d082 100644 --- a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs +++ b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs @@ -2,51 +2,50 @@ using System.Text.Json.Serialization; namespace MatrixRoomUtils.Core.StateEventTypes; -public class PolicyRuleStateEventData -{ +public class PolicyRuleStateEventData { /// <summary> - /// Entity this ban applies to, can use * and ? as globs. + /// Entity this ban applies to, can use * and ? as globs. /// </summary> [JsonPropertyName("entity")] public string Entity { get; set; } + /// <summary> - /// Reason this user is banned + /// Reason this user is banned /// </summary> [JsonPropertyName("reason")] public string? Reason { get; set; } + /// <summary> - /// Suggested action to take + /// 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. + /// 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 + /// Readable expiry time, provided for easy interaction /// </summary> [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")] - public DateTime? ExpiryDateTime - { + public DateTime? ExpiryDateTime { get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime; - set => Expiry = ((DateTimeOffset) value).ToUnixTimeMilliseconds(); + set => Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); } } -public static class PolicyRecommendationTypes -{ +public static class PolicyRecommendationTypes { /// <summary> - /// Ban this user + /// Ban this user /// </summary> public static string Ban = "m.ban"; + /// <summary> - /// Mute this user + /// Mute this user /// </summary> public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending } \ No newline at end of file |