about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-03-12 19:50:52 +0100
committerRory& <root@rory.gay>2025-03-12 19:51:23 +0100
commitcacabe2b1a15bb7492e23d477ec653513e84d260 (patch)
tree5b17d5f455ae6347de79eb73406bbf769b33d707
parentClean up stray console log from merge (diff)
downloadLibMatrix-unpacked-cacabe2b1a15bb7492e23d477ec653513e84d260.tar.xz
Add more bot stuff
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs89
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs84
2 files changed, 173 insertions, 0 deletions
diff --git a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs

index 8501d41..0d755a1 100644 --- a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs +++ b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs
@@ -0,0 +1,89 @@ +using ArcaneLibs; +using LibMatrix.Homeservers; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.AppServices; +using LibMatrix.Utilities.Bot.Interfaces; +using LibMatrix.Utilities.Bot.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace LibMatrix.Utilities.Bot; + +public static class BotCommandInstaller { + public static BotInstaller AddMatrixBot(this IServiceCollection services) { + return new BotInstaller(services).AddMatrixBot(); + } +} + +public class BotInstaller(IServiceCollection services) { + public BotInstaller AddMatrixBot() { + services.AddSingleton<LibMatrixBotConfiguration>(); + + services.AddSingleton<AuthenticatedHomeserverGeneric>(x => { + var config = x.GetService<LibMatrixBotConfiguration>() ?? throw new Exception("No configuration found!"); + var hsProvider = x.GetService<HomeserverProviderService>() ?? throw new Exception("No homeserver provider found!"); + + if (x.GetService<AppServiceConfiguration>() is AppServiceConfiguration appsvcConfig) + config.AccessToken = appsvcConfig.AppserviceToken; + else if (Environment.GetEnvironmentVariable("LIBMATRIX_ACCESS_TOKEN_PATH") is string path) + config.AccessTokenPath = path; + + if (string.IsNullOrWhiteSpace(config.AccessToken) && string.IsNullOrWhiteSpace(config.AccessTokenPath)) + throw new Exception("Unable to add bot service without an access token or access token path!"); + + if (!string.IsNullOrWhiteSpace(config.AccessTokenPath)) { + var token = File.ReadAllText(config.AccessTokenPath); + config.AccessToken = token; + } + + var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result; + + return hs; + }); + + return this; + } + + public BotInstaller AddCommandHandler() { + Console.WriteLine("Adding command handler..."); + services.AddHostedService<CommandListenerHostedService>(); + return this; + } + + public BotInstaller DiscoverAllCommands() { + foreach (var commandClass in ClassCollector<ICommand>.ResolveFromAllAccessibleAssemblies()) { + Console.WriteLine($"Adding command {commandClass.Name}"); + services.AddScoped(typeof(ICommand), commandClass); + } + + return this; + } + + public BotInstaller AddCommands(IEnumerable<Type> commandClasses) { + foreach (var commandClass in commandClasses) { + if (!commandClass.IsAssignableTo(typeof(ICommand))) + throw new Exception($"Type {commandClass.Name} is not assignable to ICommand!"); + Console.WriteLine($"Adding command {commandClass.Name}"); + services.AddScoped(typeof(ICommand), commandClass); + } + + return this; + } + + public BotInstaller WithInviteHandler(Func<InviteHandlerHostedService.InviteEventArgs, Task> inviteHandler) { + services.AddSingleton(inviteHandler); + services.AddHostedService<InviteHandlerHostedService>(); + return this; + } + + public BotInstaller WithInviteHandler<T>() where T : class, InviteHandlerHostedService.IInviteHandler { + services.AddSingleton<T>(); + services.AddSingleton<Func<InviteHandlerHostedService.InviteEventArgs, Task>>(sp => sp.GetRequiredService<T>().HandleInviteAsync); + services.AddHostedService<InviteHandlerHostedService>(); + return this; + } + + public BotInstaller WithCommandResultHandler(Func<CommandResult, Task> commandResultHandler) { + services.AddSingleton(commandResultHandler); + return this; + } +} \ No newline at end of file diff --git a/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs b/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs
index 88a6a03..cac9ca4 100644 --- a/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs +++ b/Utilities/LibMatrix.Utilities.Bot/Services/InviteListenerHostedService.cs
@@ -0,0 +1,84 @@ +using LibMatrix.Filters; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace LibMatrix.Utilities.Bot.Services; + +public class InviteHandlerHostedService : IHostedService { + private readonly AuthenticatedHomeserverGeneric _hs; + private readonly ILogger<InviteHandlerHostedService> _logger; + private readonly Func<InviteEventArgs, Task> _inviteHandler; + + private Task? _listenerTask; + + public InviteHandlerHostedService(AuthenticatedHomeserverGeneric hs, ILogger<InviteHandlerHostedService> logger, + Func<InviteEventArgs, Task> inviteHandler) { + logger.LogInformation("{} instantiated!", GetType().Name); + _hs = hs; + _logger = logger; + _inviteHandler = inviteHandler; + } + + /// <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 invite listener!"); + var filter = await _hs.NamedCaches.FilterCache.GetOrSetValueAsync("gay.rory.libmatrix.utilities.bot.command_listener_syncfilter.dev2", new SyncFilter() { + AccountData = new SyncFilter.EventFilter(notTypes: ["*"], limit: 1), + Presence = new SyncFilter.EventFilter(notTypes: ["*"]), + Room = new SyncFilter.RoomFilter() { + AccountData = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]), + Timeline = new SyncFilter.RoomFilter.StateFilter(types: ["m.room.message"], notSenders: [_hs.WhoAmI.UserId]), + } + }); + + var syncHelper = new SyncHelper(_hs, _logger) { + Timeout = 300_000, + FilterId = filter + }; + syncHelper.InviteReceivedHandlers.Add(async invite => { + _logger.LogInformation("Received invite to room {}", invite.Key); + + var inviteEventArgs = new InviteEventArgs() { + RoomId = invite.Key, + MemberEvent = invite.Value.InviteState.Events.First(x => x.Type == "m.room.member" && x.StateKey == _hs.WhoAmI.UserId), + Homeserver = _hs + }; + await _inviteHandler(inviteEventArgs); + }); + + await syncHelper.RunSyncLoopAsync(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 async Task StopAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Shutting down invite listener!"); + if (_listenerTask is null) { + _logger.LogError("Could not shut down invite listener task because it was null!"); + return; + } + + await _listenerTask.WaitAsync(cancellationToken); + } + + public class InviteEventArgs { + public string RoomId { get; set; } + public StateEventResponse MemberEvent { get; set; } + public AuthenticatedHomeserverGeneric Homeserver { get; set; } + } + + public interface IInviteHandler { + public Task HandleInviteAsync(InviteEventArgs args); + } +} \ No newline at end of file