diff options
61 files changed, 1034 insertions, 521 deletions
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs index 0211f74..8cf4f1f 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs @@ -4,6 +4,7 @@ using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State; using LibMatrix.ExampleBot.Bot.Interfaces; using LibMatrix.Extensions; +using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.Services; using Microsoft.Extensions.DependencyInjection; @@ -44,6 +45,8 @@ public class MRUBot : IHostedService { throw; } + var syncHelper = new SyncHelper(hs); + await (hs.GetRoom("!DoHEdFablOLjddKWIp:rory.gay")).JoinAsync(); // foreach (var room in await hs.GetJoinedRooms()) { @@ -54,7 +57,7 @@ public class MRUBot : IHostedService { // _logger.LogInformation($"Got room state for {room.RoomId}!"); // } - hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { + syncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => x.Type == "m.room.member" && x.StateKey == hs.UserId); @@ -71,7 +74,7 @@ public class MRUBot : IHostedService { } } }); - hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + syncHelper.TimelineEventHandlers.Add(async @event => { _logger.LogInformation( "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(indent: false, ignoreNull: true)); @@ -100,7 +103,7 @@ public class MRUBot : IHostedService { } } }); - await hs.SyncHelper.RunSyncLoop(cancellationToken: cancellationToken); + await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationToken); } /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> diff --git a/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs b/ExampleBots/MediaModeratorPoC/AccountData/BotData.cs index b4e1167..0fee4eb 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs +++ b/ExampleBots/MediaModeratorPoC/AccountData/BotData.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace MediaModeratorPoC.Bot.AccountData; +namespace MediaModeratorPoC.AccountData; public class BotData { [JsonPropertyName("control_room")] @@ -8,7 +8,4 @@ public class BotData { [JsonPropertyName("log_room")] public string? LogRoom { get; set; } = ""; - - [JsonPropertyName("policy_room")] - public string? PolicyRoom { get; set; } = ""; } diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs b/ExampleBots/MediaModeratorPoC/Commands/BanMediaCommand.cs index fd6866c..69c0583 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs +++ b/ExampleBots/MediaModeratorPoC/Commands/BanMediaCommand.cs @@ -1,14 +1,14 @@ using System.Security.Cryptography; using ArcaneLibs.Extensions; +using LibMatrix; using LibMatrix.EventTypes.Spec; using LibMatrix.Helpers; -using LibMatrix.Responses; using LibMatrix.Services; using LibMatrix.Utilities.Bot.Interfaces; -using MediaModeratorPoC.Bot.AccountData; -using MediaModeratorPoC.Bot.StateEventTypes; +using MediaModeratorPoC.AccountData; +using MediaModeratorPoC.StateEventTypes; -namespace MediaModeratorPoC.Bot.Commands; +namespace MediaModeratorPoC.Commands; public class BanMediaCommand(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver) : ICommand { public string Name { get; } = "banmedia"; @@ -16,7 +16,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic public async Task<bool> CanInvoke(CommandContext ctx) { //check if user is admin in control room - var botData = await ctx.Homeserver.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); + var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.modbot_data"); var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); if (!isAdmin) { @@ -29,7 +29,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic } public async Task Invoke(CommandContext ctx) { - var botData = await ctx.Homeserver.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); + var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.modbot_data"); var policyRoom = ctx.Homeserver.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); var logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); @@ -54,7 +54,9 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic var recommendation = ctx.Args[0]; if (recommendation is not ("ban" or "kick" or "mute" or "redact" or "spoiler" or "warn" or "warn_admins")) { - await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatError($"Invalid recommendation type {recommendation}, must be `warn_admins`, `warn`, `spoiler`, `redact`, `mute`, `kick` or `ban`!")); + await ctx.Room.SendMessageEventAsync( + MessageFormatter.FormatError( + $"Invalid recommendation type {recommendation}, must be `warn_admins`, `warn`, `spoiler`, `redact`, `mute`, `kick` or `ban`!")); return; } @@ -70,16 +72,16 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic } catch (Exception ex) { await logRoom.SendMessageEventAsync( - MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]}, retrying via {ctx.Homeserver.HomeServerDomain}...", + MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]}, retrying via {ctx.Homeserver.BaseUrl}...", ex)); try { - resolvedUri = await hsResolver.ResolveMediaUri(ctx.Homeserver.HomeServerDomain, mxcUri); + resolvedUri = await hsResolver.ResolveMediaUri(ctx.Homeserver.BaseUrl, mxcUri); fileHash = await hashAlgo.ComputeHashAsync(await ctx.Homeserver._httpClient.GetStreamAsync(resolvedUri)); } catch (Exception ex2) { await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatException("Error calculating file hash", ex2)); await logRoom.SendMessageEventAsync( - MessageFormatter.FormatException($"Error calculating file hash via {ctx.Homeserver.HomeServerDomain}!", ex2)); + MessageFormatter.FormatException($"Error calculating file hash via {ctx.Homeserver.BaseUrl}!", ex2)); } } @@ -98,7 +100,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic await logRoom.SendMessageEventAsync(MessageFormatter.FormatException("Error creating policy", e)); await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatException("Error creating policy", e)); await using var stream = new MemoryStream(e.ToString().AsBytes().ToArray()); - await logRoom.SendFileAsync("m.file", "error.log.cs", stream); + await logRoom.SendFileAsync("error.log.cs", stream); } } else { diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs b/ExampleBots/MediaModeratorPoC/MediaModBot.cs index f9bbcf3..0aacf61 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs +++ b/ExampleBots/MediaModeratorPoC/MediaModBot.cs @@ -1,8 +1,4 @@ -using System.Buffers.Text; -using System.Data; using System.Security.Cryptography; -using System.Text; -using System.Text.Encodings.Web; using System.Text.RegularExpressions; using ArcaneLibs.Extensions; using LibMatrix; @@ -14,12 +10,12 @@ using LibMatrix.Responses; using LibMatrix.RoomTypes; using LibMatrix.Services; using LibMatrix.Utilities.Bot.Interfaces; -using MediaModeratorPoC.Bot.AccountData; -using MediaModeratorPoC.Bot.StateEventTypes; +using MediaModeratorPoC.AccountData; +using MediaModeratorPoC.StateEventTypes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace MediaModeratorPoC.Bot; +namespace MediaModeratorPoC; public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> logger, MediaModBotConfiguration configuration, HomeserverResolverService hsResolver) : IHostedService { @@ -31,6 +27,8 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> private GenericRoom _logRoom; private GenericRoom _controlRoom; + + /// <summary>Triggered when the application host is ready to start the service.</summary> /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> public async Task StartAsync(CancellationToken cancellationToken) { @@ -44,7 +42,7 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> BotData botData; try { - botData = await hs.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); + botData = await hs.GetAccountDataAsync<BotData>("gay.rory.modbot_data"); } catch (Exception e) { if (e is not MatrixException { ErrorCode: "M_NOT_FOUND" }) { @@ -63,10 +61,10 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> creationContent.InitialState.Add(new StateEvent { Type = "m.room.join_rules", StateKey = "", - TypedContent = new JoinRulesEventContent { + TypedContent = new RoomJoinRulesEventContent { JoinRule = "knock_restricted", Allow = new() { - new JoinRulesEventContent.AllowEntry { + new RoomJoinRulesEventContent.AllowEntry { Type = "m.room_membership", RoomId = botData.ControlRoom } @@ -84,12 +82,13 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> creationContent.CreationContent["type"] = "gay.rory.media_moderator_poc.policy_room"; botData.PolicyRoom = (await hs.CreateRoom(creationContent)).RoomId; - await hs.SetAccountData("gay.rory.media_moderator_poc_data", botData); + await hs.SetAccountData("gay.rory.modbot_data", botData); } _policyRoom = hs.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); _logRoom = hs.GetRoom(botData.LogRoom ?? botData.ControlRoom); _controlRoom = hs.GetRoom(botData.ControlRoom); + var syncHelper = new SyncHelper(hs); List<string> admins = new(); @@ -98,7 +97,9 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> while (!cancellationToken.IsCancellationRequested) { var controlRoomMembers = _controlRoom.GetMembersAsync(); await foreach (var member in controlRoomMembers) { - if ((member.TypedContent as RoomMemberEventContent).Membership == "join") admins.Add(member.UserId); + if ((member.TypedContent as RoomMemberEventContent)? + + .Membership == "join") admins.Add(member.UserId); } await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); @@ -106,13 +107,12 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> }, cancellationToken); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { + syncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => 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")) { + logger.LogInformation("Got invite to {RoomId} by {Sender} with reason: {Reason}", args.Key, inviteEvent!.Sender, (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}!"); @@ -124,7 +124,7 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> } }); - hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + syncHelper.TimelineEventHandlers.Add(async @event => { var room = hs.GetRoom(@event.RoomId); try { logger.LogInformation( @@ -256,8 +256,8 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> await _logRoom.SendMessageEventAsync( MessageFormatter.FormatException($"Unable to ban user in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); await using var stream = new MemoryStream(e.ToString().AsBytes().ToArray()); - await _controlRoom.SendFileAsync("m.file", "error.log.cs", stream); - await _logRoom.SendFileAsync("m.file", "error.log.cs", stream); + await _controlRoom.SendFileAsync("error.log.cs", stream); + await _logRoom.SendFileAsync("error.log.cs", stream); } }); } @@ -282,15 +282,15 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> } catch (Exception ex) { await _logRoom.SendMessageEventAsync( - MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]} ({resolvedUri}), retrying via {hs.HomeServerDomain}...", + MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]} ({resolvedUri}), retrying via {hs.BaseUrl}...", ex)); try { - resolvedUri = await hsResolver.ResolveMediaUri(hs.HomeServerDomain, mxcUri); + resolvedUri = await hsResolver.ResolveMediaUri(hs.BaseUrl, mxcUri); fileHash = await hashAlgo.ComputeHashAsync(await hs._httpClient.GetStreamAsync(resolvedUri)); } catch (Exception ex2) { await _logRoom.SendMessageEventAsync( - MessageFormatter.FormatException($"Error calculating file hash via {hs.HomeServerDomain} ({resolvedUri})!", ex2)); + MessageFormatter.FormatException($"Error calculating file hash via {hs.BaseUrl} ({resolvedUri})!", ex2)); } } diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs b/ExampleBots/MediaModeratorPoC/MediaModBotConfiguration.cs index d848abe..cb5b596 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs +++ b/ExampleBots/MediaModeratorPoC/MediaModBotConfiguration.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; -namespace MediaModeratorPoC.Bot; +namespace MediaModeratorPoC; public class MediaModBotConfiguration { public MediaModBotConfiguration(IConfiguration config) => config.GetRequiredSection("MediaMod").Bind(this); diff --git a/ExampleBots/MediaModeratorPoC/PolicyEngine.cs b/ExampleBots/MediaModeratorPoC/PolicyEngine.cs new file mode 100644 index 0000000..0a0a565 --- /dev/null +++ b/ExampleBots/MediaModeratorPoC/PolicyEngine.cs @@ -0,0 +1,86 @@ +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using LibMatrix.Services; +using MediaModeratorPoC.AccountData; +using MediaModeratorPoC.StateEventTypes; +using Microsoft.Extensions.Logging; + +namespace MediaModeratorPoC; + +public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> logger, MediaModBotConfiguration configuration, + HomeserverResolverService hsResolver) { + public List<PolicyList> ActivePolicyLists { get; set; } = new(); + private GenericRoom? _logRoom; + private GenericRoom? _controlRoom; + + public async Task ReloadActivePolicyLists() { + // first time init + if (_logRoom is null || _controlRoom is null) { + var botData = await hs.GetAccountDataAsync<BotData>("gay.rory.modbot_data"); + _logRoom ??= hs.GetRoom(botData.LogRoom ?? botData.ControlRoom); + _controlRoom ??= hs.GetRoom(botData.ControlRoom); + } + + await _controlRoom?.SendMessageEventAsync(MessageFormatter.FormatSuccess("Reloading policy lists!"))!; + await _logRoom?.SendMessageEventAsync( + new RoomMessageEventContent( + body: "Reloading policy lists!", + messageType: "m.text"))!; + + await _controlRoom?.SendMessageEventAsync(MessageFormatter.FormatSuccess("0/? policy lists loaded"))!; + + var policyLists = new List<PolicyList>(); + var policyListAccountData = await hs.GetAccountDataAsync<Dictionary<string, PolicyList>>("gay.rory.modbot.policy_lists"); + foreach (var (roomId, policyList) in policyListAccountData) { + _logRoom?.SendMessageEventAsync( + new RoomMessageEventContent( + body: $"Loading policy list {MessageFormatter.HtmlFormatMention(roomId)}!", + messageType: "m.text")); + var room = hs.GetRoom(roomId); + + policyList.Room = room; + + var stateEvents = room.GetFullStateAsync(); + await foreach (var stateEvent in stateEvents) { + if (stateEvent != null && stateEvent.GetType.IsAssignableTo(typeof(BasePolicy))) { + policyList.Policies.Add(stateEvent); + } + } + + //html table of policy count by type + var policyCount = policyList.Policies.GroupBy(x => x.Type).ToDictionary(x => x.Key, x => x.Count()); + var policyCountTable = policyCount.Aggregate( + "<table><tr><th>Policy Type</th><th>Count</th></tr>", + (current, policy) => current + $"<tr><td>{policy.Key}</td><td>{policy.Value}</td></tr>"); + policyCountTable += "</table>"; + + var policyCountTablePlainText = policyCount.Aggregate( + "Policy Type | Count\n", + (current, policy) => current + $"{policy.Key,-16} | {policy.Value}\n"); + await _logRoom?.SendMessageEventAsync( + new RoomMessageEventContent() { + MessageType = "org.matrix.custom.html", + Body = $"Policy count for {roomId}:\n{policyCountTablePlainText}", + FormattedBody = $"Policy count for {MessageFormatter.HtmlFormatMention(roomId)}:\n{policyCountTable}", + })!; + + await _logRoom?.SendMessageEventAsync( + new RoomMessageEventContent( + body: $"Loaded {policyList.Policies.Count} policies for {MessageFormatter.HtmlFormatMention(roomId)}!", + messageType: "m.text"))!; + + policyLists.Add(policyList); + + var progressMsgContent = MessageFormatter.FormatSuccess($"{policyLists.Count}/{policyListAccountData.Count} policy lists loaded"); + //edit old message + progressMsgContent.RelatesTo = new() { + + }; + _controlRoom?.SendMessageEventAsync(progressMsgContent); + } + + ActivePolicyLists = policyLists; + } +} diff --git a/ExampleBots/MediaModeratorPoC/PolicyList.cs b/ExampleBots/MediaModeratorPoC/PolicyList.cs new file mode 100644 index 0000000..0f49c97 --- /dev/null +++ b/ExampleBots/MediaModeratorPoC/PolicyList.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using LibMatrix; +using LibMatrix.RoomTypes; +using MediaModeratorPoC.StateEventTypes; + +namespace MediaModeratorPoC; + +public class PolicyList { + [JsonIgnore] + public GenericRoom Room { get; set; } + + [JsonPropertyName("trusted")] + public bool Trusted { get; set; } = false; + + [JsonIgnore] + public List<StateEvent> Policies { get; set; } = new(); +} diff --git a/ExampleBots/MediaModeratorPoC/Program.cs b/ExampleBots/MediaModeratorPoC/Program.cs index 413d91d..5b8e734 100644 --- a/ExampleBots/MediaModeratorPoC/Program.cs +++ b/ExampleBots/MediaModeratorPoC/Program.cs @@ -2,7 +2,7 @@ using LibMatrix.Services; using LibMatrix.Utilities.Bot; -using MediaModeratorPoC.Bot; +using MediaModeratorPoC; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs b/ExampleBots/MediaModeratorPoC/StateEventTypes/BasePolicy.cs index 0096c78..048c1d0 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs +++ b/ExampleBots/MediaModeratorPoC/StateEventTypes/BasePolicy.cs @@ -1,27 +1,18 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using LibMatrix.Helpers; -using LibMatrix.Interfaces; +using LibMatrix; -namespace MediaModeratorPoC.Bot.StateEventTypes; +namespace MediaModeratorPoC.StateEventTypes; -[ - MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")] -[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.media")] -public class MediaPolicyEventContent : EventContent { +public abstract class BasePolicy : StateEvent { /// <summary> - /// This is an MXC URI, hashed with SHA3-256. + /// Entity this policy applies to /// </summary> [JsonPropertyName("entity")] - public byte[] Entity { get; set; } + public string Entity { get; set; } /// <summary> - /// Server this ban applies to, can use * and ? as globs. - /// </summary> - [JsonPropertyName("server_entity")] - public string? ServerEntity { get; set; } - - /// <summary> - /// Reason this user is banned + /// Reason this policy exists /// </summary> [JsonPropertyName("reason")] public string? Reason { get; set; } @@ -30,6 +21,7 @@ public class MediaPolicyEventContent : EventContent { /// Suggested action to take, one of `ban`, `kick`, `mute`, `redact`, `spoiler`, `warn` or `warn_admins` /// </summary> [JsonPropertyName("recommendation")] + [AllowedValues("ban", "kick", "mute", "redact", "spoiler", "warn", "warn_admins")] public string Recommendation { get; set; } = "warn"; /// <summary> @@ -47,7 +39,4 @@ public class MediaPolicyEventContent : EventContent { get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime; set => Expiry = value is null ? null : ((DateTimeOffset)value).ToUnixTimeMilliseconds(); } - - [JsonPropertyName("file_hash")] - public byte[]? FileHash { get; set; } } diff --git a/ExampleBots/MediaModeratorPoC/StateEventTypes/MediaPolicyStateEventData.cs b/ExampleBots/MediaModeratorPoC/StateEventTypes/MediaPolicyStateEventData.cs new file mode 100644 index 0000000..603a858 --- /dev/null +++ b/ExampleBots/MediaModeratorPoC/StateEventTypes/MediaPolicyStateEventData.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using LibMatrix.EventTypes; + +namespace MediaModeratorPoC.StateEventTypes; + +/// <summary> +/// File policy event, entity is the MXC URI of the file, hashed with SHA3-256. +/// </summary> +[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")] +[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.media")] +public class MediaPolicyEventContent : BasePolicy { + /// <summary> + /// Hash of the file + /// </summary> + [JsonPropertyName("file_hash")] + public byte[]? FileHash { get; set; } +} diff --git a/ExampleBots/PluralContactBotPoC/Bot/AccountData/BotData.cs b/ExampleBots/PluralContactBotPoC/Bot/AccountData/BotData.cs index 9477488..5d11432 100644 --- a/ExampleBots/PluralContactBotPoC/Bot/AccountData/BotData.cs +++ b/ExampleBots/PluralContactBotPoC/Bot/AccountData/BotData.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LibMatrix.EventTypes; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/ExampleBots/PluralContactBotPoC/Bot/AccountData/SystemData.cs b/ExampleBots/PluralContactBotPoC/Bot/AccountData/SystemData.cs index 5edfc0e..42edd23 100644 --- a/ExampleBots/PluralContactBotPoC/Bot/AccountData/SystemData.cs +++ b/ExampleBots/PluralContactBotPoC/Bot/AccountData/SystemData.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LibMatrix.EventTypes; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/ExampleBots/PluralContactBotPoC/Bot/Commands/CreateSystemCommand.cs b/ExampleBots/PluralContactBotPoC/Bot/Commands/CreateSystemCommand.cs index 55624a8..4a7d646 100644 --- a/ExampleBots/PluralContactBotPoC/Bot/Commands/CreateSystemCommand.cs +++ b/ExampleBots/PluralContactBotPoC/Bot/Commands/CreateSystemCommand.cs @@ -25,7 +25,7 @@ public class CreateSystemCommand(IServiceProvider services, HomeserverProviderSe var sysName = ctx.Args[0]; try { try { - await ctx.Homeserver.GetAccountData<BotData>("gay.rory.plural_contact_bot.system_data"); + await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.plural_contact_bot.system_data"); await ctx.Reply(MessageFormatter.FormatError($"System {sysName} already exists!")); } catch (MatrixException e) { diff --git a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs index 0bd2bbf..231af95 100644 --- a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs +++ b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs @@ -38,14 +38,16 @@ public class PluralContactBot(AuthenticatedHomeserverGeneric hs, ILogger<PluralC _logRoom = hs.GetRoom(botConfiguration.LogRoom); - hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { + var syncHelper = new SyncHelper(hs); + + syncHelper.InviteReceivedHandlers.Add(async Task (args) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => 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 { - var accountData = await hs.GetAccountData<SystemData>($"gay.rory.plural_contact_bot.system_data#{inviteEvent.StateKey}"); + var accountData = await hs.GetAccountDataAsync<SystemData>($"gay.rory.plural_contact_bot.system_data#{inviteEvent.StateKey}"); if (accountData.Members.Contains(inviteEvent.Sender)) { await (hs.GetRoom(args.Key)).JoinAsync(reason: "I was invited by a system member!"); @@ -74,7 +76,7 @@ public class PluralContactBot(AuthenticatedHomeserverGeneric hs, ILogger<PluralC } }); - hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + syncHelper.TimelineEventHandlers.Add(async @event => { var room = hs.GetRoom(@event.RoomId); try { logger.LogInformation( @@ -87,7 +89,7 @@ public class PluralContactBot(AuthenticatedHomeserverGeneric hs, ILogger<PluralC await _logRoom.SendMessageEventAsync( MessageFormatter.FormatException($"Exception handling event {@event.EventId} by {@event.Sender} in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(e.ToString())); - await _logRoom.SendFileAsync("m.file", "error.log.cs", stream); + await _logRoom.SendFileAsync("error.log.cs", stream); } }); } diff --git a/LibMatrix/EventTypes/Common/MjolnirShortcodeEventData.cs b/LibMatrix/EventTypes/Common/MjolnirShortcodeEventContent.cs index 9067351..9067351 100644 --- a/LibMatrix/EventTypes/Common/MjolnirShortcodeEventData.cs +++ b/LibMatrix/EventTypes/Common/MjolnirShortcodeEventContent.cs diff --git a/LibMatrix/EventTypes/Common/RoomEmotesEventData.cs b/LibMatrix/EventTypes/Common/RoomEmotesEventContent.cs index abf936c..abf936c 100644 --- a/LibMatrix/EventTypes/Common/RoomEmotesEventData.cs +++ b/LibMatrix/EventTypes/Common/RoomEmotesEventContent.cs diff --git a/LibMatrix/Helpers/MatrixEventAttribute.cs b/LibMatrix/EventTypes/MatrixEventAttribute.cs index 7efc039..92334d0 100644 --- a/LibMatrix/Helpers/MatrixEventAttribute.cs +++ b/LibMatrix/EventTypes/MatrixEventAttribute.cs @@ -1,4 +1,4 @@ -namespace LibMatrix.Helpers; +namespace LibMatrix.EventTypes; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class MatrixEventAttribute : Attribute { diff --git a/LibMatrix/EventTypes/Spec/State/PresenceStateEventData.cs b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs index b12da5b..b12da5b 100644 --- a/LibMatrix/EventTypes/Spec/State/PresenceStateEventData.cs +++ b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomTypingEventData.cs b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs index 01cfacf..01cfacf 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomTypingEventData.cs +++ b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs index f8ee58b..f8ee58b 100644 --- a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs +++ b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/PolicyRuleStateEventData.cs b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs index fde02c1..fde02c1 100644 --- a/LibMatrix/EventTypes/Spec/State/PolicyRuleStateEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/ProfileResponseEventData.cs b/LibMatrix/EventTypes/Spec/State/ProfileResponseEventContent.cs index 893fce1..893fce1 100644 --- a/LibMatrix/EventTypes/Spec/State/ProfileResponseEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/ProfileResponseEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomAliasEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs index 5b0e914..5b0e914 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomAliasEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomAvatarEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs index 601d014..601d014 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomAvatarEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/CanonicalAliasEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs index 71f3d0d..046222e 100644 --- a/LibMatrix/EventTypes/Spec/State/CanonicalAliasEventContent.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.canonical_alias")] -public class CanonicalAliasEventContent : EventContent { +public class RoomCanonicalAliasEventContent : EventContent { [JsonPropertyName("alias")] public string? Alias { get; set; } [JsonPropertyName("alt_aliases")] diff --git a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs index c5bf14e..c5bf14e 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomEncryptionEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs index 6ffa4c5..6ffa4c5 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomEncryptionEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomEncryptionEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/GuestAccessEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs index af1b2ce..2bb4d36 100644 --- a/LibMatrix/EventTypes/Spec/State/GuestAccessEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomGuestAccessEventContent.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.guest_access")] -public class GuestAccessEventContent : EventContent { +public class RoomGuestAccessEventContent : EventContent { [JsonPropertyName("guest_access")] public string GuestAccess { get; set; } diff --git a/LibMatrix/EventTypes/Spec/State/HistoryVisibilityEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs index b57ade5..a32fed2 100644 --- a/LibMatrix/EventTypes/Spec/State/HistoryVisibilityEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomHistoryVisibilityEventContent.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.history_visibility")] -public class HistoryVisibilityEventContent : EventContent { +public class RoomHistoryVisibilityEventContent : EventContent { [JsonPropertyName("history_visibility")] public string HistoryVisibility { get; set; } } diff --git a/LibMatrix/EventTypes/Spec/State/JoinRulesEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs index 0098bef..2c2a91b 100644 --- a/LibMatrix/EventTypes/Spec/State/JoinRulesEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomJoinRulesEventContent.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.join_rules")] -public class JoinRulesEventContent : EventContent { +public class RoomJoinRulesEventContent : EventContent { private static string Public = "public"; private static string Invite = "invite"; private static string Knock = "knock"; diff --git a/LibMatrix/EventTypes/Spec/State/RoomMemberEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs index da158f1..52cb293 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomMemberEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomMemberEventContent.cs @@ -13,7 +13,7 @@ public class RoomMemberEventContent : EventContent { public string Membership { get; set; } = null!; [JsonPropertyName("displayname")] - public string? Displayname { get; set; } + public string? DisplayName { get; set; } [JsonPropertyName("is_direct")] public bool? IsDirect { get; set; } diff --git a/LibMatrix/EventTypes/Spec/State/RoomNameEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs index 7cb881a..7cb881a 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomNameEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomPinnedEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs index eb02cc7..eb02cc7 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomPinnedEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPinnedEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs index 2ae9593..2ae9593 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/ServerACLEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs index f18fe43..5c5627c 100644 --- a/LibMatrix/EventTypes/Spec/State/ServerACLEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomServerACLEventContent.cs @@ -5,7 +5,7 @@ using LibMatrix.Interfaces; namespace LibMatrix.EventTypes.Spec.State; [MatrixEvent(EventName = "m.room.server_acl")] -public class ServerACLEventContent : EventContent { +public class RoomServerACLEventContent : EventContent { [JsonPropertyName("allow")] public List<string> Allow { get; set; } // = null!; diff --git a/LibMatrix/EventTypes/Spec/State/RoomTopicEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs index 52c7e42..52c7e42 100644 --- a/LibMatrix/EventTypes/Spec/State/RoomTopicEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomTopicEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs b/LibMatrix/EventTypes/Spec/State/Space/SpaceChildEventContent.cs index 0a897dc..0a897dc 100644 --- a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/Space/SpaceChildEventContent.cs diff --git a/LibMatrix/EventTypes/Spec/State/SpaceParentEventData.cs b/LibMatrix/EventTypes/Spec/State/Space/SpaceParentEventContent.cs index 0ffa193..0ffa193 100644 --- a/LibMatrix/EventTypes/Spec/State/SpaceParentEventData.cs +++ b/LibMatrix/EventTypes/Spec/State/Space/SpaceParentEventContent.cs diff --git a/LibMatrix/EventTypes/UnknownStateEventData.cs b/LibMatrix/EventTypes/UnknownStateEventContent.cs index 9a276c8..9a276c8 100644 --- a/LibMatrix/EventTypes/UnknownStateEventData.cs +++ b/LibMatrix/EventTypes/UnknownStateEventContent.cs diff --git a/LibMatrix/Extensions/EnumerableExtensions.cs b/LibMatrix/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..d9619b7 --- /dev/null +++ b/LibMatrix/Extensions/EnumerableExtensions.cs @@ -0,0 +1,28 @@ +namespace LibMatrix.Extensions; + +public static class EnumerableExtensions { + public static void MergeStateEventLists(this List<StateEvent> oldState, List<StateEvent> newState) { + foreach (var stateEvent in newState) { + var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey); + if (old is null) { + oldState.Add(stateEvent); + continue; + } + oldState.Remove(old); + oldState.Add(stateEvent); + } + } + + public static void MergeStateEventLists(this List<StateEventResponse> oldState, List<StateEventResponse> newState) { + foreach (var stateEvent in newState) { + var old = oldState.FirstOrDefault(x => x.Type == stateEvent.Type && x.StateKey == stateEvent.StateKey); + if (old is null) { + oldState.Add(stateEvent); + continue; + } + oldState.Remove(old); + oldState.Add(stateEvent); + } + } + +} diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index a5eb40f..2fe99b6 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -68,7 +68,16 @@ public class MatrixHttpClient : HttpClient { var response = await SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); - return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken); +#if DEBUG && false // This is only used for testing, so it's disabled by default + try { + await PostAsync("http://localhost:5116/validate/" + typeof(T).AssemblyQualifiedName, new StreamContent(responseStream), cancellationToken); + } + catch (Exception e) { + Console.WriteLine("[!!] Checking sync response failed: " + e); + } +#endif + return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken) ?? + throw new InvalidOperationException("Failed to deserialize response"); } // GetStreamAsync @@ -80,7 +89,8 @@ public class MatrixHttpClient : HttpClient { return await response.Content.ReadAsStreamAsync(cancellationToken); } - public new async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + public new async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) { var request = new HttpRequestMessage(HttpMethod.Put, requestUri); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType()), Encoding.UTF8, "application/json"); diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs index ae02afc..d252e85 100644 --- a/LibMatrix/Helpers/MessageFormatter.cs +++ b/LibMatrix/Helpers/MessageFormatter.cs @@ -13,8 +13,7 @@ public static class MessageFormatter { public static RoomMessageEventContent FormatException(string error, Exception e) { return new RoomMessageEventContent(body: $"{error}: {e.Message}", messageType: "m.text") { - FormattedBody = $"<font color=\"#FF0000\">{error}: <pre>{e.Message}</pre>" + - $"</font>", + FormattedBody = $"<font color=\"#FF0000\">{error}: <pre>{e.Message}</pre></font>", Format = "org.matrix.custom.html" }; } @@ -36,4 +35,10 @@ public static class MessageFormatter { public static string HtmlFormatMention(string id, string? displayName = null) { return $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; } + +#region Extension functions + + public static RoomMessageEventContent ToMatrixMessage(this Exception e, string error) => FormatException(error, e); + +#endregion } diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs index 74972a1..06ae3fe 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -1,228 +1,115 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Http.Json; -using System.Text.Json.Serialization; using ArcaneLibs.Extensions; using LibMatrix.Filters; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.Services; +using Microsoft.Extensions.Logging; namespace LibMatrix.Helpers; -public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) { - public async Task<SyncResult?> Sync( - string? since = null, - int? timeout = 30000, - string? setPresence = "online", - SyncFilter? filter = null, - CancellationToken? cancellationToken = null) { - var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}"; - if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}"; - if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}"; - // else url += "&full_state=true"; - Console.WriteLine("Calling: " + url); - try { - var req = await homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None); +public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) { + public string? Since { get; set; } + public int Timeout { get; set; } = 30000; + public string? SetPresence { get; set; } = "online"; + public SyncFilter? Filter { get; set; } + public bool FullState { get; set; } = false; -#if DEBUG && false - try { - 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 = await req.Content.ReadFromJsonAsync<SyncResult>(); - return res; -#else - return await req.Content.ReadFromJsonAsync<SyncResult>(); -#endif + public async Task<SyncResponse?> SyncAsync(CancellationToken? cancellationToken = null) { + var url = $"/_matrix/client/v3/sync?timeout={Timeout}&set_presence={SetPresence}&full_state={(FullState ? "true" : "false")}"; + if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}"; + if (Filter is not null) url += $"&filter={Filter.ToJson(ignoreNull: true, indent: false)}"; + // Console.WriteLine("Calling: " + url); + logger?.LogInformation("SyncHelper: Calling: {}", url); + try { + return await homeserver._httpClient.GetFromJsonAsync<SyncResponse>(url, cancellationToken: cancellationToken ?? CancellationToken.None); } catch (TaskCanceledException) { Console.WriteLine("Sync cancelled!"); + logger?.LogWarning("Sync cancelled due to TaskCanceledException!"); } catch (Exception e) { Console.WriteLine(e); + logger?.LogError(e, "Failed to sync!\n{}", e.ToString()); } return null; } - [SuppressMessage("ReSharper", "FunctionNeverReturns")] - public async Task RunSyncLoop( - bool skipInitialSyncEvents = true, - string? since = null, - int? timeout = 30000, - string? setPresence = "online", - SyncFilter? filter = null, - CancellationToken? cancellationToken = null - ) { - // await Task.WhenAll((await storageService.CacheStorageProvider.GetAllKeysAsync()) - // .Where(x => x.StartsWith("sync")) - // .ToList() - // .Select(x => storageService.CacheStorageProvider.DeleteObjectAsync(x))); - var nextBatch = since; - while (cancellationToken is null || !cancellationToken.Value.IsCancellationRequested) { - var sync = await Sync(since: nextBatch, timeout: timeout, setPresence: setPresence, filter: filter, - cancellationToken: cancellationToken); - nextBatch = sync?.NextBatch ?? nextBatch; + public async IAsyncEnumerable<SyncResponse> EnumerateSyncAsync(CancellationToken? cancellationToken = null) { + while(!cancellationToken?.IsCancellationRequested ?? true) { + var sync = await SyncAsync(cancellationToken); if (sync is null) continue; - Console.WriteLine($"Got sync, next batch: {nextBatch}!"); - - if (sync.Rooms is { Invite.Count: > 0 }) { - foreach (var roomInvite in sync.Rooms.Invite) { - var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList(); - await Task.WhenAll(tasks); - } - } - - if (sync.AccountData is { Events: { Count: > 0 } }) { - foreach (var accountDataEvent in sync.AccountData.Events) { - var tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList(); - await Task.WhenAll(tasks); - } - } - - // Things that are skipped on the first sync - if (skipInitialSyncEvents) { - skipInitialSyncEvents = false; - continue; - } - - if (sync.Rooms is { Join.Count: > 0 }) { - foreach (var updatedRoom in sync.Rooms.Join) { - if(updatedRoom.Value.Timeline is null) continue; - foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) { - stateEventResponse.RoomId = updatedRoom.Key; - var tasks = TimelineEventHandlers.Select(x => { - try { - return x(stateEventResponse); - } - catch (Exception e) { - Console.WriteLine(e); - return Task.CompletedTask; - } - }).ToList(); - await Task.WhenAll(tasks); - } - } - } + Since = sync.NextBatch ?? Since; + yield return sync; } } - /// <summary> - /// Event fired when a room invite is received - /// </summary> - public List<Func<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>, Task>> - InviteReceivedHandlers { get; } = new(); - - public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new(); - public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); -} - -public class SyncResult { - [JsonPropertyName("next_batch")] - public string NextBatch { get; set; } - - [JsonPropertyName("account_data")] - public EventList? AccountData { get; set; } - - [JsonPropertyName("presence")] - public PresenceDataStructure? Presence { get; set; } - - [JsonPropertyName("device_one_time_keys_count")] - public Dictionary<string, int> DeviceOneTimeKeysCount { get; set; } - - [JsonPropertyName("rooms")] - public RoomsDataStructure? Rooms { get; set; } - - [JsonPropertyName("to_device")] - public EventList? ToDevice { get; set; } - - [JsonPropertyName("device_lists")] - public DeviceListsDataStructure? DeviceLists { get; set; } - - public class DeviceListsDataStructure { - [JsonPropertyName("changed")] - public List<string>? Changed { get; set; } - - [JsonPropertyName("left")] - public List<string>? Left { get; set; } - } - - // supporting classes - public class PresenceDataStructure { - [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } + public async Task RunSyncLoopAsync(bool skipInitialSyncEvents = true, CancellationToken? cancellationToken = null) { + var sw = Stopwatch.StartNew(); + await foreach (var sync in EnumerateSyncAsync(cancellationToken)) { + logger?.LogInformation("Got sync response: {} bytes, {} elapsed", sync?.ToJson(ignoreNull: true, indent: false).Length ?? -1, sw.Elapsed); + await RunSyncLoopCallbacksAsync(sync, Since is null && skipInitialSyncEvents); + } } - public class RoomsDataStructure { - [JsonPropertyName("join")] - public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; } - - [JsonPropertyName("invite")] - public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; } - - public class JoinedRoomDataStructure { - [JsonPropertyName("timeline")] - public TimelineDataStructure? Timeline { get; set; } - - [JsonPropertyName("state")] - public EventList State { get; set; } - - [JsonPropertyName("account_data")] - public EventList AccountData { get; set; } - - [JsonPropertyName("ephemeral")] - public EventList Ephemeral { get; set; } - - [JsonPropertyName("unread_notifications")] - public UnreadNotificationsDataStructure UnreadNotifications { get; set; } - - [JsonPropertyName("summary")] - public SummaryDataStructure Summary { get; set; } + private async Task RunSyncLoopCallbacksAsync(SyncResponse syncResponse, bool isInitialSync) { - public class TimelineDataStructure { - [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } + var tasks = SyncReceivedHandlers.Select(x => x(syncResponse)).ToList(); + await Task.WhenAll(tasks); - [JsonPropertyName("prev_batch")] - public string PrevBatch { get; set; } - - [JsonPropertyName("limited")] - public bool Limited { get; set; } + if (syncResponse.AccountData is { Events: { Count: > 0 } }) { + foreach (var accountDataEvent in syncResponse.AccountData.Events) { + tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList(); + await Task.WhenAll(tasks); } + } - public class UnreadNotificationsDataStructure { - [JsonPropertyName("notification_count")] - public int NotificationCount { get; set; } + await RunSyncLoopRoomCallbacksAsync(syncResponse, isInitialSync); + } - [JsonPropertyName("highlight_count")] - public int HighlightCount { get; set; } + private async Task RunSyncLoopRoomCallbacksAsync(SyncResponse syncResponse, bool isInitialSync) { + if (syncResponse.Rooms is { Invite.Count: > 0 }) { + foreach (var roomInvite in syncResponse.Rooms.Invite) { + var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList(); + await Task.WhenAll(tasks); } + } - public class SummaryDataStructure { - [JsonPropertyName("m.heroes")] - public List<string> Heroes { get; set; } - - [JsonPropertyName("m.invited_member_count")] - public int InvitedMemberCount { get; set; } + if (isInitialSync) return; - [JsonPropertyName("m.joined_member_count")] - public int JoinedMemberCount { get; set; } + if (syncResponse.Rooms is { Join.Count: > 0 }) { + foreach (var updatedRoom in syncResponse.Rooms.Join) { + if (updatedRoom.Value.Timeline is null) continue; + foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) { + stateEventResponse.RoomId = updatedRoom.Key; + var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList(); + await Task.WhenAll(tasks); + } } } - - public class InvitedRoomDataStructure { - [JsonPropertyName("invite_state")] - public EventList InviteState { get; set; } - } } -} -public class EventList { - [JsonPropertyName("events")] - public List<StateEventResponse> Events { get; set; } + /// <summary> + /// Event fired when a sync response is received + /// </summary> + public List<Func<SyncResponse, Task>> SyncReceivedHandlers { get; } = new(); + + /// <summary> + /// Event fired when a room invite is received + /// </summary> + public List<Func<KeyValuePair<string, SyncResponse.RoomsDataStructure.InvitedRoomDataStructure>, Task>> InviteReceivedHandlers { get; } = new(); + + /// <summary> + /// Event fired when a timeline event is received + /// </summary> + public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new(); + + /// <summary> + /// Event fired when an account data event is received + /// </summary> + public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new(); } diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs new file mode 100644 index 0000000..0070d60 --- /dev/null +++ b/LibMatrix/Helpers/SyncStateResolver.cs @@ -0,0 +1,174 @@ +using LibMatrix.Extensions; +using LibMatrix.Filters; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Helpers; + +public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogger? logger = null) { + public string? Since { get; set; } + public int Timeout { get; set; } = 30000; + public string? SetPresence { get; set; } = "online"; + public SyncFilter? Filter { get; set; } + public bool FullState { get; set; } = false; + + public SyncResponse? MergedState { get; set; } = null!; + + private SyncHelper _syncHelper = new SyncHelper(homeserver, logger); + + public async Task<(SyncResponse next, SyncResponse merged)> ContinueAsync(CancellationToken? cancellationToken = null) { + // copy properties + _syncHelper.Since = Since; + _syncHelper.Timeout = Timeout; + _syncHelper.SetPresence = SetPresence; + _syncHelper.Filter = Filter; + _syncHelper.FullState = FullState; + // run sync + var sync = await _syncHelper.SyncAsync(cancellationToken); + if (sync is null) return await ContinueAsync(cancellationToken); + if (MergedState is null) MergedState = sync; + else MergedState = MergeSyncs(MergedState, sync); + Since = sync.NextBatch; + return (sync, MergedState); + } + + private SyncResponse MergeSyncs(SyncResponse oldState, SyncResponse newState) { + oldState.NextBatch = newState.NextBatch ?? oldState.NextBatch; + + oldState.AccountData ??= new(); + oldState.AccountData.Events ??= new(); + if (newState.AccountData?.Events is not null) + oldState.AccountData.Events.MergeStateEventLists(newState.AccountData?.Events ?? new()); + + oldState.Presence ??= new(); + if (newState.Presence?.Events is not null) + oldState.Presence.Events.MergeStateEventLists(newState.Presence?.Events ?? new()); + + oldState.DeviceOneTimeKeysCount ??= new(); + if (newState.DeviceOneTimeKeysCount is not null) + foreach (var (key, value) in newState.DeviceOneTimeKeysCount) { + oldState.DeviceOneTimeKeysCount[key] = value; + } + + oldState.Rooms ??= new(); + if (newState.Rooms is not null) + oldState.Rooms = MergeRoomsDataStructure(oldState.Rooms, newState.Rooms); + + oldState.ToDevice ??= new(); + oldState.ToDevice.Events ??= new(); + if (newState.ToDevice?.Events is not null) + oldState.ToDevice.Events.MergeStateEventLists(newState.ToDevice?.Events ?? new()); + + oldState.DeviceLists ??= new(); + if (newState.DeviceLists?.Changed is not null) + foreach (var s in oldState.DeviceLists.Changed!) { + oldState.DeviceLists.Changed.Add(s); + } + if (newState.DeviceLists?.Left is not null) + foreach (var s in oldState.DeviceLists.Left!) { + oldState.DeviceLists.Left.Add(s); + } + + + return oldState; + } + +#region Merge rooms + + private SyncResponse.RoomsDataStructure MergeRoomsDataStructure(SyncResponse.RoomsDataStructure oldState, SyncResponse.RoomsDataStructure newState) { + oldState.Join ??= new(); + foreach (var (key, value) in newState.Join ?? new()) { + if (!oldState.Join.ContainsKey(key)) oldState.Join[key] = value; + else oldState.Join[key] = MergeJoinedRoomDataStructure(oldState.Join[key], value); + } + + oldState.Invite ??= new(); + foreach (var (key, value) in newState.Invite ?? new()) { + if (!oldState.Invite.ContainsKey(key)) oldState.Invite[key] = value; + else oldState.Invite[key] = MergeInvitedRoomDataStructure(oldState.Invite[key], value); + } + + oldState.Leave ??= new(); + foreach (var (key, value) in newState.Leave ?? new()) { + if (!oldState.Leave.ContainsKey(key)) oldState.Leave[key] = value; + else oldState.Leave[key] = MergeLeftRoomDataStructure(oldState.Leave[key], value); + if (oldState.Invite.ContainsKey(key)) oldState.Invite.Remove(key); + if (oldState.Join.ContainsKey(key)) oldState.Join.Remove(key); + } + + return oldState; + } + + private SyncResponse.RoomsDataStructure.LeftRoomDataStructure MergeLeftRoomDataStructure(SyncResponse.RoomsDataStructure.LeftRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.LeftRoomDataStructure newData) { + oldData.AccountData ??= new(); + oldData.AccountData.Events ??= new(); + oldData.Timeline ??= new(); + oldData.Timeline.Events ??= new(); + oldData.State ??= new(); + oldData.State.Events ??= new(); + + if (newData.AccountData?.Events is not null) + oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new()); + + if (newData.Timeline?.Events is not null) + oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new()); + oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited; + oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch; + + if (newData.State?.Events is not null) + oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new()); + + return oldData; + } + + private SyncResponse.RoomsDataStructure.InvitedRoomDataStructure MergeInvitedRoomDataStructure(SyncResponse.RoomsDataStructure.InvitedRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.InvitedRoomDataStructure newData) { + oldData.InviteState ??= new(); + oldData.InviteState.Events ??= new(); + if (newData.InviteState?.Events is not null) + oldData.InviteState.Events.MergeStateEventLists(newData.InviteState?.Events ?? new()); + + return oldData; + } + + private SyncResponse.RoomsDataStructure.JoinedRoomDataStructure MergeJoinedRoomDataStructure(SyncResponse.RoomsDataStructure.JoinedRoomDataStructure oldData, + SyncResponse.RoomsDataStructure.JoinedRoomDataStructure newData) { + oldData.AccountData ??= new(); + oldData.AccountData.Events ??= new(); + oldData.Timeline ??= new(); + oldData.Timeline.Events ??= new(); + oldData.State ??= new(); + oldData.State.Events ??= new(); + oldData.Ephemeral ??= new(); + oldData.Ephemeral.Events ??= new(); + + if (newData.AccountData?.Events is not null) + oldData.AccountData.Events.MergeStateEventLists(newData.AccountData?.Events ?? new()); + + if (newData.Timeline?.Events is not null) + oldData.Timeline.Events.MergeStateEventLists(newData.Timeline?.Events ?? new()); + oldData.Timeline.Limited = newData.Timeline?.Limited ?? oldData.Timeline.Limited; + oldData.Timeline.PrevBatch = newData.Timeline?.PrevBatch ?? oldData.Timeline.PrevBatch; + + if (newData.State?.Events is not null) + oldData.State.Events.MergeStateEventLists(newData.State?.Events ?? new()); + + if (newData.Ephemeral?.Events is not null) + oldData.Ephemeral.Events.MergeStateEventLists(newData.Ephemeral?.Events ?? new()); + + oldData.UnreadNotifications ??= new(); + oldData.UnreadNotifications.HighlightCount = newData.UnreadNotifications?.HighlightCount ?? oldData.UnreadNotifications.HighlightCount; + oldData.UnreadNotifications.NotificationCount = newData.UnreadNotifications?.NotificationCount ?? oldData.UnreadNotifications.NotificationCount; + + oldData.Summary ??= new(); + oldData.Summary.Heroes = newData.Summary?.Heroes ?? oldData.Summary.Heroes; + oldData.Summary.JoinedMemberCount = newData.Summary?.JoinedMemberCount ?? oldData.Summary.JoinedMemberCount; + oldData.Summary.InvitedMemberCount = newData.Summary?.InvitedMemberCount ?? oldData.Summary.InvitedMemberCount; + + return oldData; + } + +#endregion +} diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index f70dd39..d5b0a77 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -12,19 +12,35 @@ using LibMatrix.Services; namespace LibMatrix.Homeservers; -public class AuthenticatedHomeserverGeneric : RemoteHomeServer { - 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 class AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : RemoteHomeServer(baseUrl) { + public static async Task<T> Create<T>(string baseUrl, string accessToken) where T : AuthenticatedHomeserverGeneric { + var instance = Activator.CreateInstance(typeof(T), baseUrl, accessToken) as T + ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}"); + instance._httpClient = new() { + BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl) + ?? throw new InvalidOperationException("Failed to resolve homeserver")), + Timeout = TimeSpan.FromMinutes(15), + DefaultRequestHeaders = { + Authorization = new AuthenticationHeaderValue("Bearer", accessToken) + } + }; + instance.WhoAmI = await instance._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"); + return instance; } - public virtual SyncHelper SyncHelper { get; init; } - private WhoAmIResponse? _whoAmI; + // Activator.CreateInstance(baseUrl, accessToken) { + // _httpClient = new() { + // BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl) + // ?? throw new InvalidOperationException("Failed to resolve homeserver")), + // Timeout = TimeSpan.FromMinutes(15), + // DefaultRequestHeaders = { + // Authorization = new AuthenticationHeaderValue("Bearer", accessToken) + // } + // } + // }; + - public WhoAmIResponse? WhoAmI => _whoAmI ??= _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami").Result; + public WhoAmIResponse? WhoAmI { get; set; } public string UserId => WhoAmI.UserId; // public virtual async Task<WhoAmIResponse> WhoAmI() { @@ -33,9 +49,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { // return _whoAmI; // } - public virtual string AccessToken { get; set; } + public string AccessToken { get; set; } = accessToken; - public virtual GenericRoom GetRoom(string roomId) { + public GenericRoom GetRoom(string roomId) { if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId)); return new GenericRoom(this, roomId); } @@ -112,7 +128,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer { #region Account Data - public virtual async Task<T> GetAccountData<T>(string key) { + public virtual async Task<T> GetAccountDataAsync<T>(string key) { // var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}"); // if (!res.IsSuccessStatusCode) { // Console.WriteLine($"Failed to get account data: {await res.Content.ReadAsStringAsync()}"); diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index d10c837..798349a 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,13 +11,18 @@ using LibMatrix.Services; namespace LibMatrix.Homeservers; public class RemoteHomeServer(string baseUrl) { + public static async Task<RemoteHomeServer> Create(string baseUrl) => + new(baseUrl) { + _httpClient = new() { + BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl) + ?? throw new InvalidOperationException("Failed to resolve homeserver")), + Timeout = TimeSpan.FromSeconds(120) + } + }; private Dictionary<string, object> _profileCache { 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 string BaseUrl { get; } = baseUrl; + public MatrixHttpClient _httpClient { get; set; } public async Task<ProfileResponseEventContent> GetProfileAsync(string mxid) { if (mxid is null) throw new ArgumentNullException(nameof(mxid)); diff --git a/LibMatrix/Interfaces/IStateEventType.cs b/LibMatrix/Interfaces/EventContent.cs index b187970..b21cfc7 100644 --- a/LibMatrix/Interfaces/IStateEventType.cs +++ b/LibMatrix/Interfaces/EventContent.cs @@ -9,7 +9,7 @@ public abstract class EventContent { [JsonPropertyName("m.new_content")] public EventContent? NewContent { get; set; } - public abstract class MessageRelatesTo { + public class MessageRelatesTo { [JsonPropertyName("m.in_reply_to")] public EventInReplyTo? InReplyTo { get; set; } @@ -18,6 +18,9 @@ public abstract class EventContent { public abstract class EventInReplyTo { [JsonPropertyName("event_id")] public string EventId { get; set; } + + [JsonPropertyName("rel_type")] + public string RelType { get; set; } } } } diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs index 511b3da..1ad590f 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using LibMatrix.EventTypes; using LibMatrix.EventTypes.Spec.State; using LibMatrix.Helpers; using LibMatrix.Homeservers; diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs index eb53c0a..07b1601 100644 --- a/LibMatrix/Responses/LoginResponse.cs +++ b/LibMatrix/Responses/LoginResponse.cs @@ -23,7 +23,7 @@ public class LoginResponse { public string UserId { get; set; } = null!; public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) { - return new AuthenticatedHomeserverGeneric(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken); + return await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken); } } public class LoginRequest { diff --git a/LibMatrix/Responses/StateEventResponse.cs b/LibMatrix/Responses/StateEventResponse.cs deleted file mode 100644 index 7ca6bab..0000000 --- a/LibMatrix/Responses/StateEventResponse.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace LibMatrix.Responses; - -public class StateEventResponse : StateEvent { - [JsonPropertyName("origin_server_ts")] - public ulong OriginServerTs { get; set; } - - [JsonPropertyName("room_id")] - public string RoomId { get; set; } - - [JsonPropertyName("sender")] - public string Sender { get; set; } - - [JsonPropertyName("unsigned")] - public UnsignedData? Unsigned { get; set; } - - [JsonPropertyName("event_id")] - public string EventId { get; set; } - - [JsonPropertyName("user_id")] - public string UserId { get; set; } - - [JsonPropertyName("replaces_state")] - public new string ReplacesState { get; set; } - - public class UnsignedData { - [JsonPropertyName("age")] - public ulong? Age { get; set; } - - [JsonPropertyName("redacted_because")] - public object? RedactedBecause { get; set; } - - [JsonPropertyName("transaction_id")] - public string? TransactionId { get; set; } - - [JsonPropertyName("replaces_state")] - public string? ReplacesState { get; set; } - - [JsonPropertyName("prev_sender")] - public string? PrevSender { get; set; } - - [JsonPropertyName("prev_content")] - public JsonObject? PrevContent { get; set; } - } -} - -public class ChunkedStateEventResponse { - [JsonPropertyName("chunk")] - public List<StateEventResponse>? Chunk { get; set; } -} diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs new file mode 100644 index 0000000..39cb38f --- /dev/null +++ b/LibMatrix/Responses/SyncResponse.cs @@ -0,0 +1,118 @@ +using System.Text.Json.Serialization; +using LibMatrix.Helpers; + +namespace LibMatrix.Responses; + +public class SyncResponse { + [JsonPropertyName("next_batch")] + public string NextBatch { get; set; } = null!; + + [JsonPropertyName("account_data")] + public EventList? AccountData { get; set; } + + [JsonPropertyName("presence")] + public PresenceDataStructure? Presence { get; set; } + + [JsonPropertyName("device_one_time_keys_count")] + public Dictionary<string, int>? DeviceOneTimeKeysCount { get; set; } = null!; + + [JsonPropertyName("rooms")] + public RoomsDataStructure? Rooms { get; set; } + + [JsonPropertyName("to_device")] + public EventList? ToDevice { get; set; } + + [JsonPropertyName("device_lists")] + public DeviceListsDataStructure? DeviceLists { get; set; } + + public class DeviceListsDataStructure { + [JsonPropertyName("changed")] + public List<string>? Changed { get; set; } + + [JsonPropertyName("left")] + public List<string>? Left { get; set; } + } + + // supporting classes + public class PresenceDataStructure { + [JsonPropertyName("events")] + public List<StateEventResponse> Events { get; set; } = new(); + } + + public class RoomsDataStructure { + [JsonPropertyName("join")] + public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; } + + [JsonPropertyName("invite")] + public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; } + + [JsonPropertyName("leave")] + public Dictionary<string, LeftRoomDataStructure>? Leave { get; set; } + + public class LeftRoomDataStructure { + [JsonPropertyName("account_data")] + public EventList AccountData { get; set; } + + [JsonPropertyName("timeline")] + public JoinedRoomDataStructure.TimelineDataStructure? Timeline { get; set; } + + [JsonPropertyName("state")] + public EventList State { get; set; } + } + + public class JoinedRoomDataStructure { + [JsonPropertyName("timeline")] + public TimelineDataStructure? Timeline { get; set; } + + [JsonPropertyName("state")] + public EventList? State { get; set; } + + [JsonPropertyName("account_data")] + public EventList? AccountData { get; set; } + + [JsonPropertyName("ephemeral")] + public EventList? Ephemeral { get; set; } + + [JsonPropertyName("unread_notifications")] + public UnreadNotificationsDataStructure? UnreadNotifications { get; set; } + + [JsonPropertyName("summary")] + public SummaryDataStructure? Summary { get; set; } + + public class TimelineDataStructure { + [JsonPropertyName("events")] + public List<StateEventResponse>? Events { get; set; } + + [JsonPropertyName("prev_batch")] + public string? PrevBatch { get; set; } + + [JsonPropertyName("limited")] + public bool? Limited { get; set; } + } + + public class UnreadNotificationsDataStructure { + [JsonPropertyName("notification_count")] + public int NotificationCount { get; set; } + + [JsonPropertyName("highlight_count")] + public int HighlightCount { get; set; } + } + + public class SummaryDataStructure { + [JsonPropertyName("m.heroes")] + public List<string> Heroes { get; set; } + + [JsonPropertyName("m.invited_member_count")] + public int InvitedMemberCount { get; set; } + + [JsonPropertyName("m.joined_member_count")] + public int JoinedMemberCount { get; set; } + } + } + + public class InvitedRoomDataStructure { + [JsonPropertyName("invite_state")] + public EventList? InviteState { get; set; } + } + } +} diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index 78a0873..75cb5f3 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -118,16 +118,16 @@ public class GenericRoom { #region Utility shortcuts - public async Task<EventIdResponse> SendMessageEventAsync(RoomMessageEventContent content) => + public async Task<EventIdResponse?> SendMessageEventAsync(RoomMessageEventContent content) => await SendTimelineEventAsync("m.room.message", content); - public async Task<List<string>> GetAliasesAsync() { + public async Task<List<string>?> GetAliasesAsync() { var res = await GetStateAsync<RoomAliasEventContent>("m.room.aliases"); return res.Aliases; } - public async Task<CanonicalAliasEventContent?> GetCanonicalAliasAsync() => - await GetStateAsync<CanonicalAliasEventContent>("m.room.canonical_alias"); + public async Task<RoomCanonicalAliasEventContent?> GetCanonicalAliasAsync() => + await GetStateAsync<RoomCanonicalAliasEventContent>("m.room.canonical_alias"); public async Task<RoomTopicEventContent?> GetTopicAsync() => await GetStateAsync<RoomTopicEventContent>("m.room.topic"); @@ -135,16 +135,16 @@ public class GenericRoom { public async Task<RoomAvatarEventContent?> GetAvatarUrlAsync() => await GetStateAsync<RoomAvatarEventContent>("m.room.avatar"); - public async Task<JoinRulesEventContent> GetJoinRuleAsync() => - await GetStateAsync<JoinRulesEventContent>("m.room.join_rules"); + public async Task<RoomJoinRulesEventContent?> GetJoinRuleAsync() => + await GetStateAsync<RoomJoinRulesEventContent>("m.room.join_rules"); - public async Task<HistoryVisibilityEventContent?> GetHistoryVisibilityAsync() => - await GetStateAsync<HistoryVisibilityEventContent>("m.room.history_visibility"); + public async Task<RoomHistoryVisibilityEventContent?> GetHistoryVisibilityAsync() => + await GetStateAsync<RoomHistoryVisibilityEventContent?>("m.room.history_visibility"); - public async Task<GuestAccessEventContent?> GetGuestAccessAsync() => - await GetStateAsync<GuestAccessEventContent>("m.room.guest_access"); + public async Task<RoomGuestAccessEventContent?> GetGuestAccessAsync() => + await GetStateAsync<RoomGuestAccessEventContent>("m.room.guest_access"); - public async Task<RoomCreateEventContent> GetCreateEventAsync() => + public async Task<RoomCreateEventContent?> GetCreateEventAsync() => await GetStateAsync<RoomCreateEventContent>("m.room.create"); public async Task<string?> GetRoomType() { @@ -177,24 +177,23 @@ public class GenericRoom { await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", new UserIdAndReason { UserId = userId }); - public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) => + public async Task<EventIdResponse?> SendStateEventAsync(string eventType, object content) => await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); - public async Task<EventIdResponse> SendStateEventAsync(string eventType, string stateKey, object content) => + public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) => await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); - public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, EventContent content) { + public async Task<EventIdResponse?> SendTimelineEventAsync(string eventType, EventContent content) { var res = await _httpClient.PutAsJsonAsync( $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); - var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>(); - return resu; + return await res.Content.ReadFromJsonAsync<EventIdResponse>(); } - public async Task<EventIdResponse> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") { + 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, @@ -205,7 +204,7 @@ public class GenericRoom { return await SendTimelineEventAsync("m.room.message", content); } - public async Task<T> GetRoomAccountDataAsync<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()}"); diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs index 666d2a2..1f3bd37 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -35,20 +35,15 @@ public class HomeserverProviderService { } var domain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver); - var hc = new MatrixHttpClient { BaseAddress = new Uri(domain) }; AuthenticatedHomeserverGeneric hs; if (true) { - hs = new AuthenticatedHomeserverMxApiExtended(homeserver, accessToken); + hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverMxApiExtended>(homeserver, accessToken); } else { - hs = new AuthenticatedHomeserverGeneric(homeserver, accessToken); + hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverSynapse>(homeserver, accessToken); } - 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"))!; lock(_authenticatedHomeServerCache) @@ -59,7 +54,7 @@ public class HomeserverProviderService { } public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? proxy = null) { - var hs = new RemoteHomeServer(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver)); + var hs = await RemoteHomeServer.Create(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver)); // hs._httpClient.Dispose(); // hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) }; // hs._httpClient.Timeout = TimeSpan.FromSeconds(120); diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index 685724b..75545db 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -12,6 +12,9 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new(); public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) { + if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); + if(_wellKnownCache.TryGetValue(homeserver, out var known)) return known; + logger?.LogInformation("Resolving homeserver: {}", homeserver); var res = await _resolveHomeserverFromWellKnown(homeserver); if (!res.StartsWith("http")) res = "https://" + res; if (res.EndsWith(":443")) res = res[..^4]; @@ -21,6 +24,7 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) { if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); var sem = _wellKnownSemaphores.GetOrCreate(homeserver, _ => new SemaphoreSlim(1, 1)); + if(_wellKnownCache.TryGetValue(homeserver, out var wellKnown)) return wellKnown; await sem.WaitAsync(); if (_wellKnownCache.TryGetValue(homeserver, out var known)) { sem.Release(); diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index b42bd64..c51fadb 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -39,8 +39,11 @@ public class StateEvent { public EventContent TypedContent { get { + if(Type == "m.receipt") { + return null!; + } try { - return (EventContent) RawContent.Deserialize(GetType)!; + return (EventContent)RawContent.Deserialize(GetType)!; } catch (JsonException e) { Console.WriteLine(e); @@ -122,6 +125,61 @@ public class StateEvent { public string cdtype => TypedContent.GetType().Name; } +public class StateEventResponse : StateEvent { + [JsonPropertyName("origin_server_ts")] + public ulong OriginServerTs { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("unsigned")] + public UnsignedData? Unsigned { get; set; } + + [JsonPropertyName("event_id")] + public string EventId { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonPropertyName("replaces_state")] + public new string ReplacesState { get; set; } + + public class UnsignedData { + [JsonPropertyName("age")] + public ulong? Age { get; set; } + + [JsonPropertyName("redacted_because")] + public object? RedactedBecause { get; set; } + + [JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } + + [JsonPropertyName("prev_sender")] + public string? PrevSender { get; set; } + + [JsonPropertyName("prev_content")] + public JsonObject? PrevContent { get; set; } + } +} + +public class EventList { + [JsonPropertyName("events")] + public List<StateEventResponse>? Events { get; set; } = new(); +} + +public class ChunkedStateEventResponse { + [JsonPropertyName("chunk")] + public List<StateEventResponse>? Chunk { get; set; } = new(); +} + +#region Unused code + /* public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoResolver { @@ -150,3 +208,5 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR } } */ + +#endregion diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs index abd3e99..e23d4f4 100644 --- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs @@ -6,7 +6,7 @@ namespace LibMatrix.Tests.Abstractions; public static class HomeserverAbstraction { public static async Task<AuthenticatedHomeserverGeneric> GetHomeserver() { - var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay"); + var rhs = await RemoteHomeServer.Create("https://matrixunittests.rory.gay"); // string username = Guid.NewGuid().ToString(); // string password = Guid.NewGuid().ToString(); string username = "@f1a2d2d6-1924-421b-91d0-893b347b2a49:matrixunittests.rory.gay"; @@ -25,15 +25,15 @@ public static class HomeserverAbstraction { var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay"); - var rooms = await hs.GetJoinedRooms(); + //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); + // 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(); @@ -45,28 +45,28 @@ public static class HomeserverAbstraction { } public static async Task<AuthenticatedHomeserverGeneric> GetRandomHomeserver() { - var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay"); + var rhs = await RemoteHomeServer.Create("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); + // 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 + var createRandomUserTasks = Enumerable .Range(0, count) .Select(_ => GetRandomHomeserver()).ToAsyncEnumerable(); - await foreach (var hs in createSpaceTasks) { + await foreach (var hs in createRandomUserTasks) { yield return hs; } } diff --git a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs index 44c35da..76b8c8c 100644 --- a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs +++ b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs @@ -8,11 +8,43 @@ namespace LibMatrix.Tests.Abstractions; public static class RoomAbstraction { public static async Task<GenericRoom> GetTestRoom(AuthenticatedHomeserverGeneric hs) { - var testRoom = await hs.CreateRoom(new CreateRoomRequest() { + var crq = new CreateRoomRequest() { Name = "LibMatrix Test Room", // Visibility = CreateRoomVisibility.Public, RoomAliasName = Guid.NewGuid().ToString() + }; + crq.InitialState ??= new(); + crq.InitialState.Add(new StateEvent() { + Type = "m.room.topic", + StateKey = "", + TypedContent = new RoomTopicEventContent() { + Topic = "LibMatrix Test Room " + DateTime.Now.ToString("O") + } + }); + crq.InitialState.Add(new StateEvent() { + Type = "m.room.name", + StateKey = "", + TypedContent = new RoomNameEventContent() { + Name = "LibMatrix Test Room " + DateTime.Now.ToString("O") + } + }); + crq.InitialState.Add(new StateEvent() { + Type = "m.room.avatar", + StateKey = "", + TypedContent = new RoomAvatarEventContent() { + Url = "mxc://conduit.rory.gay/r9KiT0f9eQbv8pv4RxwBZFuzhfKjGWHx" + } + }); + crq.InitialState.Add(new StateEvent() { + Type = "m.room.aliases", + StateKey = "", + TypedContent = new RoomAliasEventContent() { + Aliases = Enumerable + .Range(0, 100) + .Select(_ => $"#{Guid.NewGuid()}:matrixunittests.rory.gay").ToList() + } }); + var testRoom = await hs.CreateRoom(crq); await testRoom.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new()); diff --git a/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs b/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs new file mode 100644 index 0000000..6828087 --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/RoomEventTests.cs @@ -0,0 +1,160 @@ +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; + +namespace LibMatrix.Tests.Tests; + +public class RoomEventTests : TestBed<TestFixture> { + private readonly TestFixture _fixture; + private readonly HomeserverResolverService _resolver; + private readonly Config _config; + private readonly HomeserverProviderService _provider; + + public RoomEventTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _fixture = fixture; + _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + } + + private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() { + return await HomeserverAbstraction.GetHomeserver(); + } + + [Fact] + public async Task GetNameAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var name = await room.GetNameAsync(); + Assert.NotNull(name); + Assert.NotEmpty(name); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetTopicAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var topic = await room.GetTopicAsync(); + Assert.NotNull(topic); + Assert.NotNull(topic.Topic); + Assert.NotEmpty(topic.Topic); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetAliasesAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var aliases = await room.GetAliasesAsync(); + Assert.NotNull(aliases); + Assert.NotEmpty(aliases); + Assert.All(aliases, Assert.NotNull); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetCanonicalAliasAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var alias = await room.GetCanonicalAliasAsync(); + Assert.NotNull(alias); + Assert.NotNull(alias.Alias); + Assert.NotEmpty(alias.Alias); + } + + [SkippableFact(typeof(MatrixException))] + public async Task GetAvatarUrlAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var url = await room.GetAvatarUrlAsync(); + Assert.NotNull(url); + Assert.NotNull(url.Url); + Assert.NotEmpty(url.Url); + } + + [Fact] + public async Task GetJoinRuleAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var rule = await room.GetJoinRuleAsync(); + Assert.NotNull(rule); + Assert.NotNull(rule.JoinRule); + Assert.NotEmpty(rule.JoinRule); + } + + [Fact] + public async Task GetHistoryVisibilityAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var visibility = await room.GetHistoryVisibilityAsync(); + Assert.NotNull(visibility); + Assert.NotNull(visibility.HistoryVisibility); + Assert.NotEmpty(visibility.HistoryVisibility); + } + + [Fact] + public async Task GetGuestAccessAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + try { + var access = await room.GetGuestAccessAsync(); + Assert.NotNull(access); + Assert.NotNull(access.GuestAccess); + Assert.NotEmpty(access.GuestAccess); + } + catch (Exception e) { + if (e is not MatrixException exception) throw; + Assert.Equal("M_NOT_FOUND", exception.ErrorCode); + } + } + + [Fact] + public async Task GetCreateEventAsync() { + var hs = await HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var create = await room.GetCreateEventAsync(); + Assert.NotNull(create); + Assert.NotNull(create.Creator); + Assert.NotEmpty(create.RoomVersion!); + } + + [Fact] + public async Task GetRoomType() { + 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 HomeserverAbstraction.GetHomeserver(); + var room = await RoomAbstraction.GetTestRoom(hs); + Assert.NotNull(room); + var power = await room.GetPowerLevelsAsync(); + Assert.NotNull(power); + Assert.NotNull(power.Ban); + Assert.NotNull(power.Kick); + Assert.NotNull(power.Invite); + Assert.NotNull(power.Redact); + Assert.NotNull(power.StateDefault); + Assert.NotNull(power.EventsDefault); + Assert.NotNull(power.UsersDefault); + Assert.NotNull(power.Users); + // Assert.NotNull(power.Events); + } + +} diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs index 17c219d..72a2775 100644 --- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs +++ b/Tests/LibMatrix.Tests/Tests/RoomTests.cs @@ -31,38 +31,18 @@ public class RoomTests : TestBed<TestFixture> { public async Task GetJoinedRoomsAsync() { var hs = await HomeserverAbstraction.GetHomeserver(); //make 100 rooms - var createRoomTasks = Enumerable.Range(0, 100).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList(); + var createRoomTasks = Enumerable.Range(0, 10).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); + Assert.True(rooms.Count >= 10, "Not enough rooms were found"); await hs.Logout(); } - [Fact] - public async Task GetNameAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var name = await room.GetNameAsync(); - Assert.NotNull(name); - Assert.NotEmpty(name); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetTopicAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var topic = await room.GetTopicAsync(); - Assert.NotNull(topic); - Assert.NotNull(topic.Topic); - Assert.NotEmpty(topic.Topic); - } [Fact] public async Task GetMembersAsync() { @@ -106,115 +86,6 @@ public class RoomTests : TestBed<TestFixture> { Assert.NotEmpty(id.RoomId); } - [SkippableFact(typeof(MatrixException))] - public async Task GetAliasesAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var aliases = await room.GetAliasesAsync(); - Assert.NotNull(aliases); - Assert.NotEmpty(aliases); - Assert.All(aliases, Assert.NotNull); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetCanonicalAliasAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var alias = await room.GetCanonicalAliasAsync(); - Assert.NotNull(alias); - Assert.NotNull(alias.Alias); - Assert.NotEmpty(alias.Alias); - } - - [SkippableFact(typeof(MatrixException))] - public async Task GetAvatarUrlAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var url = await room.GetAvatarUrlAsync(); - Assert.NotNull(url); - Assert.NotNull(url.Url); - Assert.NotEmpty(url.Url); - } - - [Fact] - public async Task GetJoinRuleAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var rule = await room.GetJoinRuleAsync(); - Assert.NotNull(rule); - Assert.NotNull(rule.JoinRule); - Assert.NotEmpty(rule.JoinRule); - } - - [Fact] - public async Task GetHistoryVisibilityAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var visibility = await room.GetHistoryVisibilityAsync(); - Assert.NotNull(visibility); - Assert.NotNull(visibility.HistoryVisibility); - Assert.NotEmpty(visibility.HistoryVisibility); - } - - [Fact] - public async Task GetGuestAccessAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - try { - var access = await room.GetGuestAccessAsync(); - Assert.NotNull(access); - Assert.NotNull(access.GuestAccess); - Assert.NotEmpty(access.GuestAccess); - } - catch (Exception e) { - if (e is not MatrixException exception) throw; - Assert.Equal("M_NOT_FOUND", exception.ErrorCode); - } - } - - [Fact] - public async Task GetCreateEventAsync() { - var hs = await HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var create = await room.GetCreateEventAsync(); - Assert.NotNull(create); - Assert.NotNull(create.Creator); - Assert.NotEmpty(create.RoomVersion!); - } - - [Fact] - public async Task GetRoomType() { - 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 HomeserverAbstraction.GetHomeserver(); - var room = await RoomAbstraction.GetTestRoom(hs); - Assert.NotNull(room); - var power = await room.GetPowerLevelsAsync(); - Assert.NotNull(power); - Assert.NotNull(power.Ban); - Assert.NotNull(power.Kick); - Assert.NotNull(power.Invite); - Assert.NotNull(power.Redact); - Assert.NotNull(power.StateDefault); - Assert.NotNull(power.EventsDefault); - Assert.NotNull(power.UsersDefault); - Assert.NotNull(power.Users); - // Assert.NotNull(power.Events); - } - [Fact] public async Task ForgetAsync() { var hs = await HomeserverAbstraction.GetHomeserver(); @@ -361,17 +232,21 @@ public class RoomTests : TestBed<TestFixture> { [Fact] public async Task InviteAndJoinAsync() { - var otherUsers = HomeserverAbstraction.GetRandomHomeservers(7); var hs = await HomeserverAbstraction.GetHomeserver(); var room = await RoomAbstraction.GetTestRoom(hs); + var otherUsers = HomeserverAbstraction.GetRandomHomeservers(15); Assert.NotNull(room); // var expectedCount = 1; + var tasks = new List<Task>(); await foreach(var otherUser in otherUsers) { - await room.InviteUserAsync(otherUser.UserId); - await otherUser.GetRoom(room.RoomId).JoinAsync(); + tasks.Add(Task.Run(async () => { + await room.InviteUserAsync(otherUser.UserId); + await otherUser.GetRoom(room.RoomId).JoinAsync(); + })); } + await Task.WhenAll(tasks); var states = room.GetMembersAsync(false); var count = 0; @@ -379,6 +254,6 @@ public class RoomTests : TestBed<TestFixture> { count++; } // Assert.Equal(++expectedCount, count); - Assert.Equal(8, count); + Assert.Equal(16, count); } } diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs new file mode 100644 index 0000000..e93de3d --- /dev/null +++ b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; +using LibMatrix.Services; +using LibMatrix.Tests.Abstractions; +using LibMatrix.Tests.DataTests; +using LibMatrix.Tests.Fixtures; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace LibMatrix.Tests.Tests; + +public class TestCleanup : TestBed<TestFixture> { + private readonly TestFixture _fixture; + private readonly HomeserverResolverService _resolver; + private readonly Config _config; + private readonly HomeserverProviderService _provider; + private readonly ILogger<TestCleanup> _logger; + + public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) { + _fixture = fixture; + _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}"); + _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}"); + _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}"); + _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}"); + } + + [Fact] + public async Task Cleanup() { + 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 hs = await HomeserverAbstraction.GetHomeserver(); + Assert.NotNull(hs); + + var syncHelper = new SyncHelper(hs, _logger) { + Timeout = 3000 + }; + _testOutputHelper.WriteLine("Starting sync loop"); + var cancellationTokenSource = new CancellationTokenSource(); + var sw = Stopwatch.StartNew(); + syncHelper.SyncReceivedHandlers.Add(async response => { + if (sw.ElapsedMilliseconds >= 3000) { + _testOutputHelper.WriteLine("Cancelling sync loop"); + + var tasks = (await hs.GetJoinedRooms()).Select(async room => { + _logger.LogInformation("Leaving room: {}", room.RoomId); + await room.LeaveAsync(); + await room.ForgetAsync(); + return room; + }).ToList(); + await Task.WhenAll(tasks); + + cancellationTokenSource.Cancel(); + } + + sw.Restart(); + if (response.Rooms?.Leave is { Count: > 0 }) { + // foreach (var room in response.Rooms.Leave) { + // await hs.GetRoom(room.Key).ForgetAsync(); + // } + var tasks = response.Rooms.Leave.Select(async room => { + await hs.GetRoom(room.Key).ForgetAsync(); + return room; + }).ToList(); + await Task.WhenAll(tasks); + } + }); + await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token); + + Assert.NotNull(hs); + await hs.Logout(); + } +} diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs index 910db0a..0dcf3e3 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs @@ -37,7 +37,8 @@ public class CommandListenerHostedService : IHostedService { private async Task? Run(CancellationToken cancellationToken) { _logger.LogInformation("Starting command listener!"); - _hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + var syncHelper = new SyncHelper(_hs); + syncHelper.TimelineEventHandlers.Add(async @event => { try { var room = _hs.GetRoom(@event.RoomId); // _logger.LogInformation(eventResponse.ToJson(indent: false)); @@ -80,7 +81,7 @@ public class CommandListenerHostedService : IHostedService { _logger.LogError(e, "Error in command listener!"); } }); - await _hs.SyncHelper.RunSyncLoop(cancellationToken: cancellationToken); + await new SyncHelper(_hs).RunSyncLoopAsync(cancellationToken: cancellationToken); } /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> |