From cf455ed8de20bbee011289223e7d8d5775dfd69e Mon Sep 17 00:00:00 2001 From: TheArcaneBrony Date: Tue, 5 Sep 2023 06:28:52 +0200 Subject: Media moderator PoC works, abstract command handling to library --- .../LibMatrix.Utilities.Bot/BotCommandInstaller.cs | 44 ++++++++++++ .../Commands/HelpCommand.cs | 22 ++++++ .../Commands/PingCommand.cs | 13 ++++ .../LibMatrix.Utilities.Bot/FileStorageProvider.cs | 38 +++++++++++ .../Interfaces/CommandContext.cs | 21 ++++++ .../LibMatrix.Utilities.Bot/Interfaces/ICommand.cs | 12 ++++ .../LibMatrix.Utilities.Bot.csproj | 21 ++++++ .../LibMatrixBotConfiguration.cs | 10 +++ .../Services/CommandListenerHostedService.cs | 79 ++++++++++++++++++++++ 9 files changed, 260 insertions(+) create mode 100644 Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/Interfaces/CommandContext.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj create mode 100644 Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs create mode 100644 Utilities/LibMatrix.Utilities.Bot/Services/CommandListenerHostedService.cs (limited to 'Utilities/LibMatrix.Utilities.Bot') 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().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(); + + services.AddScoped(x => { + var config = x.GetService(); + var hsProvider = x.GetService(); + var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; + + return hs; + }); + + if (withCommands) { + Console.WriteLine("Adding command handler..."); + services.AddBotCommands(); + services.AddHostedService(); + // services.AddSingleton(); + } + return services; + } +} diff --git a/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs new file mode 100644 index 0000000..c975c8b --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/HelpCommand.cs @@ -0,0 +1,22 @@ +using System.Text; +using LibMatrix.StateEventTypes.Spec; +using MediaModeratorPoC.Bot.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace MediaModeratorPoC.Bot.Commands; + +public class HelpCommand(IServiceProvider services) : ICommand { + public string Name { get; } = "help"; + public string Description { get; } = "Displays this help message"; + + public async Task Invoke(CommandContext ctx) { + var sb = new StringBuilder(); + sb.AppendLine("Available commands:"); + var commands = services.GetServices().ToList(); + foreach (var command in commands) { + sb.AppendLine($"- {command.Name}: {command.Description}"); + } + + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(messageType: "m.notice", body: sb.ToString())); + } +} diff --git a/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs new file mode 100644 index 0000000..e7f3b10 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Commands/PingCommand.cs @@ -0,0 +1,13 @@ +using LibMatrix.StateEventTypes.Spec; +using MediaModeratorPoC.Bot.Interfaces; + +namespace MediaModeratorPoC.Bot.Commands; + +public class PingCommand : ICommand { + public string Name { get; } = "ping"; + public string Description { get; } = "Pong!"; + + public async Task Invoke(CommandContext ctx) { + await ctx.Room.SendMessageEventAsync("m.room.message", new RoomMessageEventData(body: "pong!")); + } +} diff --git a/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs new file mode 100644 index 0000000..d5b991a --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using ArcaneLibs.Extensions; +using LibMatrix.Interfaces.Services; +using Microsoft.Extensions.Logging; + +namespace MediaModeratorPoC.Bot; + +public class FileStorageProvider : IStorageProvider { + private readonly ILogger _logger; + + public string TargetPath { get; } + + /// + /// Creates a new instance of . + /// + /// + public FileStorageProvider(string targetPath) { + new Logger(new LoggerFactory()).LogInformation("test"); + Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}"); + TargetPath = targetPath; + if(!Directory.Exists(targetPath)) { + Directory.CreateDirectory(targetPath); + } + } + + public async Task SaveObjectAsync(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson()); + + public async Task LoadObjectAsync(string key) => JsonSerializer.Deserialize(await File.ReadAllTextAsync(Path.Join(TargetPath, key))); + + public Task ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key))); + + public Task> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList()); + + public Task DeleteObjectAsync(string key) { + File.Delete(Path.Join(TargetPath, key)); + return Task.CompletedTask; + } +} 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/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs new file mode 100644 index 0000000..a8fce94 --- /dev/null +++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs @@ -0,0 +1,12 @@ +namespace MediaModeratorPoC.Bot.Interfaces; + +public interface ICommand { + public string Name { get; } + public string Description { get; } + + public Task CanInvoke(CommandContext ctx) { + return Task.FromResult(true); + } + + public Task Invoke(CommandContext ctx); +} \ No newline at end of file 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 @@ + + + + net7.0 + enable + enable + preview + + + + + + + + + + + + + + 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 _logger; + private readonly IEnumerable _commands; + private readonly LibMatrixBotConfiguration _config; + + private Task? _listenerTask; + + public CommandListenerHostedService(AuthenticatedHomeserverGeneric hs, ILogger logger, IServiceProvider services, + LibMatrixBotConfiguration config) { + logger.LogInformation("{} instantiated!", this.GetType().Name); + _hs = hs; + _logger = logger; + _config = config; + _logger.LogInformation("Getting commands..."); + _commands = services.GetServices(); + _logger.LogInformation("Got {} commands!", _commands.Count()); + } + + /// Triggered when the application host is ready to start the service. + /// Indicates that the start process has been aborted. + 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); + } + + /// Triggered when the application host is performing a graceful shutdown. + /// Indicates that the shutdown process should no longer be graceful. + public Task StopAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Shutting down command listener!"); + _listenerTask.Wait(cancellationToken); + return Task.CompletedTask; + } +} -- cgit 1.4.1