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
|