diff options
25 files changed, 563 insertions, 202 deletions
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs index f04ec3a..0211f74 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs @@ -57,7 +57,7 @@ public class MRUBot : IHostedService { hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => - x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId); + x.Type == "m.room.member" && x.StateKey == hs.UserId); _logger.LogInformation( $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}"); if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender == "@mxidupwitch:the-apothecary.club") { diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs b/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs index d633f89..fd6866c 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs @@ -43,7 +43,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic messageType: "m.text")); //get replied message - var repliedMessage = await ctx.Room.GetEvent<StateEventResponse>(messageContent.RelatesTo!.InReplyTo!.EventId); + var repliedMessage = await ctx.Room.GetEventAsync<StateEventResponse>(messageContent.RelatesTo!.InReplyTo!.EventId); //check if recommendation is in list if (ctx.Args.Length < 2) { diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs index e6ba269..f9bbcf3 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs @@ -109,17 +109,17 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => - x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId); + x.Type == "m.room.member" && x.StateKey == hs.UserId); logger.LogInformation( $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}"); if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender.EndsWith(":conduit.rory.gay")) { try { var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender); - await (hs.GetRoom(args.Key)).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); + await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); } catch (Exception e) { logger.LogError("{}", e.ToString()); - await (hs.GetRoom(args.Key)).LeaveAsync(reason: "I was unable to join the room: " + e); + await hs.GetRoom(args.Key).LeaveAsync(reason: "I was unable to join the room: " + e); } } }); @@ -161,16 +161,16 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> case "warn": { await room.SendMessageEventAsync( new RoomMessageEventContent( - body: $"Please be careful when posting this image: {matchedpolicyData.Reason}", + body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}", messageType: "m.text") { Format = "org.matrix.custom.html", FormattedBody = - $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}</a></font>" + $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>" }); break; } case "redact": { - await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified"); break; } case "spoiler": { @@ -220,9 +220,15 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); //change powerlevel to -1 var currentPls = await room.GetPowerLevelsAsync(); + if(currentPls is null) { + logger.LogWarning("Unable to get power levels for {room}", room.RoomId); + await _logRoom.SendMessageEventAsync( + MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}")); + return; + } + currentPls.Users ??= new(); currentPls.Users[@event.Sender] = -1; await room.SendStateEventAsync("m.room.power_levels", currentPls); - break; } case "kick": { diff --git a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs index 6686a37..0096c78 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs @@ -4,7 +4,8 @@ using LibMatrix.Interfaces; namespace MediaModeratorPoC.Bot.StateEventTypes; -[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")] +[ + MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")] [MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.media")] public class MediaPolicyEventContent : EventContent { /// <summary> diff --git a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs index c3cebe2..0bd2bbf 100644 --- a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs +++ b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs @@ -41,7 +41,7 @@ public class PluralContactBot(AuthenticatedHomeserverGeneric hs, ILogger<PluralC hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => - x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId); + x.Type == "m.room.member" && x.StateKey == hs.UserId); logger.LogInformation("Got invite to {} by {} with reason: {}", args.Key, inviteEvent.Sender, (inviteEvent.TypedContent as RoomMemberEventContent).Reason); try { diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs index b76b176..f8ee58b 100644 --- a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs +++ b/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs @@ -28,4 +28,6 @@ public class RoomMessageEventContent : EventContent { /// </summary> [JsonPropertyName("url")] public string? Url { get; set; } + + public string? FileName { get; set; } } diff --git a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs index e409f3a..c5bf14e 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs @@ -8,12 +8,16 @@ namespace LibMatrix.EventTypes.Spec.State; public class RoomCreateEventContent : EventContent { [JsonPropertyName("room_version")] public string? RoomVersion { get; set; } + [JsonPropertyName("creator")] public string? Creator { get; set; } + [JsonPropertyName("m.federate")] public bool? Federate { get; set; } + [JsonPropertyName("predecessor")] public RoomCreatePredecessor? Predecessor { get; set; } + [JsonPropertyName("type")] public string? Type { get; set; } diff --git a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs index 1a5d5f5..2ae9593 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs @@ -7,34 +7,34 @@ namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.power_levels")] public class RoomPowerLevelEventContent : EventContent { [JsonPropertyName("ban")] - public long? Ban { get; set; } // = 50; + public long? Ban { get; set; } = 50; [JsonPropertyName("events_default")] - public long EventsDefault { get; set; } // = 0; + public long EventsDefault { get; set; } = 0; [JsonPropertyName("events")] public Dictionary<string, long>? Events { get; set; } // = null!; [JsonPropertyName("invite")] - public long? Invite { get; set; } // = 50; + public long? Invite { get; set; } = 0; [JsonPropertyName("kick")] - public long? Kick { get; set; } // = 50; + public long? Kick { get; set; } = 50; [JsonPropertyName("notifications")] public NotificationsPL? NotificationsPl { get; set; } // = null!; [JsonPropertyName("redact")] - public long? Redact { get; set; } // = 50; + public long? Redact { get; set; } = 50; [JsonPropertyName("state_default")] - public long? StateDefault { get; set; } // = 50; + public long? StateDefault { get; set; } = 50; [JsonPropertyName("users")] public Dictionary<string, long>? Users { get; set; } // = null!; [JsonPropertyName("users_default")] - public long? UsersDefault { get; set; } // = 0; + public long? UsersDefault { get; set; } = 0; [Obsolete("Historical was a key related to MSC2716, a spec change on backfill that was dropped!", true)] [JsonIgnore] @@ -47,7 +47,7 @@ public class RoomPowerLevelEventContent : EventContent { } public bool IsUserAdmin(string userId) { - return Users.TryGetValue(userId, out var level) && level >= Events.Max(x=>x.Value); + return Users.TryGetValue(userId, out var level) && level >= Events.Max(x => x.Value); } public bool UserHasPermission(string userId, string eventType) { diff --git a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs b/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs index a13ba2e..0a897dc 100644 --- a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs @@ -9,7 +9,7 @@ public class SpaceChildEventContent : EventContent { [JsonPropertyName("auto_join")] public bool? AutoJoin { get; set; } [JsonPropertyName("via")] - public string[]? Via { get; set; } + public List<string>? Via { get; set; } [JsonPropertyName("suggested")] public bool? Suggested { get; set; } } diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs index 386fd4d..74972a1 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -16,10 +16,6 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) { string? setPresence = "online", SyncFilter? filter = null, CancellationToken? cancellationToken = null) { - // var outFileName = "sync-" + - // (await storageService.CacheStorageProvider.GetAllKeysAsync()).Count( - // x => x.StartsWith("sync")) + - // ".json"; var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}"; if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}"; if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}"; @@ -28,19 +24,17 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) { try { var req = await homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None); - // var res = await JsonSerializer.DeserializeAsync<SyncResult>(await req.Content.ReadAsStreamAsync()); - #if DEBUG && false - var jsonObj = await req.Content.ReadFromJsonAsync<JsonElement>(); try { - await _homeServer._httpClient.PostAsJsonAsync( - "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName, jsonObj); + await homeserver._httpClient.PostAsync( + "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName, + new StreamContent(await req.Content.ReadAsStreamAsync())); } + catch (Exception e) { Console.WriteLine("[!!] Checking sync response failed: " + e); } - - var res = jsonObj.Deserialize<SyncResult>(); + var res = await req.Content.ReadFromJsonAsync<SyncResult>(); return res; #else return await req.Content.ReadFromJsonAsync<SyncResult>(); diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index b881e6c..f70dd39 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; @@ -12,18 +13,30 @@ using LibMatrix.Services; namespace LibMatrix.Homeservers; public class AuthenticatedHomeserverGeneric : RemoteHomeServer { - public AuthenticatedHomeserverGeneric(string canonicalHomeServerDomain, string accessToken) : base(canonicalHomeServerDomain) { + public AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : base(baseUrl) { AccessToken = accessToken.Trim(); SyncHelper = new SyncHelper(this); + + _httpClient.Timeout = TimeSpan.FromMinutes(15); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } public virtual SyncHelper SyncHelper { get; init; } - public virtual WhoAmIResponse WhoAmI { get; set; } = null!; - public virtual string UserId => WhoAmI.UserId; + private WhoAmIResponse? _whoAmI; + + public WhoAmIResponse? WhoAmI => _whoAmI ??= _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami").Result; + public string UserId => WhoAmI.UserId; + + // public virtual async Task<WhoAmIResponse> WhoAmI() { + // if (_whoAmI is not null) return _whoAmI; + // _whoAmI = await _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"); + // return _whoAmI; + // } + public virtual string AccessToken { get; set; } public virtual GenericRoom GetRoom(string roomId) { - if(roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId)); + if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId)); return new GenericRoom(this, roomId); } @@ -50,7 +63,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { } public virtual async Task<GenericRoom> CreateRoom(CreateRoomRequest creationEvent) { - creationEvent.CreationContent["creator"] = UserId; + creationEvent.CreationContent["creator"] = WhoAmI.UserId; var res = await _httpClient.PostAsJsonAsync("/_matrix/client/v3/createRoom", creationEvent, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); @@ -61,9 +74,10 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { var room = GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString()); - foreach (var user in creationEvent.Invite) { - await room.InviteUser(user); - } + if (creationEvent.Invite is not null) + foreach (var user in creationEvent.Invite) { + await room.InviteUserAsync(user); + } return room; } @@ -77,6 +91,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { } #region Utility Functions + public virtual async IAsyncEnumerable<GenericRoom> GetJoinedRoomsByType(string type) { var rooms = await GetJoinedRooms(); var tasks = rooms.Select(async room => { @@ -92,6 +107,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { if (result is not null) yield return result; } } + #endregion #region Account Data @@ -104,11 +120,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { // } // // return await res.Content.ReadFromJsonAsync<T>(); - return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{UserId}/account_data/{key}"); + return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{WhoAmI.UserId}/account_data/{key}"); } public virtual async Task SetAccountData(string key, object data) { - var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}", data); + var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{WhoAmI.UserId}/account_data/{key}", data); if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to set account data: {await res.Content.ReadAsStringAsync()}"); throw new InvalidDataException($"Failed to set account data: {await res.Content.ReadAsStringAsync()}"); @@ -116,4 +132,6 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { } #endregion + + } diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs index 5319f46..a420d71 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs @@ -4,5 +4,4 @@ using LibMatrix.Services; namespace LibMatrix.Homeservers; -public class AuthenticatedHomeserverMxApiExtended(string canonicalHomeServerDomain, string accessToken) : AuthenticatedHomeserverGeneric(canonicalHomeServerDomain, - accessToken); +public class AuthenticatedHomeserverMxApiExtended(string baseUrl, string accessToken) : AuthenticatedHomeserverGeneric(baseUrl, accessToken); diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs index ae26f69..e355d2d 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs @@ -102,7 +102,7 @@ public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric { } } - public AuthenticatedHomeserverSynapse(string canonicalHomeServerDomain, string accessToken) : base(canonicalHomeServerDomain, accessToken) { + public AuthenticatedHomeserverSynapse(string baseUrl, string accessToken) : base(baseUrl, accessToken) { Admin = new(this); } } diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index ab3ab51..d10c837 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -1,19 +1,22 @@ using System.Net.Http.Json; +using System.Text.Json; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Extensions; using LibMatrix.Responses; +using LibMatrix.Services; namespace LibMatrix.Homeservers; -public class RemoteHomeServer(string canonicalHomeServerDomain) { - // _httpClient.Timeout = TimeSpan.FromSeconds(5); +public class RemoteHomeServer(string baseUrl) { private Dictionary<string, object> _profileCache { get; set; } = new(); - public string HomeServerDomain { get; } = canonicalHomeServerDomain.Trim(); - public string FullHomeServerDomain { get; set; } - public MatrixHttpClient _httpClient { get; set; } = new(); + public string BaseUrl { get; } = baseUrl.Trim(); + public MatrixHttpClient _httpClient { get; set; } = new() { + BaseAddress = new Uri(new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl).Result ?? throw new InvalidOperationException("Failed to resolve homeserver")), + Timeout = TimeSpan.FromSeconds(120) + }; public async Task<ProfileResponseEventContent> GetProfileAsync(string mxid) { if (mxid is null) throw new ArgumentNullException(nameof(mxid)); @@ -46,6 +49,42 @@ public class RemoteHomeServer(string canonicalHomeServerDomain) { if (!resp.IsSuccessStatusCode) Console.WriteLine("ResolveAlias: " + data.ToJson()); return data; } + +#region Authentication + + public async Task<LoginResponse> LoginAsync(string username, string password, string? deviceName = null) { + var resp = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/login", new { + type = "m.login.password", + identifier = new { + type = "m.id.user", + user = username + }, + password = password, + initial_device_display_name = deviceName + }); + var data = await resp.Content.ReadFromJsonAsync<LoginResponse>(); + if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToJson()); + return data; + } + + public async Task<LoginResponse> RegisterAsync(string username, string password, string? deviceName = null) { + var resp = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/register", new { + kind = "user", + auth = new { + type = "m.login.dummy" + }, + username, + password, + initial_device_display_name = deviceName ?? "LibMatrix" + }, new JsonSerializerOptions() { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + var data = await resp.Content.ReadFromJsonAsync<LoginResponse>(); + if (!resp.IsSuccessStatusCode) Console.WriteLine("Register: " + data.ToJson()); + return data; + } + +#endregion } public class AliasResult { diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs index 381271b..511b3da 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs @@ -40,7 +40,7 @@ public class CreateRoomRequest { public JsonObject CreationContent { get; set; } = new(); [JsonPropertyName("invite")] - public List<string> Invite { get; set; } + public List<string>? Invite { get; set; } /// <summary> /// For use only when you can't use the CreationContent property diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs index 175f337..eb53c0a 100644 --- a/LibMatrix/Responses/LoginResponse.cs +++ b/LibMatrix/Responses/LoginResponse.cs @@ -1,19 +1,30 @@ using System.Text.Json.Serialization; +using LibMatrix.Homeservers; +using LibMatrix.Services; namespace LibMatrix.Responses; public class LoginResponse { [JsonPropertyName("access_token")] - public string AccessToken { get; set; } + public string AccessToken { get; set; } = null!; [JsonPropertyName("device_id")] - public string DeviceId { get; set; } + public string DeviceId { get; set; } = null!; + + private string? _homeserver; [JsonPropertyName("home_server")] - public string Homeserver { get; set; } + public string Homeserver { + get => _homeserver ?? UserId.Split(':', 2).Last(); + protected init => _homeserver = value; + } [JsonPropertyName("user_id")] - public string UserId { get; set; } + public string UserId { get; set; } = null!; + + public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) { + return new AuthenticatedHomeserverGeneric(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken); + } } public class LoginRequest { [JsonPropertyName("type")] diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index ab748fe..78a0873 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -28,14 +28,6 @@ public class GenericRoom { public string RoomId { get; set; } - [Obsolete("", true)] - public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "") { - var url = $"/_matrix/client/v3/rooms/{RoomId}/state"; - if (!string.IsNullOrEmpty(type)) url += $"/{type}"; - if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}"; - return await _httpClient.GetFromJsonAsync<JsonElement>(url); - } - public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() { var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/state"); var result = @@ -68,7 +60,7 @@ public class GenericRoom { } catch (MatrixException e) { // if (e is not { ErrorCodode: "M_NOT_FOUND" }) { - throw; + throw; // } // Console.WriteLine(e); @@ -202,21 +194,18 @@ public class GenericRoom { return resu; } - public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) { - var content = new MultipartFormDataContent(); - content.Add(new StreamContent(fileStream), "file", fileName); - var res = await - ( - await _httpClient.PutAsync( - $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), - content - ) - ) - .Content.ReadFromJsonAsync<EventIdResponse>(); - return res; + public async Task<EventIdResponse> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") { + var url = await Homeserver.UploadFile(fileName, fileStream); + var content = new RoomMessageEventContent() { + MessageType = messageType, + Url = url, + Body = fileName, + FileName = fileName, + }; + return await SendTimelineEventAsync("m.room.message", content); } - public async Task<T> GetRoomAccountData<T>(string key) { + public async Task<T> GetRoomAccountDataAsync<T>(string key) { var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}"); if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to get room account data: {await res.Content.ReadAsStringAsync()}"); @@ -226,7 +215,7 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<T>(); } - public async Task SetRoomAccountData(string key, object data) { + public async Task SetRoomAccountDataAsync(string key, object data) { var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data); if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to set room account data: {await res.Content.ReadAsStringAsync()}"); @@ -236,7 +225,7 @@ public class GenericRoom { public readonly SpaceRoom AsSpace; - public async Task<T> GetEvent<T>(string eventId) { + public async Task<T> GetEventAsync<T>(string eventId) { return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}"); } @@ -246,9 +235,38 @@ public class GenericRoom { $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!; } - public async Task InviteUser(string userId, string? reason = null) { + public async Task InviteUserAsync(string userId, string? reason = null) { await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason)); } + +#region Disband room + + public async Task DisbandRoomAsync() { + var states = GetFullStateAsync(); + List<string> stateTypeIgnore = new() { + "m.room.create", + "m.room.power_levels", + "m.room.join_rules", + "m.room.history_visibility", + "m.room.guest_access", + "m.room.member", + }; + await foreach (var state in states) { + if (state is null || state.RawContent is not { Count: > 0 }) continue; + if (state.Type == "m.room.member" && state.StateKey != Homeserver.UserId) + try { + await BanAsync(state.StateKey, "Disbanding room"); + } + catch (MatrixException e) { + if (e.ErrorCode != "M_FORBIDDEN") throw; + } + + if (stateTypeIgnore.Contains(state.Type)) continue; + await SendStateEventAsync(state.Type, state.StateKey, new()); + } + } + +#endregion } public class RoomIdResponse { diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs index a43ae82..4a8e247 100644 --- a/LibMatrix/RoomTypes/SpaceRoom.cs +++ b/LibMatrix/RoomTypes/SpaceRoom.cs @@ -1,26 +1,37 @@ using ArcaneLibs.Extensions; using LibMatrix.Homeservers; +using Microsoft.Extensions.Logging; namespace LibMatrix.RoomTypes; -public class SpaceRoom : GenericRoom { - private new readonly AuthenticatedHomeserverGeneric _homeserver; +public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) { private readonly GenericRoom _room; - public SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : base(homeserver, roomId) { - _homeserver = homeserver; - } - - private static SemaphoreSlim _semaphore = new(1, 1); public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) { - // await _semaphore.WaitAsync(); var rooms = new List<GenericRoom>(); var state = GetFullStateAsync(); await foreach (var stateEvent in state) { - if (stateEvent.Type != "m.space.child") continue; - if (stateEvent.RawContent.ToJson() != "{}" || includeRemoved) - yield return _homeserver.GetRoom(stateEvent.StateKey); + if (stateEvent!.Type != "m.space.child") continue; + if (stateEvent.RawContent!.ToJson() != "{}" || includeRemoved) + yield return Homeserver.GetRoom(stateEvent.StateKey); + } + } + + public async Task<EventIdResponse> AddChildAsync(GenericRoom room) { + var members = room.GetMembersAsync(true); + Dictionary<string, int> memberCountByHs = new(); + await foreach (var member in members) { + var server = member.StateKey.Split(':')[1]; + if (memberCountByHs.ContainsKey(server)) memberCountByHs[server]++; + else memberCountByHs[server] = 1; } - // _semaphore.Release(); + + var resp = await SendStateEventAsync("m.space.child", room.RoomId, new { + via = memberCountByHs + .OrderByDescending(x => x.Value) + .Select(x => x.Key) + .Take(10) + }); + return resp; } } diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs index 49167fa..666d2a2 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -9,29 +9,29 @@ using Microsoft.Extensions.Logging; namespace LibMatrix.Services; public class HomeserverProviderService { - private readonly TieredStorageService _tieredStorageService; private readonly ILogger<HomeserverProviderService> _logger; private readonly HomeserverResolverService _homeserverResolverService; - public HomeserverProviderService(TieredStorageService tieredStorageService, - ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) { - _tieredStorageService = tieredStorageService; + public HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) { _logger = logger; _homeserverResolverService = homeserverResolverService; - logger.LogDebug("New HomeserverProviderService created with TieredStorageService<{}>!", - string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name))); } private static Dictionary<string, SemaphoreSlim> _authenticatedHomeserverSemaphore = new(); private static Dictionary<string, AuthenticatedHomeserverGeneric> _authenticatedHomeServerCache = new(); + private static Dictionary<string, SemaphoreSlim> _remoteHomeserverSemaphore = new(); + private static Dictionary<string, RemoteHomeServer> _remoteHomeServerCache = new(); + public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedWithToken(string homeserver, string accessToken, string? proxy = null) { var sem = _authenticatedHomeserverSemaphore.GetOrCreate(homeserver + accessToken, _ => new SemaphoreSlim(1, 1)); await sem.WaitAsync(); - if (_authenticatedHomeServerCache.ContainsKey(homeserver + accessToken)) { - sem.Release(); - return _authenticatedHomeServerCache[homeserver + accessToken]; + lock (_authenticatedHomeServerCache) { + if (_authenticatedHomeServerCache.ContainsKey(homeserver + accessToken)) { + sem.Release(); + return _authenticatedHomeServerCache[homeserver + accessToken]; + } } var domain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver); @@ -45,12 +45,11 @@ public class HomeserverProviderService { hs = new AuthenticatedHomeserverGeneric(homeserver, accessToken); } - hs.FullHomeServerDomain = domain; hs._httpClient = hc; hs._httpClient.Timeout = TimeSpan.FromMinutes(15); hs._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!; + // (() => hs.WhoAmI) = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!; lock(_authenticatedHomeServerCache) _authenticatedHomeServerCache[homeserver + accessToken] = hs; @@ -60,11 +59,10 @@ public class HomeserverProviderService { } public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? proxy = null) { - var hs = new RemoteHomeServer(homeserver); - hs.FullHomeServerDomain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver); - hs._httpClient.Dispose(); - hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) }; - hs._httpClient.Timeout = TimeSpan.FromSeconds(120); + var hs = new RemoteHomeServer(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver)); + // hs._httpClient.Dispose(); + // hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) }; + // hs._httpClient.Timeout = TimeSpan.FromSeconds(120); return hs; } diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 97348a5..b42bd64 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using ArcaneLibs; using ArcaneLibs.Extensions; using LibMatrix.EventTypes; @@ -48,7 +49,7 @@ public class StateEvent { return null; } - set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value)); + set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value, value.GetType())); } [JsonPropertyName("state_key")] @@ -120,3 +121,32 @@ public class StateEvent { [JsonIgnore] public string cdtype => TypedContent.GetType().Name; } + +/* +public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoResolver +{ + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options); + + Type baseType = typeof(EventContent); + if (jsonTypeInfo.Type == baseType) { + jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions { + TypeDiscriminatorPropertyName = "type", + IgnoreUnrecognizedTypeDiscriminators = true, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType, + + DerivedTypes = StateEvent.KnownStateEventTypesByName.Select(x => new JsonDerivedType(x.Value, x.Key)).ToList() + + // DerivedTypes = new ClassCollector<EventContent>() + // .ResolveFromAllAccessibleAssemblies() + // .SelectMany(t => t.GetCustomAttributes<MatrixEventAttribute>() + // .Select(a => new JsonDerivedType(t, attr.EventName)); + + }; + } + + return jsonTypeInfo; + } +} +*/ diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs new file mode 100644 index 0000000..abd3e99 --- /dev/null +++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs @@ -0,0 +1,73 @@ +using ArcaneLibs.Extensions; +using LibMatrix.Homeservers; +using LibMatrix.Responses; + +namespace LibMatrix.Tests.Abstractions; + +public static class HomeserverAbstraction { + public static async Task<AuthenticatedHomeserverGeneric> GetHomeserver() { + var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay"); + // string username = Guid.NewGuid().ToString(); + // string password = Guid.NewGuid().ToString(); + string username = "@f1a2d2d6-1924-421b-91d0-893b347b2a49:matrixunittests.rory.gay"; + string password = "d6d782d6-8bc9-4fac-9cd8-78e101b4298b"; + LoginResponse reg; + try { + reg = await rhs.LoginAsync(username, password); + } + catch (MatrixException e) { + if (e.ErrorCode == "M_FORBIDDEN") { + await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); + reg = await rhs.RegisterAsync(username, password, "Unit tests!"); + } + else throw new Exception("Failed to register", e); + } + + var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay"); + + var rooms = await hs.GetJoinedRooms(); + + var disbandRoomTasks = rooms.Select(async room => { + // await room.DisbandRoomAsync(); + await room.LeaveAsync(); + await room.ForgetAsync(); + return room; + }).ToList(); + await Task.WhenAll(disbandRoomTasks); + + // foreach (var room in rooms) { + // // await room.DisbandRoomAsync(); + // await room.LeaveAsync(); + // await room.ForgetAsync(); + // } + + return hs; + } + + public static async Task<AuthenticatedHomeserverGeneric> GetRandomHomeserver() { + var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay"); + LoginResponse reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); + var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay"); + + var rooms = await hs.GetJoinedRooms(); + + var disbandRoomTasks = rooms.Select(async room => { + // await room.DisbandRoomAsync(); + await room.LeaveAsync(); + await room.ForgetAsync(); + return room; + }).ToList(); + await Task.WhenAll(disbandRoomTasks); + + return hs; + } + + public static async IAsyncEnumerable<AuthenticatedHomeserverGeneric> GetRandomHomeservers(int count = 1) { + var createSpaceTasks = Enumerable + .Range(0, count) + .Select(_ => GetRandomHomeserver()).ToAsyncEnumerable(); + await foreach (var hs in createSpaceTasks) { + yield return hs; + } + } +} diff --git a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs new file mode 100644 index 0000000..44c35da --- /dev/null +++ b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs @@ -0,0 +1,76 @@ +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; + +namespace LibMatrix.Tests.Abstractions; + +public static class RoomAbstraction { + public static async Task<GenericRoom> GetTestRoom(AuthenticatedHomeserverGeneric hs) { + var testRoom = await hs.CreateRoom(new CreateRoomRequest() { + Name = "LibMatrix Test Room", + // Visibility = CreateRoomVisibility.Public, + RoomAliasName = Guid.NewGuid().ToString() + }); + + await testRoom.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new()); + + return testRoom; + } + + private static SemaphoreSlim _spaceSemaphore = null!; + + public static async Task<SpaceRoom> GetTestSpace(AuthenticatedHomeserverGeneric hs, int roomCount = 100, bool addSpaces = false, int spaceSizeReduction = 10) { + _spaceSemaphore ??= new(roomCount / spaceSizeReduction, roomCount / spaceSizeReduction); + var crq = new CreateRoomRequest() { + Name = $"LibMatrix Test Space ({roomCount} children)", + // Visibility = CreateRoomVisibility.Public, + RoomAliasName = Guid.NewGuid().ToString(), + InitialState = new() + }; + crq._creationContentBaseType.Type = "m.space"; + + + var createRoomTasks = Enumerable.Range(0, roomCount) + .Select(_ => hs.CreateRoom(new CreateRoomRequest() { + Name = $"LibMatrix Test Room {Guid.NewGuid()}", + // Visibility = CreateRoomVisibility.Public, + RoomAliasName = Guid.NewGuid().ToString() + })).ToAsyncEnumerable(); + + await foreach (var room in createRoomTasks) { + crq.InitialState.Add(new() { + Type = "m.space.child", + StateKey = room.RoomId, + TypedContent = new SpaceChildEventContent() { + Via = new() { + room.RoomId.Split(":")[1] + } + } + }); + } + + if (addSpaces) { + for (int i = 0; i < roomCount; i++) { + var space = await GetTestSpace(hs, roomCount - spaceSizeReduction, true, spaceSizeReduction); + crq.InitialState.Add(new() { + Type = "m.space.child", + StateKey = space.RoomId, + TypedContent = new SpaceChildEventContent() { + Via = new() { + space.RoomId.Split(":")[1] + } + } + }); + } + } + + var testSpace = (await hs.CreateRoom(crq)).AsSpace; + + await testSpace.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new()); + + // _spaceSemaphore.Release(); + return testSpace; + } +} diff --git a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs new file mode 100644 index 0000000..f737363 --- /dev/null +++ b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs @@ -0,0 +1,10 @@ +namespace LibMatrix.Tests.DataTests; + +public static class WhoAmITests { + public static void VerifyRequiredFields(this WhoAmIResponse obj, bool isAppservice = false) { + Assert.NotNull(obj); + Assert.NotNull(obj.UserId); + if(!isAppservice) + Assert.NotNull(obj.DeviceId); + } +} diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs index 72a509d..5476b84 100644 --- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs +++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs @@ -1,4 +1,5 @@ using LibMatrix.Services; +using LibMatrix.Tests.DataTests; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; using Xunit.Microsoft.DependencyInjection.Abstracts; @@ -45,6 +46,24 @@ public class AuthTests : TestBed<TestFixture> { var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken); Assert.NotNull(hs); Assert.NotNull(hs.WhoAmI); + hs.WhoAmI.VerifyRequiredFields(); + Assert.NotNull(hs.UserId); + Assert.NotNull(hs.AccessToken); + await hs.Logout(); + } + + [Fact] + public async Task RegisterAsync() { + var rhs = await _provider.GetRemoteHomeserver("matrixunittests.rory.gay"); + var reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!"); + Assert.NotNull(reg); + Assert.NotNull(reg.AccessToken); + Assert.NotNull(reg.DeviceId); + Assert.NotNull(reg.UserId); + var hs = await reg.GetAuthenticatedHomeserver(); + Assert.NotNull(hs); + Assert.NotNull(hs.WhoAmI); + hs.WhoAmI.VerifyRequiredFields(); Assert.NotNull(hs.UserId); Assert.NotNull(hs.AccessToken); await hs.Logout(); diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs index ef63ec9..17c219d 100644 --- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs +++ b/Tests/LibMatrix.Tests/Tests/RoomTests.cs @@ -1,7 +1,9 @@ +using System.Text; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Homeservers; using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; using Xunit.Microsoft.DependencyInjection.Abstracts; @@ -22,34 +24,29 @@ public class RoomTests : TestBed<TestFixture> { } private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() { - Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!"); - Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!"); - Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!"); - - // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!); - var login = await _provider.Login(_config.TestHomeserver!, _config.TestUsername!, _config.TestPassword!); - Assert.NotNull(login); - - var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken); - return hs; + return await HomeserverAbstraction.GetHomeserver(); } [Fact] public async Task GetJoinedRoomsAsync() { - var hs = await GetHomeserver(); + var hs = await HomeserverAbstraction.GetHomeserver(); + //make 100 rooms + var createRoomTasks = Enumerable.Range(0, 100).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); + await Task.WhenAll(createRoomTasks); var rooms = await hs.GetJoinedRooms(); Assert.NotNull(rooms); Assert.NotEmpty(rooms); Assert.All(rooms, Assert.NotNull); + Assert.Equal(100, rooms.Count); await hs.Logout(); } [Fact] public async Task GetNameAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var name = await room.GetNameAsync(); Assert.NotNull(name); @@ -58,8 +55,8 @@ public class RoomTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetTopicAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var topic = await room.GetTopicAsync(); Assert.NotNull(topic); @@ -72,8 +69,8 @@ public class RoomTests : TestBed<TestFixture> { Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!"); Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!"); - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var members = room.GetMembersAsync(); Assert.NotNull(members); @@ -98,41 +95,10 @@ public class RoomTests : TestBed<TestFixture> { Assert.True(hitMembers, "No members were found in the room"); } - /* - tests remaining: - GetStateAsync(string,string) 0% 8/8 - GetMessagesAsync(string,int,string,string) 0% 7/7 - JoinAsync(string[],string) 0% 8/8 - SendMessageEventAsync(RoomMessageEventContent) 0% 1/1 - GetAliasesAsync() 0% 4/4 - GetCanonicalAliasAsync() 0% 1/1 - GetAvatarUrlAsync() 0% 1/1 - GetJoinRuleAsync() 0% 1/1 - GetHistoryVisibilityAsync() 0% 1/1 - GetGuestAccessAsync() 0% 1/1 - GetCreateEventAsync() 0% 1/1 - GetRoomType() 0% 4/4 - GetPowerLevelsAsync() 0% 1/1 - ForgetAsync() 0% 1/1 - LeaveAsync(string) 0% 1/1 - KickAsync(string,string) 0% 1/1 - BanAsync(string,string) 0% 1/1 - UnbanAsync(string) 0% 1/1 - SendStateEventAsync(string,object) 0% 1/1 - SendStateEventAsync(string,string,object) 0% 1/1 - SendTimelineEventAsync(string,EventContent) 0% 5/5 - SendFileAsync(string,string,Stream) 0% 6/6 - GetRoomAccountData<T>(string) 0% 8/8 - SetRoomAccountData(string,object) 0% 7/7 - GetEvent<T>(string) 0% 3/3 - RedactEventAsync(string,string) 0% 4/4 - InviteUser(string,string) 0% 3/3 - */ - [Fact] public async Task JoinAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var id = await room.JoinAsync(); Assert.NotNull(id); @@ -142,8 +108,8 @@ public class RoomTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetAliasesAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var aliases = await room.GetAliasesAsync(); Assert.NotNull(aliases); @@ -153,8 +119,8 @@ public class RoomTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetCanonicalAliasAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var alias = await room.GetCanonicalAliasAsync(); Assert.NotNull(alias); @@ -164,8 +130,8 @@ public class RoomTests : TestBed<TestFixture> { [SkippableFact(typeof(MatrixException))] public async Task GetAvatarUrlAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var url = await room.GetAvatarUrlAsync(); Assert.NotNull(url); @@ -175,8 +141,8 @@ public class RoomTests : TestBed<TestFixture> { [Fact] public async Task GetJoinRuleAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var rule = await room.GetJoinRuleAsync(); Assert.NotNull(rule); @@ -186,8 +152,8 @@ public class RoomTests : TestBed<TestFixture> { [Fact] public async Task GetHistoryVisibilityAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var visibility = await room.GetHistoryVisibilityAsync(); Assert.NotNull(visibility); @@ -197,8 +163,8 @@ public class RoomTests : TestBed<TestFixture> { [Fact] public async Task GetGuestAccessAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); try { var access = await room.GetGuestAccessAsync(); @@ -207,15 +173,15 @@ public class RoomTests : TestBed<TestFixture> { Assert.NotEmpty(access.GuestAccess); } catch (Exception e) { - if(e is not MatrixException exception) throw; + if (e is not MatrixException exception) throw; Assert.Equal("M_NOT_FOUND", exception.ErrorCode); } } [Fact] public async Task GetCreateEventAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var create = await room.GetCreateEventAsync(); Assert.NotNull(create); @@ -225,16 +191,16 @@ public class RoomTests : TestBed<TestFixture> { [Fact] public async Task GetRoomType() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); await room.GetRoomType(); } [Fact] public async Task GetPowerLevelsAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); var power = await room.GetPowerLevelsAsync(); Assert.NotNull(power); @@ -246,54 +212,73 @@ public class RoomTests : TestBed<TestFixture> { Assert.NotNull(power.EventsDefault); Assert.NotNull(power.UsersDefault); Assert.NotNull(power.Users); - Assert.NotNull(power.Events); + // Assert.NotNull(power.Events); } - [Fact(Skip = "This test is destructive!")] + [Fact] public async Task ForgetAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); await room.ForgetAsync(); } - [Fact(Skip = "This test is destructive!")] + [Fact] public async Task LeaveAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); await room.LeaveAsync(); } - [Fact(Skip = "This test is destructive!")] + [Fact] public async Task KickAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); - // await room.KickAsync(_config.TestUserId, "test"); + await room.InviteUserAsync(hs2.UserId,"Unit test!"); + await hs2.GetRoom(room.RoomId).JoinAsync(); + await room.KickAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("leave", banState.Membership); } - [Fact(Skip = "This test is destructive!")] + [Fact] public async Task BanAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); - // await room.BanAsync(_config.TestUserId, "test"); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); } - [Fact(Skip = "This test is destructive!")] + [Fact] public async Task UnbanAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var hs2 = await HomeserverAbstraction.GetRandomHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); - // await room.UnbanAsync(_config.TestUserId); + await room.BanAsync(hs2.UserId, "test"); + var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(banState); + Assert.Equal("ban", banState.Membership); + await room.UnbanAsync(hs2.UserId); + var unbanState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId); + Assert.NotNull(unbanState); + Assert.Equal("leave", unbanState.Membership); } [SkippableFact(typeof(MatrixException))] public async Task SendStateEventAsync() { - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new ProfileResponseEventContent() { DisplayName = "wee_woo", AvatarUrl = "no" @@ -305,12 +290,21 @@ public class RoomTests : TestBed<TestFixture> { } [SkippableFact(typeof(MatrixException))] - public async Task GetStateEventAsync() { + public async Task SendAndGetStateEventAsync() { await SendStateEventAsync(); - - var hs = await GetHomeserver(); - var room = hs.GetRoom(_config.TestRoomId); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); Assert.NotNull(room); + + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new ProfileResponseEventContent() { + DisplayName = "wee_woo", + AvatarUrl = "no" + }); + await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new ProfileResponseEventContent() { + DisplayName = "wee_woo", + AvatarUrl = "yes" + }); + var state1 = await room.GetStateAsync<ProfileResponseEventContent>("gay.rory.libmatrix.unit_tests"); Assert.NotNull(state1); Assert.NotNull(state1.DisplayName); @@ -329,4 +323,62 @@ public class RoomTests : TestBed<TestFixture> { Assert.Equal("wee_woo", state2.DisplayName); Assert.Equal("yes", state2.AvatarUrl); } + + [Fact] + public async Task DisbandAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + await room.DisbandRoomAsync(); + } + + [Fact] + public async Task SendFileAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." + + "\nIf you are reading this on matrix, it means the unit test for uploading a file works!"))); + Assert.NotNull(res); + Assert.NotNull(res.EventId); + } + + [Fact] + public async Task GetSpaceChildrenAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var space = await RoomAbstraction.GetTestSpace(hs, 2, false, 1); + Assert.NotNull(space); + var children = space.GetChildrenAsync(); + Assert.NotNull(children); + int found = 0; + await foreach (var room in children) { + found++; + } + Assert.Equal(2, found); + } + + [Fact] + public async Task InviteAndJoinAsync() { + var otherUsers = HomeserverAbstraction.GetRandomHomeservers(7); + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + + // var expectedCount = 1; + + await foreach(var otherUser in otherUsers) { + await room.InviteUserAsync(otherUser.UserId); + await otherUser.GetRoom(room.RoomId).JoinAsync(); + } + + var states = room.GetMembersAsync(false); + var count = 0; + await foreach(var state in states) { + count++; + } + // Assert.Equal(++expectedCount, count); + Assert.Equal(8, count); + } } |