diff --git a/KfChatDotNetBot/Commands/ICommand.cs b/KfChatDotNetBot/Commands/ICommand.cs index d162f9e..2cf5e0a 100644 --- a/KfChatDotNetBot/Commands/ICommand.cs +++ b/KfChatDotNetBot/Commands/ICommand.cs @@ -7,9 +7,10 @@ namespace KfChatDotNetBot.Commands; internal interface ICommand { List Patterns { get; } - string HelpText { get; } - bool HideFromHelp { get; } + // Set to null to disable help for a given command + string? HelpText { get; } UserRight RequiredRight { get; } + TimeSpan Timeout { get; } Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx); } \ No newline at end of file diff --git a/KfChatDotNetBot/Commands/JuiceCommand.cs b/KfChatDotNetBot/Commands/JuiceCommand.cs index 43013d0..0ca5dcb 100644 --- a/KfChatDotNetBot/Commands/JuiceCommand.cs +++ b/KfChatDotNetBot/Commands/JuiceCommand.cs @@ -14,6 +14,7 @@ public class JuiceCommand : ICommand public string HelpText => "Get juice!"; public bool HideFromHelp => false; public UserRight RequiredRight => UserRight.Guest; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { await using var db = new ApplicationDbContext(); @@ -50,12 +51,13 @@ public class JuiceCommand : ICommand public class JuiceStatsCommand : ICommand { public List Patterns => [ - new Regex("^juice stats"), + new Regex("^juice stats$"), new Regex(@"^juice stats (?\d+)$") ]; public string HelpText => "Get juice stats!"; public bool HideFromHelp => false; public UserRight RequiredRight => UserRight.Guest; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { int top; diff --git a/KfChatDotNetBot/Commands/MemeCommands.cs b/KfChatDotNetBot/Commands/MemeCommands.cs index a6ca3af..c55642d 100644 --- a/KfChatDotNetBot/Commands/MemeCommands.cs +++ b/KfChatDotNetBot/Commands/MemeCommands.cs @@ -7,10 +7,9 @@ namespace KfChatDotNetBot.Commands; public class InsanityCommand : ICommand { public List Patterns => [new Regex("^insanity")]; - public string HelpText => "Insanity"; - public bool HideFromHelp => false; + public string? HelpText => "Insanity"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { // ReSharper disable once StringLiteralTypo @@ -21,10 +20,9 @@ public class InsanityCommand : ICommand public class TwistedCommand : ICommand { public List Patterns => [new Regex("^twisted")]; - public string HelpText => "Get it twisted"; - public bool HideFromHelp => false; + public string? HelpText => "Get it twisted"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { // ReSharper disable once StringLiteralTypo @@ -35,10 +33,9 @@ public class TwistedCommand : ICommand public class HelpMeCommand : ICommand { public List Patterns => [new Regex("^helpme")]; - public string HelpText => "Somebody please help me"; - public bool HideFromHelp => false; + public string? HelpText => "Somebody please help me"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { // ReSharper disable once StringLiteralTypo @@ -49,10 +46,9 @@ public class HelpMeCommand : ICommand public class SentCommand : ICommand { public List Patterns => [new Regex("^sent$")]; - public string HelpText => "Sent love"; - public bool HideFromHelp => false; + public string? HelpText => "Sent love"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { // ReSharper disable once StringLiteralTypo diff --git a/KfChatDotNetBot/Commands/RainbetCommands.cs b/KfChatDotNetBot/Commands/RainbetCommands.cs index a7b3626..acd611f 100644 --- a/KfChatDotNetBot/Commands/RainbetCommands.cs +++ b/KfChatDotNetBot/Commands/RainbetCommands.cs @@ -12,9 +12,9 @@ public class RainbetStatsCommand : ICommand public List Patterns => [ new Regex(@"^rainbet stats (?\d+)$") ]; - public string HelpText => "Get betting statistics in the given window"; - public bool HideFromHelp => false; + public string? HelpText => "Get betting statistics in the given window"; public UserRight RequiredRight => UserRight.Guest; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var window = Convert.ToInt32(arguments["window"].Value); @@ -28,7 +28,7 @@ public class RainbetStatsCommand : ICommand return; } var output = $"Rainbet stats for the last {window} hours (as seen on the bet feed):[br]" + - $"Bets: {bets.Count:N0}; Payout: ${bets.Sum(b => b.Payout):C}; Wagered: {bets.Sum(b => b.Value):C}"; + $"Bets: {bets.Count:N0}; Payout: {bets.Sum(b => b.Payout):C}; Wagered: {bets.Sum(b => b.Value):C}"; botInstance.SendChatMessage(output, true); } } @@ -38,9 +38,9 @@ public class RainbetRecentBetCommand : ICommand public List Patterns => [ new Regex(@"^rainbet recent$") ]; - public string HelpText => "Get the most recent 3 bets"; - public bool HideFromHelp => false; + public string? HelpText => "Get the most recent 3 bets"; public UserRight RequiredRight => UserRight.Guest; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var settings = await Helpers.GetMultipleValues([ diff --git a/KfChatDotNetBot/Commands/TestCommands.cs b/KfChatDotNetBot/Commands/TestCommands.cs index 9f829e8..3f561a5 100644 --- a/KfChatDotNetBot/Commands/TestCommands.cs +++ b/KfChatDotNetBot/Commands/TestCommands.cs @@ -13,10 +13,10 @@ public class EditTestCommand : ICommand new Regex("^test edit (?.+)") ]; - public string HelpText => "Test the editing functionality"; - public bool HideFromHelp => true; + public string? HelpText => null; public UserRight RequiredRight => UserRight.Admin; - + // Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes + public TimeSpan Timeout => TimeSpan.FromSeconds(60); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var logger = LogManager.GetCurrentClassLogger(); @@ -49,4 +49,36 @@ public class EditTestCommand : ICommand await Task.Delay(delay, ctx); botInstance.KfClient.DeleteMessage(status.ChatMessageId!.Value); } +} + +public class TimeoutTestCommand : ICommand +{ + public List Patterns => [ + new Regex("^test timeout$") + ]; + + public string? HelpText => null; + public UserRight RequiredRight => UserRight.Admin; + // Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes + public TimeSpan Timeout => TimeSpan.FromSeconds(15); + public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) + { + await Task.Delay(TimeSpan.FromMinutes(1), ctx); + } +} + +public class ExceptionTestCommand : ICommand +{ + public List Patterns => [ + new Regex("^test exception$") + ]; + + public string? HelpText => null; + public UserRight RequiredRight => UserRight.Admin; + // Increased timeout as it has to wait for Sneedchat to echo the message and that can be slow sometimes + public TimeSpan Timeout => TimeSpan.FromSeconds(15); + public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) + { + throw new Exception("Caused by the test exception command"); + } } \ No newline at end of file diff --git a/KfChatDotNetBot/Commands/TimeCommand.cs b/KfChatDotNetBot/Commands/TimeCommand.cs index 412baf4..c500753 100644 --- a/KfChatDotNetBot/Commands/TimeCommand.cs +++ b/KfChatDotNetBot/Commands/TimeCommand.cs @@ -7,10 +7,9 @@ namespace KfChatDotNetBot.Commands; public class TimeCommand : ICommand { public List Patterns => [new Regex("^time")]; - public string HelpText => "Get current time in BMT"; - public bool HideFromHelp => false; + public string? HelpText => "Get current time in BMT"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { var bmt = new DateTimeOffset(TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, diff --git a/KfChatDotNetBot/Commands/WhoisCommand.cs b/KfChatDotNetBot/Commands/WhoisCommand.cs index f7fb671..d1fbc6b 100644 --- a/KfChatDotNetBot/Commands/WhoisCommand.cs +++ b/KfChatDotNetBot/Commands/WhoisCommand.cs @@ -11,10 +11,9 @@ public class WhoisCommand : ICommand new Regex("^whois (?.+)") ]; - public string HelpText => "Lookup user IDs by username"; - public bool HideFromHelp => false; + public string? HelpText => "Lookup user IDs by username"; public UserRight RequiredRight => UserRight.Guest; - + public TimeSpan Timeout => TimeSpan.FromSeconds(10); public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) { await using var db = new ApplicationDbContext(); diff --git a/KfChatDotNetBot/Services/BotCommands.cs b/KfChatDotNetBot/Services/BotCommands.cs index de859f1..065e3ea 100644 --- a/KfChatDotNetBot/Services/BotCommands.cs +++ b/KfChatDotNetBot/Services/BotCommands.cs @@ -1,5 +1,7 @@ -using Humanizer; +using System.Text.RegularExpressions; +using Humanizer; using KfChatDotNetBot.Commands; +using KfChatDotNetBot.Models.DbModels; using KfChatDotNetWsClient.Models.Events; using NLog; @@ -14,7 +16,6 @@ internal class BotCommands private char CommandPrefix = '!'; private IEnumerable Commands; private CancellationToken _cancellationToken; - private List _commandTasks = []; internal BotCommands(ChatBot bot, CancellationToken? ctx = null) { @@ -63,28 +64,29 @@ internal class BotCommands _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); break; } - var task = Task.Run(() => command.RunCommand(_bot, message, user, match.Groups, _cancellationToken), _cancellationToken); - _commandTasks.Add(task); + _ = ProcessMessageAsync(command, message, user, match.Groups); + break; } } - - // Check on the state of the tasks, there's no way to know what error they produce if they failed otherwise - List removals = []; - foreach (var task in _commandTasks) - { - if (!task.IsCompleted) continue; - if (task.IsFaulted) - { - _logger.Error("Command task failed at some point"); - _logger.Error(task.Exception); - } + } - removals.Add(task); - } - // .NET doesn't support modifying a collection you're iterating over - foreach (var removal in removals) + 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 { - _commandTasks.Remove(removal); + 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); } } } \ No newline at end of file