diff options
Diffstat (limited to 'MatrixRoomUtils.Core')
18 files changed, 235 insertions, 263 deletions
diff --git a/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs b/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs deleted file mode 100644 index 34a0b67..0000000 --- a/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace MatrixRoomUtils.Core.Attributes; - -[AttributeUsage(AttributeTargets.All)] -public class TraceAttribute : Attribute { - public TraceAttribute([CallerMemberName] string callerName = "") { - Console.WriteLine($"{callerName} called!"); - } -} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs index 10bbb36..09da766 100644 --- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs +++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs @@ -28,7 +28,7 @@ public class AuthenticatedHomeServer : IHomeServer { } public WhoAmIResponse WhoAmI { get; set; } = null!; - public string UserId { get; } + public string UserId => WhoAmI.UserId; public string AccessToken { get; set; } @@ -80,7 +80,7 @@ public class AuthenticatedHomeServer : IHomeServer { 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 != null) url += $"&from={res.NextBatch}"; + if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); @@ -88,7 +88,7 @@ public class AuthenticatedHomeServer : IHomeServer { totalRooms ??= res?.TotalRooms; Console.WriteLine(res.ToJson(false)); foreach (var room in res.Rooms) { - if (localFilter != null) { + if (localFilter is not null) { if (!room.RoomId.Contains(localFilter.RoomIdContains)) { totalRooms--; continue; @@ -144,7 +144,7 @@ public class AuthenticatedHomeServer : IHomeServer { continue; } } - // if (contentSearch != null && !string.IsNullOrEmpty(contentSearch) && + // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) && // !( // room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || // room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs deleted file mode 100644 index 28e0c49..0000000 --- a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Net.Http.Json; -using System.Text.Json; -using MatrixRoomUtils.Core.Extensions; -using MatrixRoomUtils.Core.Responses; -using MatrixRoomUtils.Core.StateEventTypes; - -namespace MatrixRoomUtils.Core.Authentication; - -public class MatrixAuth { - [Obsolete("This is possibly broken and should not be used.", true)] - public static async Task<LoginResponse> Login(string homeserver, string username, string password) { - Console.WriteLine($"Logging in to {homeserver} as {username}..."); - homeserver = (new RemoteHomeServer(homeserver)).FullHomeServerDomain; - var hc = new MatrixHttpClient(); - var payload = new { - type = "m.login.password", - identifier = new { - type = "m.id.user", - user = username - }, - password, - initial_device_display_name = "Rory&::MatrixRoomUtils" - }; - Console.WriteLine($"Sending login request to {homeserver}..."); - var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/v3/login", payload); - Console.WriteLine($"Login: {resp.StatusCode}"); - var data = await resp.Content.ReadFromJsonAsync<JsonElement>(); - if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data); - - Console.WriteLine($"Login: {data.ToJson()}"); - return data.Deserialize<LoginResponse>(); - //var token = data.GetProperty("access_token").GetString(); - //return token; - } -} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs index 47b3121..852e1d8 100644 --- a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs +++ b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Text.Json; namespace MatrixRoomUtils.Core.Extensions; @@ -18,23 +19,35 @@ public static class HttpClientExtensions { public class MatrixHttpClient : HttpClient { public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + try + { + HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"); + request.Options.Set(WebAssemblyEnableStreamingResponseKey, true); + // var asm = Assembly.Load("Microsoft.AspNetCore.Components.WebAssembly"); + // var browserHttpHandlerType = asm.GetType("Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions", true); + // var browserHttpHandlerMethod = browserHttpHandlerType.GetMethod("SetBrowserResponseStreamingEnabled", BindingFlags.Public | BindingFlags.Static); + // browserHttpHandlerMethod?.Invoke(null, new object[] {request, 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) { 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) { + 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; } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Helpers/MediaResolver.cs b/MatrixRoomUtils.Core/Helpers/MediaResolver.cs new file mode 100644 index 0000000..0869135 --- /dev/null +++ b/MatrixRoomUtils.Core/Helpers/MediaResolver.cs @@ -0,0 +1,6 @@ +namespace MatrixRoomUtils.Core.Helpers; + +public class MediaResolver { + public static string ResolveMediaUri(string homeserver, string mxc) => + mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/"); +} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs index 1b9c598..eff412b 100644 --- a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs +++ b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs @@ -20,7 +20,7 @@ public class SyncHelper { public async Task<SyncResult?> Sync(string? since = null, CancellationToken? cancellationToken = null) { var outFileName = "sync-" + - (await _storageService.CacheStorageProvider.GetAllKeys()).Count(x => x.StartsWith("sync")) + + (await _storageService.CacheStorageProvider.GetAllKeysAsync()).Count(x => x.StartsWith("sync")) + ".json"; var url = "/_matrix/client/v3/sync?timeout=30000&set_presence=online"; if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}"; @@ -29,7 +29,7 @@ public class SyncHelper { try { var res = await _homeServer._httpClient.GetFromJsonAsync<SyncResult>(url, cancellationToken: cancellationToken ?? CancellationToken.None); - await _storageService.CacheStorageProvider.SaveObject(outFileName, res); + await _storageService.CacheStorageProvider.SaveObjectAsync(outFileName, res); Console.WriteLine($"Wrote file: {outFileName}"); return res; } diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs index 4fa6c1b..4ee2a3e 100644 --- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs +++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs @@ -6,34 +6,36 @@ using MatrixRoomUtils.Core.StateEventTypes; namespace MatrixRoomUtils.Core.Interfaces; public class IHomeServer { - private readonly Dictionary<string, ProfileResponse?> _profileCache = new(); + private readonly Dictionary<string, object> _profileCache = new(); public string HomeServerDomain { get; set; } public string FullHomeServerDomain { get; set; } protected internal MatrixHttpClient _httpClient { get; set; } = new(); - 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]; - } + // 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]; + // } + // } + 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 ProfileResponse p) return p; } - - _profileCache.Add(mxid, null); + _profileCache[mxid] = new SemaphoreSlim(1); + var resp = await _httpClient.GetAsync($"/_matrix/client/v3/profile/{mxid}"); - var data = await resp.Content.ReadFromJsonAsync<JsonElement>(); + var data = await resp.Content.ReadFromJsonAsync<ProfileResponse>(); if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); - var profile = data.Deserialize<ProfileResponse>(); - _profileCache[mxid] = profile; - return profile; + _profileCache[mxid] = data; + + return data; } - - public string? ResolveMediaUri(string mxc) => mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/v3/download/"); } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs b/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs index 01314da..eefb79c 100644 --- a/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs +++ b/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs @@ -2,45 +2,45 @@ namespace MatrixRoomUtils.Core.Interfaces.Services; public interface IStorageProvider { // save all children of a type with reflection - public Task SaveAllChildren<T>(string key, T value) { + public Task SaveAllChildrenAsync<T>(string key, T value) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren<T>(key, value)!"); - return Task.CompletedTask; + throw new NotImplementedException(); } // load all children of a type with reflection - public Task<T?> LoadAllChildren<T>(string key) { + public Task<T?> LoadAllChildrenAsync<T>(string key) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren<T>(key)!"); - return Task.FromResult(default(T)); + throw new NotImplementedException(); } - public Task SaveObject<T>(string key, T value) { + public Task SaveObjectAsync<T>(string key, T value) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!"); - return Task.CompletedTask; + throw new NotImplementedException(); } // load - public Task<T?> LoadObject<T>(string key) { + public Task<T?> LoadObjectAsync<T>(string key) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject<T>(key)!"); - return Task.FromResult(default(T)); + throw new NotImplementedException(); } // check if exists - public Task<bool> ObjectExists(string key) { + public Task<bool> ObjectExistsAsync(string key) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!"); - return Task.FromResult(false); + throw new NotImplementedException(); } // get all keys - public Task<List<string>> GetAllKeys() { + public Task<List<string>> GetAllKeysAsync() { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!"); - return Task.FromResult(new List<string>()); + throw new NotImplementedException(); } // delete - public Task DeleteObject(string key) { + public Task DeleteObjectAsync(string key) { Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!"); - return Task.CompletedTask; + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/RemoteHomeServer.cs b/MatrixRoomUtils.Core/RemoteHomeServer.cs index 9c5f2a3..e6c28c3 100644 --- a/MatrixRoomUtils.Core/RemoteHomeServer.cs +++ b/MatrixRoomUtils.Core/RemoteHomeServer.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using System.Text.Json; using MatrixRoomUtils.Core.Extensions; using MatrixRoomUtils.Core.Interfaces; +using MatrixRoomUtils.Core.Services; namespace MatrixRoomUtils.Core; diff --git a/MatrixRoomUtils.Core/Responses/LoginResponse.cs b/MatrixRoomUtils.Core/Responses/LoginResponse.cs index b248739..239ea03 100644 --- a/MatrixRoomUtils.Core/Responses/LoginResponse.cs +++ b/MatrixRoomUtils.Core/Responses/LoginResponse.cs @@ -10,7 +10,7 @@ public class LoginResponse { public string DeviceId { get; set; } [JsonPropertyName("home_server")] - public string HomeServer { get; set; } + public string Homeserver { get; set; } [JsonPropertyName("user_id")] public string UserId { get; set; } diff --git a/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs index 8dc30d1..f57c855 100644 --- a/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs +++ b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs @@ -1,15 +1,17 @@ using System.Net.Http.Json; -using System.Text; using System.Text.Json; using System.Web; using MatrixRoomUtils.Core.Extensions; +using MatrixRoomUtils.Core.Responses; using MatrixRoomUtils.Core.RoomTypes; +using MatrixRoomUtils.Core.StateEventTypes; +using Microsoft.Extensions.Logging; namespace MatrixRoomUtils.Core; public class GenericRoom { internal readonly AuthenticatedHomeServer _homeServer; - internal readonly HttpClient _httpClient; + internal readonly MatrixHttpClient _httpClient; public GenericRoom(AuthenticatedHomeServer homeServer, string roomId) { _homeServer = homeServer; @@ -21,51 +23,41 @@ public class GenericRoom { public string RoomId { get; set; } - public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "", bool logOnFailure = true) { + [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); + } - var res = await _httpClient.GetAsync(url); - if (!res.IsSuccessStatusCode) { - if (logOnFailure) Console.WriteLine($"{RoomId}/{stateKey}/{type} - got status: {res.StatusCode}"); - return null; + 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; } - - var result = await res.Content.ReadFromJsonAsync<JsonElement>(); - return result; } - 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<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}"; + return await _httpClient.GetFromJsonAsync<T>(url); } 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.GetAsync(url); - 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}"); - } - - var result = await res.Content.ReadFromJsonAsync<MessagesResponse>(); - return result ?? new MessagesResponse(); + var res = await _httpClient.GetFromJsonAsync<MessagesResponse>(url); + return res ?? new MessagesResponse(); } public async Task<string> GetNameAsync() { - var res = await GetStateAsync("m.room.name"); - if (!res.HasValue) { - Console.WriteLine($"Room {RoomId} has no name!"); - return RoomId; - } - - var resn = res?.TryGetProperty("name", out var name) ?? false ? name.GetString() ?? RoomId : RoomId; - //Console.WriteLine($"Got name: {resn}"); - return resn; + var res = await GetStateAsync<RoomNameEventData>("m.room.name"); + return res.Name ?? RoomId; } public async Task JoinAsync(string[]? homeservers = null, string? reason = null) { @@ -78,165 +70,89 @@ public class GenericRoom { }); } - 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()) { - 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(); - members.Add( - memberId ?? throw new InvalidOperationException("Event type was member but state key was null!")); + 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; } - - return members; } 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() ?? ""); - - return aliases; + var res = await GetStateAsync<RoomAliasEventData>("m.room.aliases"); + return res.Aliases; } - 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() { - var res = await GetStateAsync("m.room.topic"); - if (!res.HasValue) return ""; - return res.Value.GetProperty("topic").GetString() ?? ""; - } - - 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<CanonicalAliasEventData?> GetCanonicalAliasAsync() => + await GetStateAsync<CanonicalAliasEventData>("m.room.canonical_alias"); - public async Task<JoinRulesEventData> GetJoinRuleAsync() { - var res = await GetStateAsync("m.room.join_rules"); - if (!res.HasValue) return new JoinRulesEventData(); - return res.Value.Deserialize<JoinRulesEventData>() ?? new JoinRulesEventData(); - } + public async Task<RoomTopicEventData?> GetTopicAsync() => + await GetStateAsync<RoomTopicEventData>("m.room.topic"); - 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<RoomAvatarEventData?> GetAvatarUrlAsync() => + await GetStateAsync<RoomAvatarEventData>("m.room.avatar"); - 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<JoinRulesEventData> GetJoinRuleAsync() => + await GetStateAsync<JoinRulesEventData>("m.room.join_rules"); - public async Task<CreateEvent> GetCreateEventAsync() { - var res = await GetStateAsync("m.room.create"); - if (!res.HasValue) return new CreateEvent(); + public async Task<HistoryVisibilityData?> GetHistoryVisibilityAsync() => + await GetStateAsync<HistoryVisibilityData>("m.room.history_visibility"); - res.FindExtraJsonElementFields(typeof(CreateEvent)); + public async Task<GuestAccessData?> GetGuestAccessAsync() => + await GetStateAsync<GuestAccessData>("m.room.guest_access"); - return res.Value.Deserialize<CreateEvent>() ?? new CreateEvent(); - } + public async Task<CreateEvent> GetCreateEventAsync() => + await GetStateAsync<CreateEvent>("m.room.create"); public async Task<string?> GetRoomType() { - var res = await GetStateAsync("m.room.create"); - if (!res.HasValue) return null; - if (res.Value.TryGetProperty("type", out var type)) return type.GetString(); - return null; + var res = await GetStateAsync<RoomCreateEventData>("m.room.create"); + return res.Type; } - public async Task ForgetAsync() { - var res = await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to forget room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to forget room {RoomId} - got status: {res.StatusCode}"); - } - } + public async Task ForgetAsync() => + await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null); - public async Task LeaveAsync(string? reason = null) { - var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new { + public async Task LeaveAsync(string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new { reason }); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to leave room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to leave room {RoomId} - got status: {res.StatusCode}"); - } - } - public async Task KickAsync(string userId, string? reason = null) { - var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", + public async Task KickAsync(string userId, string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", new UserIdAndReason() { UserId = userId, Reason = reason }); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}"); - } - } - public async Task BanAsync(string userId, string? reason = null) { - var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", + public async Task BanAsync(string userId, string? reason = null) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", new UserIdAndReason() { UserId = userId, Reason = reason }); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}"); - } - } - public async Task UnbanAsync(string userId) { - var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", + public async Task UnbanAsync(string userId) => + await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", new UserIdAndReason() { UserId = userId }); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}"); - } - } - - public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) { - var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content); - if (!res.IsSuccessStatusCode) { - Console.WriteLine( - $"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - throw new Exception( - $"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - } - return await res.Content.ReadFromJsonAsync<EventIdResponse>(); - } + 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, MessageEventData content) { - var url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(); - var res = await _httpClient.PutAsJsonAsync(url, content); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - } - + 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 url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(); var content = new MultipartFormDataContent(); content.Add(new StreamContent(fileStream), "file", fileName); - var res = await _httpClient.PutAsync(url, content); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - throw new Exception($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}"); - } - - var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>(); - - return resu; + 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/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs index 6b586c7..3be3130 100644 --- a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs +++ b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs @@ -8,20 +8,18 @@ namespace MatrixRoomUtils.Core.RoomTypes; public class SpaceRoom : GenericRoom { private readonly AuthenticatedHomeServer _homeServer; private readonly GenericRoom _room; + public SpaceRoom(AuthenticatedHomeServer homeServer, string roomId) : base(homeServer, roomId) { _homeServer = homeServer; } public async Task<List<GenericRoom>> GetRoomsAsync(bool includeRemoved = false) { var rooms = new List<GenericRoom>(); - var state = await GetStateAsync(""); - if (state != null) { - var states = state.Value.Deserialize<StateEventResponse[]>()!; - foreach (var stateEvent in states.Where(x => x.Type == "m.space.child")) { - var roomId = stateEvent.StateKey; - if(stateEvent.TypedContent.ToJson() != "{}" || includeRemoved) - rooms.Add(await _homeServer.GetRoom(roomId)); - } + var state = GetFullStateAsync().ToBlockingEnumerable().ToList(); + var childStates = state.Where(x => x.Type == "m.space.child"); + foreach (var stateEvent in childStates) { + if (stateEvent.TypedContent.ToJson() != "{}" || includeRemoved) + rooms.Add(await _homeServer.GetRoom(stateEvent.StateKey)); } return rooms; diff --git a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs index 8a22d33..870e0d4 100644 --- a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs +++ b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs @@ -1,7 +1,11 @@ using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using MatrixRoomUtils.Core.Extensions; +using MatrixRoomUtils.Core.Responses; using Microsoft.Extensions.Logging; + namespace MatrixRoomUtils.Core.Services; public class HomeserverProviderService { @@ -9,7 +13,8 @@ public class HomeserverProviderService { private readonly ILogger<HomeserverProviderService> _logger; private readonly HomeserverResolverService _homeserverResolverService; - public HomeserverProviderService(TieredStorageService tieredStorageService, ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) { + public HomeserverProviderService(TieredStorageService tieredStorageService, + ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) { Console.WriteLine("Homeserver provider service instantiated!"); _tieredStorageService = tieredStorageService; _logger = logger; @@ -18,9 +23,11 @@ public class HomeserverProviderService { $"New HomeserverProviderService created with TieredStorageService<{string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name))}>!"); } - public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken) { + public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken, + string? overrideFullDomain = null) { var hs = new AuthenticatedHomeServer(_tieredStorageService, homeserver, accessToken); - hs.FullHomeServerDomain = await _homeserverResolverService.ResolveHomeserverFromWellKnown(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(5); @@ -29,4 +36,48 @@ public class HomeserverProviderService { hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!; 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(5); + 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; } = ""; + } + } } \ No newline at end of file diff --git a/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs b/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs index f6363ab..526a261 100644 --- a/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs +++ b/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs @@ -6,11 +6,12 @@ using Microsoft.Extensions.Logging; namespace MatrixRoomUtils.Core.Services; public class HomeserverResolverService { - private readonly HttpClient _httpClient; + private readonly MatrixHttpClient _httpClient = new MatrixHttpClient(); private readonly ILogger<HomeserverResolverService> _logger; - public HomeserverResolverService(HttpClient httpClient, ILogger<HomeserverResolverService> logger) { - _httpClient = httpClient; + private static Dictionary<string, object> _wellKnownCache = new(); + + public HomeserverResolverService(ILogger<HomeserverResolverService> logger) { _logger = logger; } @@ -22,6 +23,12 @@ public class HomeserverResolverService { } private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) { + if(homeserver is null) throw new ArgumentNullException(nameof(homeserver)); + if (_wellKnownCache.ContainsKey(homeserver)) { + if (_wellKnownCache[homeserver] is SemaphoreSlim s) await s.WaitAsync(); + if (_wellKnownCache[homeserver] is string p) return p; + } + _wellKnownCache[homeserver] = new SemaphoreSlim(1); string? result = null; _logger.LogInformation($"Attempting to resolve homeserver: {homeserver}"); if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver; @@ -34,6 +41,7 @@ public class HomeserverResolverService { if(result is not null) { _logger.LogInformation($"Resolved homeserver: {homeserver} -> {result}"); + _wellKnownCache.TryAdd(homeserver, result); return result; } diff --git a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs index 4a831c1..ef9238d 100644 --- a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs +++ b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs @@ -9,14 +9,14 @@ public static class ServiceInstaller { if (!services.Any(x => x.ServiceType == typeof(TieredStorageService))) throw new Exception("[MRUCore/DI] No TieredStorageService has been registered!"); //Add config - if(config != null) + if(config is not null) services.AddSingleton(config); else { services.AddSingleton(new RoryLibMatrixConfiguration()); } //Add services services.AddScoped<HomeserverProviderService>(); - services.AddScoped<HomeserverResolverService>(); + services.AddSingleton<HomeserverResolverService>(); services.AddScoped<HttpClient>(); return services; } diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs index f2c8701..18b4632 100644 --- a/MatrixRoomUtils.Core/StateEvent.cs +++ b/MatrixRoomUtils.Core/StateEvent.cs @@ -29,7 +29,7 @@ public class StateEvent { get => _type; set { _type = value; - if (RawContent != null && this is StateEventResponse stateEventResponse) { + if (RawContent is not null && this is StateEventResponse stateEventResponse) { if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return; var x = GetType.Name; } @@ -46,7 +46,7 @@ public class StateEvent { get => _rawContent; set { _rawContent = value; - if (Type != null && this is StateEventResponse stateEventResponse) { + if (Type is not null && this is StateEventResponse stateEventResponse) { if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return; var x = GetType.Name; } diff --git a/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs new file mode 100644 index 0000000..efc946d --- /dev/null +++ b/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using MatrixRoomUtils.Core.Extensions; +using MatrixRoomUtils.Core.Interfaces; + +namespace MatrixRoomUtils.Core.StateEventTypes; + +[MatrixEvent(EventName = "org.matrix.mjolnir.shortcode")] +public class MjolnirShortcodeEventData : IStateEventType { + [JsonPropertyName("shortcode")] + public string? Shortcode { get; set; } +} \ No newline at end of file diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs new file mode 100644 index 0000000..5141ed2 --- /dev/null +++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using MatrixRoomUtils.Core.Extensions; +using MatrixRoomUtils.Core.Interfaces; + +namespace MatrixRoomUtils.Core; + +[MatrixEvent(EventName = "m.room.alias")] +public class RoomAliasEventData : IStateEventType { + [JsonPropertyName("aliases")] + public List<string>? Aliases { get; set; } +} \ No newline at end of file |