diff options
77 files changed, 695 insertions, 367 deletions
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/CmdCommand.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/CmdCommand.cs index ca10326..efedbba 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/CmdCommand.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/CmdCommand.cs @@ -17,9 +17,7 @@ public class CmdCommand : ICommand { cmd = cmd.Trim(); cmd += "\""; - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = $"Command being executed: `{cmd}`" - }); + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(body: $"Command being executed: `{cmd}`")); var output = ArcaneLibs.Util.GetCommandOutputSync( Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/sh" : "cmd.exe", diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/HelpCommand.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/HelpCommand.cs index 69766d1..09c4e3f 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/HelpCommand.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/HelpCommand.cs @@ -17,8 +17,6 @@ public class HelpCommand(IServiceProvider services) : ICommand { sb.AppendLine($"- {command.Name}: {command.Description}"); } - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = sb.ToString() - }); + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(body: sb.ToString())); } } diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/PingCommand.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/PingCommand.cs index a7c65b5..f70cd78 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/PingCommand.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/Commands/PingCommand.cs @@ -8,8 +8,6 @@ public class PingCommand : ICommand { public string Description { get; } = "Pong!"; public async Task Invoke(CommandContext ctx) { - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = "pong!" - }); + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(body: "pong!")); } } diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs index 4f9b173..3f69d90 100644 --- a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs +++ b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs @@ -19,7 +19,7 @@ public class MRUBot : IHostedService { public MRUBot(HomeserverProviderService homeserverProviderService, ILogger<MRUBot> logger, MRUBotConfiguration configuration, IServiceProvider services) { - logger.LogInformation("MRUBot hosted service instantiated!"); + logger.LogInformation("{} instantiated!", this.GetType().Name); _homeserverProviderService = homeserverProviderService; _logger = logger; _configuration = configuration; @@ -81,10 +81,7 @@ public class MRUBot : IHostedService { var command = _commands.FirstOrDefault(x => x.Name == message.Body.Split(' ')[0][_configuration.Prefix.Length..]); if (command == null) { await room.SendMessageEventAsync("m.room.message", - new RoomMessageEventData { - MessageType = "m.text", - Body = "Command not found!" - }); + new RoomMessageEventData(messageType: "m.text", body: "Command not found!")); return; } @@ -97,10 +94,7 @@ public class MRUBot : IHostedService { } else { await room.SendMessageEventAsync("m.room.message", - new RoomMessageEventData { - MessageType = "m.text", - Body = "You do not have permission to run this command!" - }); + new RoomMessageEventData(messageType: "m.text", body: "You do not have permission to run this command!")); } } } diff --git a/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj b/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj index 1fc421a..13cbb15 100644 --- a/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj +++ b/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj @@ -22,7 +22,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.5.23280.8" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.7.23375.6" /> </ItemGroup> <ItemGroup> <Content Include="appsettings*.json"> diff --git a/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs b/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs index 9b92948..b4e1167 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/AccountData/BotData.cs @@ -1,7 +1,14 @@ +using System.Text.Json.Serialization; + namespace MediaModeratorPoC.Bot.AccountData; public class BotData { + [JsonPropertyName("control_room")] public string ControlRoom { get; set; } = ""; + + [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/Bot/Commands/BanMediaCommand.cs index d0b5674..90de136 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs @@ -1,4 +1,8 @@ +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; using LibMatrix.Responses; +using LibMatrix.Services; using LibMatrix.StateEventTypes.Spec; using MediaModeratorPoC.Bot.AccountData; using MediaModeratorPoC.Bot.Interfaces; @@ -6,7 +10,7 @@ using MediaModeratorPoC.Bot.StateEventTypes; namespace MediaModeratorPoC.Bot.Commands; -public class BanMediaCommand(IServiceProvider services) : ICommand { +public class BanMediaCommand(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver) : ICommand { public string Name { get; } = "banmedia"; public string Description { get; } = "Create a policy banning a piece of media, must be used in reply to a message"; @@ -14,53 +18,93 @@ public class BanMediaCommand(IServiceProvider services) : ICommand { //check if user is admin in control room var botData = await ctx.Homeserver.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); var controlRoom = await ctx.Homeserver.GetRoom(botData.ControlRoom); - var powerLevels = await controlRoom.GetPowerLevelAsync(); - var isAdmin = powerLevels.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); if (!isAdmin) { // await ctx.Reply("You do not have permission to use this command!"); - var logRoom = await ctx.Homeserver.GetRoom(botData.LogRoom); - await logRoom.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", - MessageType = "m.text" - }); + await (await ctx.Homeserver.GetRoom(botData.LogRoom!)).SendMessageEventAsync("m.room.message", + new RoomMessageEventData(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); } + return isAdmin; } public async Task Invoke(CommandContext ctx) { + var botData = await ctx.Homeserver.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); + var policyRoom = await ctx.Homeserver.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); + var logRoom = await ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + //check if reply - if ((ctx.MessageEvent.TypedContent as RoomMessageEventData).RelatesTo is { InReplyTo: not null } ) { - var messageContent = ctx.MessageEvent.TypedContent as RoomMessageEventData; + var messageContent = ctx.MessageEvent.TypedContent as RoomMessageEventData; + if (messageContent?.RelatesTo is { InReplyTo: not null }) { try { - var botData = await ctx.Homeserver.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); - var policyRoom = await ctx.Homeserver.GetRoom(botData.PolicyRoom); - var logRoom = await ctx.Homeserver.GetRoom(botData.LogRoom); - await logRoom.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = $"User {ctx.MessageEvent.Sender} is trying to ban media {messageContent.RelatesTo!.InReplyTo!.EventId}", - MessageType = "m.text" - }); + await logRoom.SendMessageEventAsync("m.room.message", + new RoomMessageEventData( + body: $"User {MessageFormatter.HtmlFormatMention(ctx.MessageEvent.Sender)} is trying to ban media {messageContent!.RelatesTo!.InReplyTo!.EventId}", + messageType: "m.text")); //get replied message var repliedMessage = await ctx.Room.GetEvent<StateEventResponse>(messageContent.RelatesTo!.InReplyTo!.EventId); - await policyRoom.SendStateEventAsync("gay.rory.media_moderator_poc.rule.media", new MediaPolicyStateEventData() { - Entity = (repliedMessage.TypedContent as RoomMessageEventData).Url!, - Reason = string.Join(' ', ctx.Args), - Recommendation = PolicyRecommendationTypes.Ban + //check if recommendation is in list + if (ctx.Args.Length < 2) { + await ctx.Room.SendMessageEventAsync("m.room.message", MessageFormatter.FormatError("You must specify a recommendation type and reason!")); + return; + } + + 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("m.room.message", MessageFormatter.FormatError($"Invalid recommendation type {recommendation}, must be `warn_admins`, `warn`, `spoiler`, `redact`, `mute`, `kick` or `ban`!")); + return; + } + + + + //hash file + var mxcUri = (repliedMessage.TypedContent as RoomMessageEventData).Url!; + var resolvedUri = await hsResolver.ResolveMediaUri(mxcUri.Split('/')[2], mxcUri); + var hashAlgo = SHA3_256.Create(); + var uriHash = hashAlgo.ComputeHash(mxcUri.AsBytes().ToArray()); + byte[]? fileHash = null; + + try { + fileHash = await hashAlgo.ComputeHashAsync(await ctx.Homeserver._httpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex) { + await logRoom.SendMessageEventAsync("m.room.message", + MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]}, retrying via {ctx.Homeserver.HomeServerDomain}...", + ex)); + try { + resolvedUri = await hsResolver.ResolveMediaUri(ctx.Homeserver.HomeServerDomain, mxcUri); + fileHash = await hashAlgo.ComputeHashAsync(await ctx.Homeserver._httpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex2) { + await ctx.Room.SendMessageEventAsync("m.room.message", MessageFormatter.FormatException("Error calculating file hash", ex2)); + await logRoom.SendMessageEventAsync("m.room.message", + MessageFormatter.FormatException($"Error calculating file hash via {ctx.Homeserver.HomeServerDomain}!", ex2)); + } + } + + MediaPolicyStateEventData policy; + await policyRoom.SendStateEventAsync("gay.rory.media_moderator_poc.rule.media", Guid.NewGuid().ToString(), policy = new MediaPolicyStateEventData { + Entity = uriHash, + FileHash = fileHash, + Reason = string.Join(' ', ctx.Args[1..]), + Recommendation = recommendation, }); + + await ctx.Room.SendMessageEventAsync("m.room.message", MessageFormatter.FormatSuccessJson("Media policy created", policy)); + await logRoom.SendMessageEventAsync("m.room.message", MessageFormatter.FormatSuccessJson("Media policy created", policy)); } catch (Exception e) { - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = $"Error: {e.Message}", - MessageType = "m.text" - }); + await logRoom.SendMessageEventAsync("m.room.message", MessageFormatter.FormatException("Error creating policy", e)); + await ctx.Room.SendMessageEventAsync("m.room.message", 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); } } else { - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = "This command must be used in reply to a message!", - MessageType = "m.text", - }); + await ctx.Room.SendMessageEventAsync("m.room.message", MessageFormatter.FormatError("This command must be used in reply to a message!")); } } } diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/CmdCommand.cs b/ExampleBots/MediaModeratorPoC/Bot/Commands/CmdCommand.cs deleted file mode 100644 index 14c4334..0000000 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/CmdCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using LibMatrix.StateEventTypes.Spec; -using MediaModeratorPoC.Bot.Interfaces; - -namespace MediaModeratorPoC.Bot.Commands; - -public class CmdCommand : ICommand { - public string Name => "cmd"; - public string Description => "Runs a command on the host system"; - - public Task<bool> CanInvoke(CommandContext ctx) { - return Task.FromResult(ctx.MessageEvent.Sender.EndsWith(":rory.gay")); - } - - public async Task Invoke(CommandContext ctx) { - var cmd = ctx.Args.Aggregate("\"", (current, arg) => current + arg + " "); - - cmd = cmd.Trim(); - cmd += "\""; - - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = $"Command being executed: `{cmd}`" - }); - - var output = ArcaneLibs.Util.GetCommandOutputSync( - Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/sh" : "cmd.exe", - (Environment.OSVersion.Platform == PlatformID.Unix ? "-c " : "/c ") + cmd) - .Replace("`", "\\`") - .Split("\n").ToList(); - foreach (var _out in output) Console.WriteLine($"{_out.Length:0000} {_out}"); - - var msg = ""; - while (output.Count > 0) { - Console.WriteLine("Adding: " + output[0]); - msg += output[0] + "\n"; - output.RemoveAt(0); - if ((output.Count > 0 && (msg + output[0]).Length > 64000) || output.Count == 0) { - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - FormattedBody = $"```ansi\n{msg}\n```", - // Body = Markdig.Markdown.ToHtml(msg), - Format = "org.matrix.custom.html" - }); - msg = ""; - } - } - } -} diff --git a/ExampleBots/MediaModeratorPoC/Bot/Interfaces/CommandContext.cs b/ExampleBots/MediaModeratorPoC/Bot/Interfaces/CommandContext.cs deleted file mode 100644 index 29a5a3f..0000000 --- a/ExampleBots/MediaModeratorPoC/Bot/Interfaces/CommandContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using LibMatrix.Homeservers; -using LibMatrix.Responses; -using LibMatrix.RoomTypes; -using LibMatrix.StateEventTypes.Spec; - -namespace MediaModeratorPoC.Bot.Interfaces; - -public class CommandContext { - public GenericRoom Room { get; set; } - public StateEventResponse MessageEvent { get; set; } - public string CommandName => (MessageEvent.TypedContent as RoomMessageEventData).Body.Split(' ')[0][1..]; - public string[] Args => (MessageEvent.TypedContent as RoomMessageEventData).Body.Split(' ')[1..]; - public AuthenticatedHomeserverGeneric Homeserver { get; set; } -} diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs index 1b379c7..81aecc7 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs @@ -1,7 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +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; +using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.RoomTypes; @@ -10,50 +15,47 @@ using LibMatrix.StateEventTypes.Spec; using MediaModeratorPoC.Bot.AccountData; using MediaModeratorPoC.Bot.Interfaces; using MediaModeratorPoC.Bot.StateEventTypes; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MediaModeratorPoC.Bot; public class MediaModBot : IHostedService { - private readonly HomeserverProviderService _homeserverProviderService; + private readonly AuthenticatedHomeserverGeneric _hs; private readonly ILogger<MediaModBot> _logger; private readonly MediaModBotConfiguration _configuration; + private readonly HomeserverResolverService _hsResolver; private readonly IEnumerable<ICommand> _commands; - private GenericRoom PolicyRoom; + private Task _listenerTask; - public MediaModBot(HomeserverProviderService homeserverProviderService, ILogger<MediaModBot> logger, - MediaModBotConfiguration configuration, IServiceProvider services) { - logger.LogInformation("MRUBot hosted service instantiated!"); - _homeserverProviderService = homeserverProviderService; + private GenericRoom _policyRoom; + private GenericRoom _logRoom; + private GenericRoom _controlRoom; + + public MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot> logger, + MediaModBotConfiguration configuration, HomeserverResolverService hsResolver) { + logger.LogInformation("{} instantiated!", this.GetType().Name); + _hs = hs; _logger = logger; _configuration = configuration; - _logger.LogInformation("Getting commands..."); - _commands = services.GetServices<ICommand>(); - _logger.LogInformation("Got {} commands!", _commands.Count()); + _hsResolver = hsResolver; } /// <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> - [SuppressMessage("ReSharper", "FunctionNeverReturns")] public async Task StartAsync(CancellationToken cancellationToken) { + _listenerTask = Run(cancellationToken); + _logger.LogInformation("Bot started!"); + } + + private async Task Run(CancellationToken cancellationToken) { Directory.GetFiles("bot_data/cache").ToList().ForEach(File.Delete); - AuthenticatedHomeserverGeneric hs; - try { - hs = await _homeserverProviderService.GetAuthenticatedWithToken(_configuration.Homeserver, - _configuration.AccessToken); - } - catch (Exception e) { - _logger.LogError("{}", e.Message); - throw; - } BotData botData; try { - botData = await hs.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); + botData = await _hs.GetAccountData<BotData>("gay.rory.media_moderator_poc_data"); } catch (Exception e) { if (e is not MatrixException { ErrorCode: "M_NOT_FOUND" }) { @@ -62,20 +64,20 @@ public class MediaModBot : IHostedService { } botData = new BotData(); - var creationContent = CreateRoomRequest.CreatePrivate(hs, name: "Media Moderator PoC - Control room", roomAliasName: "media-moderator-poc-control-room"); + var creationContent = CreateRoomRequest.CreatePrivate(_hs, name: "Media Moderator PoC - Control room", roomAliasName: "media-moderator-poc-control-room"); creationContent.Invite = _configuration.Admins; creationContent.CreationContent["type"] = "gay.rory.media_moderator_poc.control_room"; - botData.ControlRoom = (await hs.CreateRoom(creationContent)).RoomId; + botData.ControlRoom = (await _hs.CreateRoom(creationContent)).RoomId; //set access rules to allow joining via control room creationContent.InitialState.Add(new StateEvent { Type = "m.room.join_rules", StateKey = "", - TypedContent = new JoinRulesEventData() { + TypedContent = new JoinRulesEventData { JoinRule = "knock_restricted", Allow = new() { - new JoinRulesEventData.AllowEntry() { + new JoinRulesEventData.AllowEntry { Type = "m.room_membership", RoomId = botData.ControlRoom } @@ -86,94 +88,257 @@ public class MediaModBot : IHostedService { creationContent.Name = "Media Moderator PoC - Log room"; creationContent.RoomAliasName = "media-moderator-poc-log-room"; creationContent.CreationContent["type"] = "gay.rory.media_moderator_poc.log_room"; - botData.LogRoom = (await hs.CreateRoom(creationContent)).RoomId; + botData.LogRoom = (await _hs.CreateRoom(creationContent)).RoomId; creationContent.Name = "Media Moderator PoC - Policy room"; creationContent.RoomAliasName = "media-moderator-poc-policy-room"; creationContent.CreationContent["type"] = "gay.rory.media_moderator_poc.policy_room"; - botData.PolicyRoom = (await hs.CreateRoom(creationContent)).RoomId; + botData.PolicyRoom = (await _hs.CreateRoom(creationContent)).RoomId; - await hs.SetAccountData("gay.rory.media_moderator_poc_data", botData); + await _hs.SetAccountData("gay.rory.media_moderator_poc_data", botData); } - PolicyRoom = await hs.GetRoom(botData.PolicyRoom); + _policyRoom = await _hs.GetRoom(botData.PolicyRoom ?? botData.ControlRoom); + _logRoom = await _hs.GetRoom(botData.LogRoom ?? botData.ControlRoom); + _controlRoom = await _hs.GetRoom(botData.ControlRoom); + + List<string> admins = new(); - hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(async () => { + while (!cancellationToken.IsCancellationRequested) { + var controlRoomMembers = _controlRoom.GetMembersAsync(); + await foreach (var member in controlRoomMembers) { + if ((member.TypedContent as RoomMemberEventData).Membership == "join") admins.Add(member.UserId); + } + + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + } + }, 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) => { var inviteEvent = args.Value.InviteState.Events.FirstOrDefault(x => - x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId); + x.Type == "m.room.member" && x.StateKey == _hs.WhoAmI.UserId); _logger.LogInformation( $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventData).Reason}"); if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender.EndsWith(":conduit.rory.gay")) { try { - var senderProfile = await hs.GetProfile(inviteEvent.Sender); - await (await hs.GetRoom(args.Key)).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); + var senderProfile = await _hs.GetProfile(inviteEvent.Sender); + await (await _hs.GetRoom(args.Key)).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!"); } catch (Exception e) { _logger.LogError("{}", e.ToString()); - await (await hs.GetRoom(args.Key)).LeaveAsync(reason: "I was unable to join the room: " + e); + await (await _hs.GetRoom(args.Key)).LeaveAsync(reason: "I was unable to join the room: " + e); } } }); - hs.SyncHelper.TimelineEventHandlers.Add(async @event => { - _logger.LogInformation( - "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(indent: true, ignoreNull: true)); - - var room = await hs.GetRoom(@event.RoomId); - // _logger.LogInformation(eventResponse.ToJson(indent: false)); - if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventData message }) { - if (message is { MessageType: "m.text" } && message.Body.StartsWith(_configuration.Prefix)) { - var command = _commands.FirstOrDefault(x => x.Name == message.Body.Split(' ')[0][_configuration.Prefix.Length..]); - if (command == null) { - await room.SendMessageEventAsync("m.room.message", - new RoomMessageEventData { - MessageType = "m.notice", - Body = "Command not found!" - }); - return; - } - var ctx = new CommandContext { - Room = room, - MessageEvent = @event, - Homeserver = hs - }; - if (await command.CanInvoke(ctx)) { - await command.Invoke(ctx); - } - else { - await room.SendMessageEventAsync("m.room.message", - new RoomMessageEventData { - MessageType = "m.notice", - Body = "You do not have permission to run this command!" + _hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + var room = await _hs.GetRoom(@event.RoomId); + try { + _logger.LogInformation( + "Got timeline event in {}: {}", @event.RoomId, @event.ToJson(indent: true, ignoreNull: true)); + + if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventData message }) { + if (message is { MessageType: "m.image" }) { + //check media + var matchedPolicy = await CheckMedia(@event); + if (matchedPolicy is null) return; + var matchedpolicyData = matchedPolicy.TypedContent as MediaPolicyStateEventData; + var recommendation = matchedpolicyData.Recommendation; + await _logRoom.SendMessageEventAsync("m.room.message", + new RoomMessageEventData( + body: + $"User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: {matchedPolicy.RawContent!.ToJson(ignoreNull: true)}", + messageType: "m.text") { + Format = "org.matrix.custom.html", + FormattedBody = + $"<font color=\"#FFFF00\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: <pre>{matchedPolicy.RawContent!.ToJson(ignoreNull: true)}</pre></font>" }); + switch (recommendation) { + case "warn_admins": { + await _controlRoom.SendMessageEventAsync("m.room.message", + new RoomMessageEventData(body: $"{string.Join(' ', admins)}\nUser {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image {message.Url}", + messageType: "m.text") { + Format = "org.matrix.custom.html", + FormattedBody = $"{string.Join(' ', admins.Select(u=>MessageFormatter.HtmlFormatMention(u)))}\n" + + $"<font color=\"#FF0000\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image <a href=\"{message.Url}\">{message.Url}</a></font>" + }); + break; + } + case "warn": { + await room.SendMessageEventAsync("m.room.message", + new RoomMessageEventData( + body: $"Please be careful when posting this image: {matchedpolicyData.Reason}", + messageType: "m.text") { + Format = "org.matrix.custom.html", + FormattedBody = + $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}</a></font>" + }); + break; + } + case "redact": { + await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + break; + } + case "spoiler": { + // <blockquote> + // <a href=\"https://matrix.to/#/@emma:rory.gay\">@emma:rory.gay</a><br> + // <a href=\"https://codeberg.org/crimsonfork/CN\"></a> + // <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + // <b>CN</b> + // </font>: + // <a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">test</a><br> + // <span data-mx-spoiler=\"\"><a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\"> + // <img src=\"mxc://rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\" height=\"69\"></a> + // </span> + // </blockquote> + await room.SendMessageEventAsync("m.room.message", + new RoomMessageEventData( + body: + $"Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:", + messageType: "m.text") { + Format = "org.matrix.custom.html", + FormattedBody = + $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:</a></font>" + }); + var imageUrl = message.Url; + await room.SendMessageEventAsync("m.room.message", + new RoomMessageEventData(body: $"CN: {imageUrl}", + messageType: "m.text") { + Format = "org.matrix.custom.html", + FormattedBody = $""" + <blockquote> + <font color=\"#dc143c\" data-mx-color=\"#dc143c\"> + <b>CN</b> + </font>: + <a href=\"{imageUrl}\">{matchedpolicyData.Reason}</a><br> + <span data-mx-spoiler=\"\"> + <a href=\"{imageUrl}\"> + <img src=\"{imageUrl}\" height=\"69\"> + </a> + </span> + </blockquote> + """ + }); + await room.RedactEventAsync(@event.EventId, "Automatically spoilered: " + matchedpolicyData.Reason); + break; + } + case "mute": { + await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + //change powerlevel to -1 + var currentPls = await room.GetPowerLevelsAsync(); + currentPls.Users[@event.Sender] = -1; + await room.SendStateEventAsync("m.room.power_levels", currentPls); + + break; + } + case "kick": { + await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + await room.KickAsync(@event.Sender, matchedpolicyData.Reason); + break; + } + case "ban": { + await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason); + await room.BanAsync(@event.Sender, matchedpolicyData.Reason); + break; + } + default: { + throw new ArgumentOutOfRangeException("recommendation", + $"Unknown response type {matchedpolicyData.Recommendation}!"); + } + } } } } + catch (Exception e) { + _logger.LogError("{}", e.ToString()); + await _controlRoom.SendMessageEventAsync("m.room.message", + MessageFormatter.FormatException($"Unable to ban user in {MessageFormatter.HtmlFormatMention(room.RoomId)}", e)); + await _logRoom.SendMessageEventAsync("m.room.message", + 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 hs.SyncHelper.RunSyncLoop(cancellationToken: cancellationToken); } /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> - public Task StopAsync(CancellationToken cancellationToken) { + public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Shutting down bot!"); - return Task.CompletedTask; } + private async Task<StateEventResponse?> CheckMedia(StateEventResponse @event) { + var stateList = _policyRoom.GetFullStateAsync(); + var hashAlgo = SHA3_256.Create(); + + var mxcUri = @event.RawContent["url"].GetValue<string>(); + var resolvedUri = await _hsResolver.ResolveMediaUri(mxcUri.Split('/')[2], mxcUri); + var uriHash = hashAlgo.ComputeHash(mxcUri.AsBytes().ToArray()); + byte[]? fileHash = null; + + try { + fileHash = await hashAlgo.ComputeHashAsync(await _hs._httpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex) { + await _logRoom.SendMessageEventAsync("m.room.message", + MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]} ({resolvedUri}), retrying via {_hs.HomeServerDomain}...", + ex)); + try { + resolvedUri = await _hsResolver.ResolveMediaUri(_hs.HomeServerDomain, mxcUri); + fileHash = await hashAlgo.ComputeHashAsync(await _hs._httpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex2) { + await _logRoom.SendMessageEventAsync("m.room.message", + MessageFormatter.FormatException($"Error calculating file hash via {_hs.HomeServerDomain} ({resolvedUri})!", ex2)); + } + } + + _logger.LogInformation("Checking media {url} with hash {hash}", resolvedUri, fileHash); - private async Task<bool> CheckMedia(StateEventResponse @event) { - var stateList = PolicyRoom.GetFullStateAsync(); await foreach (var state in stateList) { - if(state.Type != "gay.rory.media_moderator_poc.rule.media") continue; + if (state.Type != "gay.rory.media_moderator_poc.rule.media" && state.Type != "gay.rory.media_moderator_poc.rule.server") continue; + if (!state.RawContent.ContainsKey("entity")) { + _logger.LogWarning("Rule {rule} has no entity, this event was probably redacted!", state.StateKey); + continue; + } + _logger.LogInformation("Checking rule {rule}: {data}", state.StateKey, state.TypedContent.ToJson(ignoreNull: true, indent: false)); var rule = state.TypedContent as MediaPolicyStateEventData; - rule.Entity = rule.Entity.Replace("\\*", ".*").Replace("\\?", "."); - var regex = new Regex(rule.Entity); - if (regex.IsMatch(@event.RawContent["url"].GetValue<string>())) { - _logger.LogInformation("{url} matched rule {rule}", @event.RawContent["url"], rule.ToJson(ignoreNull: true)); - return true; + if (state.Type == "gay.rory.media_moderator_poc.rule.server" && rule.ServerEntity is not null) { + rule.ServerEntity = rule.ServerEntity.Replace("\\*", ".*").Replace("\\?", "."); + var regex = new Regex($"mxc://({rule.ServerEntity})/.*", RegexOptions.Compiled | RegexOptions.IgnoreCase); + if (regex.IsMatch(@event.RawContent["url"].GetValue<string>())) { + _logger.LogInformation("{url} matched rule {rule}", @event.RawContent["url"], rule.ToJson(ignoreNull: true)); + return state; + } } + + if (rule.Entity is not null && uriHash.SequenceEqual(rule.Entity)) { + _logger.LogInformation("{url} matched rule {rule} by uri hash", @event.RawContent["url"], rule.ToJson(ignoreNull: true)); + return state; + } + + _logger.LogInformation("uri hash {uriHash} did not match rule's {ruleUriHash}", Convert.ToBase64String(uriHash), Convert.ToBase64String(rule.Entity)); + + if (rule.FileHash is not null && fileHash is not null && rule.FileHash.SequenceEqual(fileHash)) { + _logger.LogInformation("{url} matched rule {rule} by file hash", @event.RawContent["url"], rule.ToJson(ignoreNull: true)); + return state; + } + + _logger.LogInformation("file hash {fileHash} did not match rule's {ruleFileHash}", Convert.ToBase64String(fileHash), Convert.ToBase64String(rule.FileHash)); + + + //check pixels every 10% of the way through the image using ImageSharp + // var image = Image.Load(await _hs._httpClient.GetStreamAsync(resolvedUri)); } - return false; + + + _logger.LogInformation("{url} did not match any rules", @event.RawContent["url"]); + + return null; } } diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs b/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs index c441e2a..d848abe 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/MediaModBotConfiguration.cs @@ -3,11 +3,8 @@ using Microsoft.Extensions.Configuration; namespace MediaModeratorPoC.Bot; public class MediaModBotConfiguration { - public MediaModBotConfiguration(IConfiguration config) { - config.GetRequiredSection("MediaMod").Bind(this); - } - public string Homeserver { get; set; } = ""; - public string AccessToken { get; set; } = ""; - public string Prefix { get; set; } + public MediaModBotConfiguration(IConfiguration config) => config.GetRequiredSection("MediaMod").Bind(this); + public List<string> Admins { get; set; } = new(); + public bool DemoMode { get; set; } = false; } diff --git a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs index 812ccf2..f37d33c 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs +++ b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs @@ -4,15 +4,20 @@ using LibMatrix.Interfaces; namespace MediaModeratorPoC.Bot.StateEventTypes; - +[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")] [MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.media")] public class MediaPolicyStateEventData : IStateEventType { /// <summary> - /// Entity this ban applies to, can use * and ? as globs. - /// This is an MXC URI. + /// This is an MXC URI, hashed with SHA3-256. /// </summary> [JsonPropertyName("entity")] - public string Entity { get; set; } + public byte[] 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 @@ -21,10 +26,10 @@ public class MediaPolicyStateEventData : IStateEventType { public string? Reason { get; set; } /// <summary> - /// Suggested action to take + /// Suggested action to take, one of `ban`, `kick`, `mute`, `redact`, `spoiler`, `warn` or `warn_admins` /// </summary> [JsonPropertyName("recommendation")] - public string? Recommendation { get; set; } + public string Recommendation { get; set; } = "warn"; /// <summary> /// Expiry time in milliseconds since the unix epoch, or null if the ban has no expiry. @@ -39,6 +44,9 @@ public class MediaPolicyStateEventData : IStateEventType { [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")] public DateTime? ExpiryDateTime { get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime; - set => Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds(); + set => Expiry = value is null ? null : ((DateTimeOffset)value).ToUnixTimeMilliseconds(); } + + [JsonPropertyName("file_hash")] + public byte[]? FileHash { get; set; } } diff --git a/ExampleBots/MediaModeratorPoC/MediaModeratorPoC.csproj b/ExampleBots/MediaModeratorPoC/MediaModeratorPoC.csproj index 1fc421a..c3a5d87 100644 --- a/ExampleBots/MediaModeratorPoC/MediaModeratorPoC.csproj +++ b/ExampleBots/MediaModeratorPoC/MediaModeratorPoC.csproj @@ -17,12 +17,13 @@ </PropertyGroup> <ItemGroup> - <ProjectReference Include="..\..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" /> +<!-- <ProjectReference Include="..\..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" />--> <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" /> + <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.5.23280.8" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.7.23375.6" /> </ItemGroup> <ItemGroup> <Content Include="appsettings*.json"> diff --git a/ExampleBots/MediaModeratorPoC/Program.cs b/ExampleBots/MediaModeratorPoC/Program.cs index 1bc0c62..413d91d 100644 --- a/ExampleBots/MediaModeratorPoC/Program.cs +++ b/ExampleBots/MediaModeratorPoC/Program.cs @@ -1,9 +1,8 @@ // See https://aka.ms/new-console-template for more information -using ArcaneLibs; using LibMatrix.Services; +using LibMatrix.Utilities.Bot; using MediaModeratorPoC.Bot; -using MediaModeratorPoC.Bot.Interfaces; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -17,11 +16,9 @@ var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => { ) ); services.AddSingleton<MediaModBotConfiguration>(); + services.AddRoryLibMatrixServices(); - foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) { - Console.WriteLine($"Adding command {commandClass.Name}"); - services.AddScoped(typeof(ICommand), commandClass); - } + services.AddBot(withCommands: true); services.AddHostedService<MediaModBot>(); }).UseConsoleLifetime().Build(); diff --git a/ExampleBots/MediaModeratorPoC/appsettings.Development.json b/ExampleBots/MediaModeratorPoC/appsettings.Development.json index a895170..600efc3 100644 --- a/ExampleBots/MediaModeratorPoC/appsettings.Development.json +++ b/ExampleBots/MediaModeratorPoC/appsettings.Development.json @@ -6,13 +6,15 @@ "Microsoft": "Information" } }, - "MediaMod": { + "LibMatrixBot": { // The homeserver to connect to "Homeserver": "rory.gay", // The access token to use "AccessToken": "syt_xxxxxxxxxxxxxxxxx", // The command prefix - "Prefix": "?", + "Prefix": "?" + }, + "MediaMod": { // List of people who should be invited to the control room "Admins": [ "@emma:conduit.rory.gay", diff --git a/LibMatrix.sln b/LibMatrix.sln index 4613119..fd241b3 100644 --- a/LibMatrix.sln +++ b/LibMatrix.sln @@ -5,9 +5,19 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{E81BDE83-7DC0-4639-A373-0D63029D620F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{840309F0-435B-43A7-8471-8C2BE643889D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{EA172316-118E-4CDE-9DCE-B9747D4DC183}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A6345ECE-4C5E-400F-9130-886E343BF314}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{32D9616B-91BB-4B43-97C6-2C3840C12EA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{1B1B2197-61FB-416F-B6C8-845F2E5A0442}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaModeratorPoC", "ExampleBots\MediaModeratorPoC\MediaModeratorPoC.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,13 +32,31 @@ Global {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.Build.0 = Release|Any CPU - {E81BDE83-7DC0-4639-A373-0D63029D620F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E81BDE83-7DC0-4639-A373-0D63029D620F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E81BDE83-7DC0-4639-A373-0D63029D620F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E81BDE83-7DC0-4639-A373-0D63029D620F}.Release|Any CPU.Build.0 = Release|Any CPU - {EA172316-118E-4CDE-9DCE-B9747D4DC183}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA172316-118E-4CDE-9DCE-B9747D4DC183}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA172316-118E-4CDE-9DCE-B9747D4DC183}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA172316-118E-4CDE-9DCE-B9747D4DC183}.Release|Any CPU.Build.0 = Release|Any CPU + {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.Build.0 = Release|Any CPU + {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.Build.0 = Release|Any CPU + {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.Build.0 = Release|Any CPU + {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.Build.0 = Release|Any CPU + {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D} + {8F0A820E-F6AE-45A2-970E-7A3759693919} = {840309F0-435B-43A7-8471-8C2BE643889D} + {35DF9A1A-D988-4225-AFA3-06BB8EDEB559} = {A6345ECE-4C5E-400F-9130-886E343BF314} + {3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7} = {A6345ECE-4C5E-400F-9130-886E343BF314} EndGlobalSection EndGlobal diff --git a/LibMatrix.sln.DotSettings.user b/LibMatrix.sln.DotSettings.user new file mode 100644 index 0000000..822b0e8 --- /dev/null +++ b/LibMatrix.sln.DotSettings.user @@ -0,0 +1,4 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer> + <Assembly Path="/home/root@Rory/.cache/NuGetPackages/microsoft.extensions.hosting.abstractions/7.0.0/lib/net7.0/Microsoft.Extensions.Hosting.Abstractions.dll" /> +</AssemblyExplorer></s:String></wpf:ResourceDictionary> \ No newline at end of file diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index 4d81b6e..d4017ed 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -1,12 +1,7 @@ -using System; using System.Diagnostics; -using System.IO; -using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; namespace LibMatrix.Extensions; diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs index 99fa72d..f39f300 100644 --- a/LibMatrix/Extensions/JsonElementExtensions.cs +++ b/LibMatrix/Extensions/JsonElementExtensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; diff --git a/LibMatrix/Filters/SyncFilter.cs b/LibMatrix/Filters/SyncFilter.cs index e281346..c907f6b 100644 --- a/LibMatrix/Filters/SyncFilter.cs +++ b/LibMatrix/Filters/SyncFilter.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; namespace LibMatrix.Filters; diff --git a/LibMatrix/Helpers/MatrixEventAttribute.cs b/LibMatrix/Helpers/MatrixEventAttribute.cs index 7556019..7efc039 100644 --- a/LibMatrix/Helpers/MatrixEventAttribute.cs +++ b/LibMatrix/Helpers/MatrixEventAttribute.cs @@ -1,5 +1,3 @@ -using System; - namespace LibMatrix.Helpers; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] diff --git a/LibMatrix/Helpers/MediaResolver.cs b/LibMatrix/Helpers/MediaResolver.cs index 6ddb221..5886618 100644 --- a/LibMatrix/Helpers/MediaResolver.cs +++ b/LibMatrix/Helpers/MediaResolver.cs @@ -1,6 +1,7 @@ +using LibMatrix.Services; + namespace LibMatrix.Helpers; -public class MediaResolver { - public static string ResolveMediaUri(string homeserver, string mxc) => - mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/"); +public static class MediaResolver { + public static string ResolveMediaUri(string homeserver, string mxc) => mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/"); } diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs new file mode 100644 index 0000000..ff0a00f --- /dev/null +++ b/LibMatrix/Helpers/MessageFormatter.cs @@ -0,0 +1,39 @@ +using ArcaneLibs.Extensions; +using LibMatrix.StateEventTypes.Spec; + +namespace LibMatrix.Helpers; + +public static class MessageFormatter { + public static RoomMessageEventData FormatError(string error) { + return new RoomMessageEventData(body: error, messageType: "m.text") { + FormattedBody = $"<font color=\"#FF0000\">{error}: {error}</font>", + Format = "org.matrix.custom.html" + }; + } + + public static RoomMessageEventData FormatException(string error, Exception e) { + return new RoomMessageEventData(body: $"{error}: {e.Message}", messageType: "m.text") { + FormattedBody = $"<font color=\"#FF0000\">{error}: <pre>{e.Message}</pre>" + + $"</font>", + Format = "org.matrix.custom.html" + }; + } + + public static RoomMessageEventData FormatSuccess(string text) { + return new RoomMessageEventData(body: text, messageType: "m.text") { + FormattedBody = $"<font color=\"#00FF00\">{text}</font>", + Format = "org.matrix.custom.html" + }; + } + + public static RoomMessageEventData FormatSuccessJson(string text, object data) { + return new RoomMessageEventData(body: text, messageType: "m.text") { + FormattedBody = $"<font color=\"#00FF00\">{text}: <pre>{data.ToJson(ignoreNull: true)}</pre></font>", + Format = "org.matrix.custom.html" + }; + } + + public static string HtmlFormatMention(string id, string? displayName = null) { + return $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>"; + } +} diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs index 83b1685..d719184 100644 --- a/LibMatrix/Helpers/SyncHelper.cs +++ b/LibMatrix/Helpers/SyncHelper.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Net.Http.Json; using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Filters; using LibMatrix.Homeservers; using LibMatrix.Responses; @@ -15,15 +9,7 @@ using LibMatrix.Services; namespace LibMatrix.Helpers; -public class SyncHelper { - private readonly AuthenticatedHomeserverGeneric _homeserver; - private readonly TieredStorageService _storageService; - - public SyncHelper(AuthenticatedHomeserverGeneric homeserver, TieredStorageService storageService) { - _homeserver = homeserver; - _storageService = storageService; - } - +public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, TieredStorageService storageService) { public async Task<SyncResult?> Sync( string? since = null, int? timeout = 30000, @@ -31,7 +17,7 @@ public class SyncHelper { SyncFilter? filter = null, CancellationToken? cancellationToken = null) { var outFileName = "sync-" + - (await _storageService.CacheStorageProvider.GetAllKeysAsync()).Count( + (await storageService.CacheStorageProvider.GetAllKeysAsync()).Count( x => x.StartsWith("sync")) + ".json"; var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}"; @@ -40,7 +26,7 @@ public class SyncHelper { // else url += "&full_state=true"; Console.WriteLine("Calling: " + url); try { - var req = await _homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None); + var req = await homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None); // var res = await JsonSerializer.DeserializeAsync<SyncResult>(await req.Content.ReadAsStreamAsync()); @@ -79,10 +65,10 @@ public class SyncHelper { SyncFilter? filter = null, CancellationToken? cancellationToken = null ) { - await Task.WhenAll((await _storageService.CacheStorageProvider.GetAllKeysAsync()) + await Task.WhenAll((await storageService.CacheStorageProvider.GetAllKeysAsync()) .Where(x => x.StartsWith("sync")) .ToList() - .Select(x => _storageService.CacheStorageProvider.DeleteObjectAsync(x))); + .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, @@ -116,7 +102,15 @@ public class SyncHelper { 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(); + var tasks = TimelineEventHandlers.Select(x => { + try { + return x(stateEventResponse); + } + catch (Exception e) { + Console.WriteLine(e); + return Task.CompletedTask; + } + }).ToList(); await Task.WhenAll(tasks); } } diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index bb34112..0b3201c 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using System.Threading.Tasks; using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Responses; diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs index 3b0bc10..218ded0 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using ArcaneLibs.Extensions; using LibMatrix.Filters; using LibMatrix.Responses.Admin; diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index fc31f4f..923986d 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Net.Http.Json; -using System.Threading; -using System.Threading.Tasks; using LibMatrix.Extensions; using LibMatrix.Responses; using LibMatrix.StateEventTypes.Spec; diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs index e07e136..519d8ed 100644 --- a/LibMatrix/Interfaces/Services/IStorageProvider.cs +++ b/LibMatrix/Interfaces/Services/IStorageProvider.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - namespace LibMatrix.Interfaces.Services; public interface IStorageProvider { diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index 8ae57cc..709e079 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -4,6 +4,7 @@ <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs index 1b38a6e..3aaad19 100644 --- a/LibMatrix/MatrixException.cs +++ b/LibMatrix/MatrixException.cs @@ -1,7 +1,5 @@ -using System; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; namespace LibMatrix; diff --git a/LibMatrix/MessagesResponse.cs b/LibMatrix/MessagesResponse.cs index d7bb54a..f09d136 100644 --- a/LibMatrix/MessagesResponse.cs +++ b/LibMatrix/MessagesResponse.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; using LibMatrix.Responses; diff --git a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs index bbc23e6..f035184 100644 --- a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs +++ b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; namespace LibMatrix.Responses.Admin; diff --git a/LibMatrix/Responses/ClientVersionsResponse.cs b/LibMatrix/Responses/ClientVersionsResponse.cs index 7fac565..8e0a92a 100644 --- a/LibMatrix/Responses/ClientVersionsResponse.cs +++ b/LibMatrix/Responses/ClientVersionsResponse.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; namespace LibMatrix.Responses; diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs index 2c05088..24c9ae0 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.StateEventTypes.Spec; @@ -85,7 +81,7 @@ public class CreateRoomRequest { } public static CreateRoomRequest CreatePrivate(AuthenticatedHomeserverGeneric hs, string? name = null, string? roomAliasName = null) { - var request = new CreateRoomRequest() { + var request = new CreateRoomRequest { Name = name ?? "Private Room", Visibility = "private", CreationContent = new(), diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index 8ba9a4b..4c784ce 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; using System.Web; using LibMatrix.Extensions; using LibMatrix.Homeservers; using LibMatrix.Responses; using LibMatrix.StateEventTypes.Spec; +using Microsoft.Extensions.Logging; namespace LibMatrix.RoomTypes; @@ -19,6 +15,8 @@ public class GenericRoom { internal readonly MatrixHttpClient _httpClient; public GenericRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) { + if (string.IsNullOrWhiteSpace(roomId)) + throw new ArgumentException("Room ID cannot be null or whitespace", nameof(roomId)); Homeserver = homeserver; _httpClient = homeserver._httpClient; RoomId = roomId; @@ -105,18 +103,24 @@ public class GenericRoom { }); } - // TODO: rewrite (members endpoint?) public async IAsyncEnumerable<StateEventResponse> GetMembersAsync(bool joinedOnly = true) { - var res = GetFullStateAsync(); - await foreach (var member in res) { - if (member?.Type != "m.room.member") continue; - if (joinedOnly && (member.TypedContent as RoomMemberEventData)?.Membership is not "join") continue; - yield return member; + // var res = GetFullStateAsync(); + // await foreach (var member in res) { + // if (member?.Type != "m.room.member") continue; + // if (joinedOnly && (member.TypedContent as RoomMemberEventData)?.Membership is not "join") continue; + // yield return member; + // } + var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members"); + var result = + JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(await res.Content.ReadAsStreamAsync()); + await foreach (var resp in result) { + if (resp?.Type != "m.room.member") continue; + if (joinedOnly && (resp.TypedContent as RoomMemberEventData)?.Membership is not "join") continue; + yield return resp; } } - #region Utility shortcuts public async Task<List<string>> GetAliasesAsync() { @@ -150,13 +154,11 @@ public class GenericRoom { return res.Type; } - public async Task<RoomPowerLevelEventData?> GetPowerLevelAsync() => + public async Task<RoomPowerLevelEventData?> GetPowerLevelsAsync() => await GetStateAsync<RoomPowerLevelEventData>("m.room.power_levels"); #endregion - - public async Task ForgetAsync() => await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null); @@ -178,12 +180,16 @@ public class GenericRoom { new UserIdAndReason { UserId = userId }); public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) => - await (await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content)) + 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) => + await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); public async Task<EventIdResponse> SendMessageEventAsync(string eventType, RoomMessageEventData content) { var res = await _httpClient.PutAsJsonAsync( - $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content, new JsonSerializerOptions() { + $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>(); @@ -227,4 +233,10 @@ public class GenericRoom { public async Task<T> GetEvent<T>(string eventId) { return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}"); } + + public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) { + var data = new { reason }; + return (await (await _httpClient.PutAsJsonAsync( + $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!; + } } diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs index 017a123..e1e9879 100644 --- a/LibMatrix/RoomTypes/SpaceRoom.cs +++ b/LibMatrix/RoomTypes/SpaceRoom.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Threading; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Homeservers; namespace LibMatrix.RoomTypes; diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs index 776c7eb..71d9860 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Headers; using System.Net.Http.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; using ArcaneLibs.Extensions; using LibMatrix.Extensions; using LibMatrix.Homeservers; diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index dcd0fe9..f2c0781 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -1,26 +1,16 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using ArcaneLibs.Extensions; using LibMatrix.Extensions; using Microsoft.Extensions.Logging; namespace LibMatrix.Services; -public class HomeserverResolverService { +public class HomeserverResolverService(ILogger<HomeserverResolverService>? logger) { private readonly MatrixHttpClient _httpClient = new(); - private readonly ILogger<HomeserverResolverService> _logger; private static readonly Dictionary<string, string> _wellKnownCache = new(); private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new(); - public HomeserverResolverService(ILogger<HomeserverResolverService> logger) { - _logger = logger; - } - public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) { var res = await _resolveHomeserverFromWellKnown(homeserver); if (!res.StartsWith("http")) res = "https://" + res; @@ -38,7 +28,7 @@ public class HomeserverResolverService { } string? result = null; - _logger.LogInformation("Attempting to resolve homeserver: {}", homeserver); + logger?.LogInformation("Attempting to resolve homeserver: {}", homeserver); result ??= await _tryResolveFromClientWellknown(homeserver); result ??= await _tryResolveFromServerWellknown(homeserver); result ??= await _tryCheckIfDomainHasHomeserver(homeserver); @@ -46,7 +36,7 @@ public class HomeserverResolverService { if (result is null) throw new InvalidDataException($"Failed to resolve homeserver for {homeserver}! Is it online and configured correctly?"); //success! - _logger.LogInformation("Resolved homeserver: {} -> {}", homeserver, result); + logger?.LogInformation("Resolved homeserver: {} -> {}", homeserver, result); _wellKnownCache[homeserver] = result; sem.Release(); return result; @@ -60,7 +50,7 @@ public class HomeserverResolverService { return hs; } - _logger.LogInformation("No client well-known..."); + logger?.LogInformation("No client well-known..."); return null; } @@ -72,15 +62,15 @@ public class HomeserverResolverService { return hs; } - _logger.LogInformation("No server well-known..."); + logger?.LogInformation("No server well-known..."); return null; } private async Task<string?> _tryCheckIfDomainHasHomeserver(string homeserver) { - _logger.LogInformation("Checking if {} hosts a homeserver...", homeserver); + logger?.LogInformation("Checking if {} hosts a homeserver...", homeserver); if (await _httpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions")) return homeserver; - _logger.LogInformation("No homeserver on shortname..."); + logger?.LogInformation("No homeserver on shortname..."); return null; } @@ -88,4 +78,12 @@ public class HomeserverResolverService { homeserver = homeserver.Replace("https://", $"https://{subdomain}."); return await _tryCheckIfDomainHasHomeserver(homeserver); } + + public async Task<string?> ResolveMediaUri(string homeserver, string mxc) { + if (homeserver is null) throw new ArgumentNullException(nameof(homeserver)); + if (mxc is null) throw new ArgumentNullException(nameof(mxc)); + if (!mxc.StartsWith("mxc://")) throw new InvalidDataException("mxc must start with mxc://"); + homeserver = await ResolveHomeserverFromWellKnown(homeserver); + return mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/"); + } } diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs index 9c4cdb9..b1c98e1 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Microsoft.Extensions.DependencyInjection; namespace LibMatrix.Services; diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 26dc39a..175d706 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ArcaneLibs; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs index c3fee34..7a4b3f3 100644 --- a/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs +++ b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs index 754a9dc..ad65b0f 100644 --- a/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs +++ b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs index 384ca43..269bd6d 100644 --- a/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs index eadba67..7ba3428 100644 --- a/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs index 1c73346..deca7c8 100644 --- a/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs index 08e8f22..b64c1dd 100644 --- a/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs index c0aed9e..2e66bd9 100644 --- a/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs @@ -1,6 +1,4 @@ -using System; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs index c5a95ae..5167502 100644 --- a/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs index df80a08..3f2c39e 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs index 4d3fabf..f71e1fb 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs index 0b1bd5c..31f9411 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs index 126117d..9673dcc 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs index 7c181ae..c99aa8d 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs index 11a0e82..d13c273 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs @@ -1,5 +1,5 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; +using ArcaneLibs.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; @@ -7,8 +7,21 @@ namespace LibMatrix.StateEventTypes.Spec; [MatrixEvent(EventName = "m.room.message")] public class RoomMessageEventData : IStateEventType { + public RoomMessageEventData() { } + + public RoomMessageEventData(string messageType, string body) { + MessageType = messageType; + Body = body; + } + + public RoomMessageEventData(string body) : this() { + Body = body; + MessageType = "m.notice"; + } + [JsonPropertyName("body")] public string Body { get; set; } + [JsonPropertyName("msgtype")] public string MessageType { get; set; } = "m.notice"; @@ -28,7 +41,6 @@ public class RoomMessageEventData : IStateEventType { public string? Url { get; set; } public class MessageRelatesTo { - [JsonPropertyName("m.in_reply_to")] public MessageInReplyTo? InReplyTo { get; set; } diff --git a/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs index 2245793..e04f0dc 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs index 10ef3f5..bb78aeb 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs index 3c985f6..b4f7d53 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs index eaf9e8c..c3deb98 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs index cebb238..3812c46 100644 --- a/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs index a258707..d00b464 100644 --- a/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs index 5ccab88..e8c6d18 100644 --- a/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs index 6477290..ebd083e 100644 --- a/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs +++ b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs @@ -1,5 +1,4 @@ using System.Text.Json.Serialization; -using LibMatrix.Extensions; using LibMatrix.Helpers; using LibMatrix.Interfaces; diff --git a/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs b/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs index 4dbee54..4dbee54 100644 --- a/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs +++ b/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs diff --git a/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj index 205ff68..aad8ee0 100644 --- a/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj +++ b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj @@ -5,6 +5,7 @@ <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <InvariantGlobalization>true</InvariantGlobalization> + <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> @@ -13,7 +14,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\LibMatrix\LibMatrix.csproj" /> + <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" /> </ItemGroup> </Project> diff --git a/LibMatrix.DebugDataValidationApi/Program.cs b/Utilities/LibMatrix.DebugDataValidationApi/Program.cs index cf9dc55..cf9dc55 100644 --- a/LibMatrix.DebugDataValidationApi/Program.cs +++ b/Utilities/LibMatrix.DebugDataValidationApi/Program.cs diff --git a/LibMatrix.DebugDataValidationApi/Properties/launchSettings.json b/Utilities/LibMatrix.DebugDataValidationApi/Properties/launchSettings.json index fe668ce..fe668ce 100644 --- a/LibMatrix.DebugDataValidationApi/Properties/launchSettings.json +++ b/Utilities/LibMatrix.DebugDataValidationApi/Properties/launchSettings.json diff --git a/LibMatrix.DebugDataValidationApi/appsettings.Development.json b/Utilities/LibMatrix.DebugDataValidationApi/appsettings.Development.json index 12c8ab9..12c8ab9 100644 --- a/LibMatrix.DebugDataValidationApi/appsettings.Development.json +++ b/Utilities/LibMatrix.DebugDataValidationApi/appsettings.Development.json diff --git a/LibMatrix.DebugDataValidationApi/appsettings.json b/Utilities/LibMatrix.DebugDataValidationApi/appsettings.json index 10f68b8..10f68b8 100644 --- a/LibMatrix.DebugDataValidationApi/appsettings.json +++ b/Utilities/LibMatrix.DebugDataValidationApi/appsettings.json diff --git a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs new file mode 100644 index 0000000..42cdb6c --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs @@ -0,0 +1,44 @@ +using ArcaneLibs; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using LibMatrix.StateEventTypes.Spec; +using LibMatrix.Utilities.Bot.Services; +using MediaModeratorPoC.Bot.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Utilities.Bot; + +public static class BotCommandInstaller { + public static IServiceCollection AddBotCommands(this IServiceCollection services) { + foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) { + Console.WriteLine($"Adding command {commandClass.Name}"); + services.AddScoped(typeof(ICommand), commandClass); + } + + return services; + } + + public static IServiceCollection AddBot(this IServiceCollection services, bool withCommands = true) { + services.AddSingleton<LibMatrixBotConfiguration>(); + + services.AddScoped<AuthenticatedHomeserverGeneric>(x => { + var config = x.GetService<LibMatrixBotConfiguration>(); + var hsProvider = x.GetService<HomeserverProviderService>(); + var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; + + return hs; + }); + + if (withCommands) { + Console.WriteLine("Adding command handler..."); + services.AddBotCommands(); + services.AddHostedService<CommandListenerHostedService>(); + // services.AddSingleton<IHostedService, CommandListenerHostedService>(); + } + return services; + } +} diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/HelpCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs index 8d63daa..c975c8b 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/HelpCommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs @@ -17,9 +17,6 @@ public class HelpCommand(IServiceProvider services) : ICommand { sb.AppendLine($"- {command.Name}: {command.Description}"); } - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - MessageType = "m.notice", - Body = sb.ToString() - }); + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(messageType: "m.notice", body: sb.ToString())); } } diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/PingCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs index f9f46c2..e7f3b10 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Commands/PingCommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs @@ -8,8 +8,6 @@ public class PingCommand : ICommand { public string Description { get; } = "Pong!"; public async Task Invoke(CommandContext ctx) { - await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData { - Body = "pong!" - }); + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(body: "pong!")); } } diff --git a/ExampleBots/MediaModeratorPoC/Bot/FileStorageProvider.cs b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs index d5b991a..d5b991a 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/FileStorageProvider.cs +++ b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs new file mode 100644 index 0000000..0ad3e09 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs @@ -0,0 +1,21 @@ +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using LibMatrix.StateEventTypes.Spec; + +namespace MediaModeratorPoC.Bot.Interfaces; + +public class CommandContext { + public GenericRoom Room { get; set; } + public StateEventResponse MessageEvent { get; set; } + + public string MessageContentWithoutReply => + (MessageEvent.TypedContent as RoomMessageEventData)! + .Body.Split('\n') + .SkipWhile(x => x.StartsWith(">")) + .Aggregate((x, y) => $"{x}\n{y}"); + + public string CommandName => MessageContentWithoutReply.Split(' ')[0][1..]; + public string[] Args => MessageContentWithoutReply.Split(' ')[1..]; + public AuthenticatedHomeserverGeneric Homeserver { get; set; } +} diff --git a/ExampleBots/MediaModeratorPoC/Bot/Interfaces/ICommand.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs index a8fce94..a8fce94 100644 --- a/ExampleBots/MediaModeratorPoC/Bot/Interfaces/ICommand.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj new file mode 100644 index 0000000..db6570d --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <LangVersion>preview</LangVersion> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> + </ItemGroup> + + +</Project> diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs new file mode 100644 index 0000000..118b4df --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Configuration; + +namespace LibMatrix.Utilities.Bot; + +public class LibMatrixBotConfiguration { + public LibMatrixBotConfiguration(IConfiguration config) => config.GetRequiredSection("LibMatrixBot").Bind(this); + public string Homeserver { get; set; } = ""; + public string AccessToken { get; set; } = ""; + public string Prefix { get; set; } = "?"; +} diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs new file mode 100644 index 0000000..d5e7dd6 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs @@ -0,0 +1,79 @@ +using LibMatrix.Homeservers; +using LibMatrix.StateEventTypes.Spec; +using MediaModeratorPoC.Bot.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Utilities.Bot.Services; + +public class CommandListenerHostedService : IHostedService { + private readonly AuthenticatedHomeserverGeneric _hs; + private readonly ILogger<CommandListenerHostedService> _logger; + private readonly IEnumerable<ICommand> _commands; + private readonly LibMatrixBotConfiguration _config; + + private Task? _listenerTask; + + public CommandListenerHostedService(AuthenticatedHomeserverGeneric hs, ILogger<CommandListenerHostedService> logger, IServiceProvider services, + LibMatrixBotConfiguration config) { + logger.LogInformation("{} instantiated!", this.GetType().Name); + _hs = hs; + _logger = logger; + _config = config; + _logger.LogInformation("Getting commands..."); + _commands = services.GetServices<ICommand>(); + _logger.LogInformation("Got {} commands!", _commands.Count()); + } + + /// <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 Task StartAsync(CancellationToken cancellationToken) { + _listenerTask = Run(cancellationToken); + _logger.LogInformation("Command listener started (StartAsync)!"); + return Task.CompletedTask; + } + + private async Task? Run(CancellationToken cancellationToken) { + _logger.LogInformation("Starting command listener!"); + _hs.SyncHelper.TimelineEventHandlers.Add(async @event => { + var room = await _hs.GetRoom(@event.RoomId); + // _logger.LogInformation(eventResponse.ToJson(indent: false)); + if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventData message }) { + if (message is { MessageType: "m.text" }) { + var messageContentWithoutReply = message.Body.Split('\n', StringSplitOptions.RemoveEmptyEntries).SkipWhile(x=>x.StartsWith(">")).Aggregate((x, y) => $"{x}\n{y}"); + if (messageContentWithoutReply.StartsWith(_config.Prefix)) { + var command = _commands.FirstOrDefault(x => x.Name == messageContentWithoutReply.Split(' ')[0][_config.Prefix.Length..]); + if (command == null) { + await room.SendMessageEventAsync("m.room.message", + new RoomMessageEventData(messageType: "m.notice", body: "Command not found!")); + return; + } + + var ctx = new CommandContext { + Room = room, + MessageEvent = @event, + Homeserver = _hs + }; + if (await command.CanInvoke(ctx)) { + await command.Invoke(ctx); + } + else { + await room.SendMessageEventAsync("m.room.message", + new RoomMessageEventData(messageType: "m.notice", body: "You do not have permission to run this command!")); + } + } + } + } + }); + await _hs.SyncHelper.RunSyncLoop(cancellationToken: cancellationToken); + } + + /// <summary>Triggered when the application host is performing a graceful shutdown.</summary> + /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> + public Task StopAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Shutting down command listener!"); + _listenerTask.Wait(cancellationToken); + return Task.CompletedTask; + } +} |