using System.Text.RegularExpressions; using Humanizer; using KfChatDotNetBot.Commands; using KfChatDotNetBot.Models.DbModels; using KfChatDotNetWsClient.Models.Events; using NLog; namespace KfChatDotNetBot.Services; // Took one look at GambaSesh's code, and it made my head explode // This implementation is inspired by similar bot I wrote years ago internal class BotCommands { private ChatBot _bot; private Logger _logger = LogManager.GetCurrentClassLogger(); private char CommandPrefix = '!'; private IEnumerable Commands; private CancellationToken _cancellationToken; internal BotCommands(ChatBot bot, CancellationToken? ctx = null) { _cancellationToken = ctx ?? CancellationToken.None; _bot = bot; var interfaceType = typeof(ICommand); Commands = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetTypes()) .Where(x => interfaceType.IsAssignableFrom(x) && x is { IsInterface: false, IsAbstract: false }) .Select(Activator.CreateInstance).Cast(); foreach (var command in Commands) { _logger.Debug($"Found command {command.GetType().Name}"); } } internal void ProcessMessage(MessageModel message) { if (string.IsNullOrEmpty(message.MessageRaw)) { return; } if (!message.MessageRaw.StartsWith(CommandPrefix)) { return; } var messageTrimmed = message.MessageRaw.TrimStart(CommandPrefix); foreach (var command in Commands) { foreach (var regex in command.Patterns) { var match = regex.Match(messageTrimmed); if (!match.Success) continue; _logger.Debug($"Message matches {regex}"); using var db = new ApplicationDbContext(); var user = db.Users.FirstOrDefault(u => u.KfId == message.Author.Id); // This should never happen as brand-new users are created upon join if (user == null) return; if (user.Ignored) return; var continueAfterProcess = HasAttribute(command); if (user.UserRight < command.RequiredRight) { _bot.SendChatMessage($"@{message.Author.Username}, you do not have access to use this command. Your rank: {user.UserRight.Humanize()}; Required rank: {command.RequiredRight.Humanize()}", true); if (continueAfterProcess) continue; break; } _ = ProcessMessageAsync(command, message, user, match.Groups); if (!continueAfterProcess) break; } } } private async Task ProcessMessageAsync(ICommand command, MessageModel message, UserDbModel user, GroupCollection arguments) { var task = Task.Run(() => command.RunCommand(_bot, message, user, arguments, _cancellationToken), _cancellationToken); try { await task.WaitAsync(command.Timeout, _cancellationToken); } catch (Exception e) { _logger.Error("Caught an exception while waiting for the command to complete"); _logger.Error(e); return; } if (task.IsFaulted) { _logger.Error("Command task failed"); _logger.Error(task.Exception); } } private static bool HasAttribute(ICommand command) where T : Attribute { return Attribute.GetCustomAttribute(command.GetType(), typeof(T)) != null; } } /// /// Normally if a command is matched and executed, the loop breaks and no further commands are processed /// Use this attribute if you want to continue attempting to match and run other commands after this one /// Keep in mind since commands are executed in a throwaway task and not awaited, they will run concurrently /// [AttributeUsage(AttributeTargets.Class)] internal class AllowAdditionalMatches : Attribute;