diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3644285..db777a276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,13 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. ### Changed -- prune `--after` and `--safe` are now proper command options, and will show in .h help +- `.prune` options `--after` and `--safe` are now proper command options, and will show in .h help +- `.cmdcd` code mostly rewritten, slight QoL improvements. + +### Fixed + +- Fixed trivia bugs +- Fixed `.yun` not working with channels with underscore in the name ## [4.3.7] diff --git a/src/NadekoBot/Modules/Games/Trivia/Games.cs b/src/NadekoBot/Modules/Games/Trivia/Games.cs index d22e5e60b..9dbdb87ee 100644 --- a/src/NadekoBot/Modules/Games/Trivia/Games.cs +++ b/src/NadekoBot/Modules/Games/Trivia/Games.cs @@ -112,25 +112,25 @@ public partial class Games private void RegisterEvents(TriviaGame trivia) { - trivia.OnQuestion += OnTriviaOnOnQuestion; - trivia.OnHint += OnTriviaOnOnHint; - trivia.OnGuess += OnTriviaOnOnGuess; - trivia.OnEnded += OnTriviaOnOnEnded; - trivia.OnStats += OnTriviaOnOnStats; - trivia.OnTimeout += OnTriviaOnOnTimeout; + trivia.OnQuestion += OnTriviaQuestion; + trivia.OnHint += OnTriviaHint; + trivia.OnGuess += OnTriviaGuess; + trivia.OnEnded += OnTriviaEnded; + trivia.OnStats += OnTriviaStats; + trivia.OnTimeout += OnTriviaTimeout; } private void UnregisterEvents(TriviaGame trivia) { - trivia.OnQuestion -= OnTriviaOnOnQuestion; - trivia.OnHint -= OnTriviaOnOnHint; - trivia.OnGuess -= OnTriviaOnOnGuess; - trivia.OnEnded -= OnTriviaOnOnEnded; - trivia.OnStats -= OnTriviaOnOnStats; - trivia.OnTimeout -= OnTriviaOnOnTimeout; + trivia.OnQuestion -= OnTriviaQuestion; + trivia.OnHint -= OnTriviaHint; + trivia.OnGuess -= OnTriviaGuess; + trivia.OnEnded -= OnTriviaEnded; + trivia.OnStats -= OnTriviaStats; + trivia.OnTimeout -= OnTriviaTimeout; } - private async Task OnTriviaOnOnHint(TriviaGame game, TriviaQuestion question) + private async Task OnTriviaHint(TriviaGame game, TriviaQuestion question) { try { @@ -150,11 +150,11 @@ public partial class Games } catch (Exception ex) { - Log.Warning(ex, "Error editing triva message"); + Log.Warning(ex, "Error editing trivia message"); } } - private async Task OnTriviaOnOnQuestion(TriviaGame game, TriviaQuestion question) + private async Task OnTriviaQuestion(TriviaGame game, TriviaQuestion question) { try { @@ -173,19 +173,16 @@ public partial class Games questionMessage = await ctx.Channel.EmbedAsync(questionEmbed); } - catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden or HttpStatusCode.BadRequest) + catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden + or HttpStatusCode.BadRequest) { Log.Warning("Unable to send trivia questions. Stopping immediately"); game.Stop(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error sending trivia embed"); - await Task.Delay(2000); + throw; } } - private async Task OnTriviaOnOnTimeout(TriviaGame _, TriviaQuestion question) + private async Task OnTriviaTimeout(TriviaGame _, TriviaQuestion question) { try { @@ -205,7 +202,7 @@ public partial class Games } } - private async Task OnTriviaOnOnStats(TriviaGame game) + private async Task OnTriviaStats(TriviaGame game) { try { @@ -217,7 +214,7 @@ public partial class Games } } - private async Task OnTriviaOnOnEnded(TriviaGame game) + private async Task OnTriviaEnded(TriviaGame game) { try { @@ -237,10 +234,9 @@ public partial class Games } UnregisterEvents(game); - await Task.Delay(1000); } - private async Task OnTriviaOnOnGuess(TriviaGame _, TriviaUser user, TriviaQuestion question, bool isWin) + private async Task OnTriviaGuess(TriviaGame _, TriviaUser user, TriviaQuestion question, bool isWin) { try { diff --git a/src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs index 46023e4ab..da0eed93d 100644 --- a/src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Trivia/TriviaGame.cs @@ -1,4 +1,5 @@ using System.Threading.Channels; +using Exception = System.Exception; namespace NadekoBot.Modules.Games.Common.Trivia; @@ -64,7 +65,6 @@ public sealed class TriviaGame if (errorCount >= 5) { Log.Warning("Trivia errored 5 times and will quit"); - await OnEnded(this); break; } @@ -80,7 +80,7 @@ public sealed class TriviaGame var maybeQuestion = await _questionPool.GetQuestionAsync(); - if (!(maybeQuestion is TriviaQuestion question)) + if (maybeQuestion is not { } question) { // if question is null (ran out of question, or other bugg ) - stop break; @@ -110,7 +110,8 @@ public sealed class TriviaGame var guessed = false; while (true) { - var readTask = _inputs.Reader.ReadAsync().AsTask(); + using var readCancel = new CancellationTokenSource(); + var readTask = _inputs.Reader.ReadAsync(readCancel.Token).AsTask(); // wait for either someone to attempt to guess // or for timeout @@ -119,6 +120,8 @@ public sealed class TriviaGame // if the task which completed is the timeout task if (task == halfGuessTimerTask) { + readCancel.Cancel(); + // if hint is already sent, means time expired // break (end the round) if (hintSent) @@ -130,7 +133,7 @@ public sealed class TriviaGame halfGuessTimerTask = TimeOutFactory(); // send a hint out await OnHint(this, question); - + continue; } @@ -147,6 +150,7 @@ public sealed class TriviaGame // reset inactivity counter inactivity = 0; + errorCount = 0; var isWin = false; // if user won the game, tell the game to stop @@ -174,9 +178,9 @@ public sealed class TriviaGame } } } - catch + catch (Exception ex) { - + Log.Error(ex, "Fatal error in trivia game: {ErrorMessage}", ex.Message); } finally { diff --git a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdService.cs b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdService.cs index 76371d9e8..4c42484ed 100644 --- a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdService.cs +++ b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdService.cs @@ -1,68 +1,119 @@ #nullable disable using NadekoBot.Common.ModuleBehaviors; -using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Permissions.Services; -public class CmdCdService : IExecPreCommand, INService +public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService { - public ConcurrentDictionary> CommandCooldowns { get; } - public ConcurrentDictionary> ActiveCooldowns { get; } = new(); + private readonly ConcurrentDictionary> _settings = new(); - public int Priority { get; } = 0; + private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary> _activeCooldowns = + new(); + + public int Priority => 0; public CmdCdService(Bot bot) - => CommandCooldowns = new(bot.AllGuildConfigs.ToDictionary(k => k.GuildId, - v => new ConcurrentHashSet(v.CommandCooldowns))); - - public Task TryBlock(IGuild guild, IUser user, string commandName) { - if (guild is null) - return Task.FromResult(false); + _settings = bot + .AllGuildConfigs + .ToDictionary(x => x.GuildId, x => x.CommandCooldowns + .ToDictionary(c => c.CommandName, c => c.Seconds) + .ToConcurrent()) + .ToConcurrent(); + } - var cmdcds = CommandCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); - CommandCooldown cdRule; - if ((cdRule = cmdcds.FirstOrDefault(cc => cc.CommandName == commandName)) is not null) + public Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command) + => TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant()); + + public async Task TryBlock(IGuild guild, IUser user, string commandName) + { + if (!_settings.TryGetValue(guild.Id, out var cooldownSettings)) + return false; + + if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds)) + return false; + + var cooldowns = _activeCooldowns.GetOrAdd( + (guild.Id, commandName), + static _ => new()); + + // if user is not already on cooldown, add + if (cooldowns.TryAdd(user.Id, DateTime.UtcNow)) { - var activeCdsForGuild = ActiveCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); - if (activeCdsForGuild.FirstOrDefault(ac => ac.UserId == user.Id && ac.Command == commandName) is not null) - return Task.FromResult(true); - - activeCdsForGuild.Add(new() - { - UserId = user.Id, - Command = commandName - }); - - _ = Task.Run(async () => - { - try - { - await Task.Delay(cdRule.Seconds * 1000); - activeCdsForGuild.RemoveWhere(ac => ac.Command == commandName && ac.UserId == user.Id); - } - catch - { - // ignored - } - }); + return false; } - return Task.FromResult(false); + // if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did + // - just update + if (cooldowns.TryGetValue(user.Id, out var oldValue)) + { + var diff = DateTime.UtcNow - oldValue; + if (diff.Seconds > cdSeconds) + { + if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue)) + return false; + } + } + + return true; } - public Task ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command) + public async Task OnReadyAsync() { - var guild = ctx.Guild; - var user = ctx.User; - var commandName = command.Name.ToLowerInvariant(); + using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); - return TryBlock(guild, user, commandName); + while (await timer.WaitForNextTickAsync()) + { + var now = DateTime.UtcNow; + // once per hour delete expired entries + foreach (var ((guildId, commandName), dict) in _activeCooldowns) + { + // if this pair no longer has associated config, that means it has been removed. + // remove all cooldowns + if (!_settings.TryGetValue(guildId, out var inner) + || !inner.TryGetValue(commandName, out var cdSeconds)) + { + _activeCooldowns.Remove((guildId, commandName), out _); + continue; + } + + Cleanup(dict, cdSeconds); + } + } } -} -public class ActiveCooldown -{ - public string Command { get; set; } - public ulong UserId { get; set; } + private void Cleanup(ConcurrentDictionary dict, int cdSeconds) + { + var now = DateTime.UtcNow; + foreach (var (key, _) in dict.Where(x => (now - x.Value).Seconds > cdSeconds).ToArray()) + { + dict.TryRemove(key, out _); + } + } + + public void ClearCooldowns(ulong guildId, string cmdName) + { + if (_settings.TryGetValue(guildId, out var dict)) + dict.TryRemove(cmdName, out _); + + _activeCooldowns.TryRemove((guildId, cmdName), out _); + } + + public void AddCooldown(ulong guildId, string name, int secs) + { + var sett = _settings.GetOrAdd(guildId, static _ => new()); + sett[name] = secs; + + // force cleanup + if (_activeCooldowns.TryGetValue((guildId, name), out var dict)) + Cleanup(dict, secs); + } + + public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId) + { + if (!_settings.TryGetValue(guildId, out var dict)) + return Array.Empty<(string, int)>(); + + return dict.Select(x => (x.Key, x.Value)).ToArray(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs index aecab2208..b90c1dbf9 100644 --- a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs @@ -1,4 +1,5 @@ #nullable disable +using Humanizer.Localisation; using Microsoft.EntityFrameworkCore; using NadekoBot.Common.TypeReaders; using NadekoBot.Db; @@ -12,12 +13,6 @@ public partial class Permissions [Group] public partial class CmdCdsCommands : NadekoModule { - private ConcurrentDictionary> CommandCooldowns - => _service.CommandCooldowns; - - private ConcurrentDictionary> ActiveCooldowns - => _service.ActiveCooldowns; - private readonly DbService _db; private readonly CmdCdService _service; @@ -40,12 +35,10 @@ public partial class Permissions await using (var uow = _db.GetDbContext()) { var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns)); - var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name); if (toDelete is not null) uow.Set().Remove(toDelete); - localSet.RemoveWhere(cc => cc.CommandName == name); if (secs != 0) { var cc = new CommandCooldown @@ -54,7 +47,7 @@ public partial class Permissions Seconds = secs }; config.CommandCooldowns.Add(cc); - localSet.Add(cc); + _service.AddCooldown(channel.Guild.Id, name, secs); } await uow.SaveChangesAsync(); @@ -62,8 +55,7 @@ public partial class Permissions if (secs == 0) { - var activeCds = ActiveCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - activeCds.RemoveWhere(ac => ac.Command == name); + _service.ClearCooldowns(ctx.Guild.Id, cmdName); await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name))); } else @@ -84,19 +76,29 @@ public partial class Permissions [Cmd] [RequireContext(ContextType.Guild)] - public async Task AllCmdCooldowns() + public async Task AllCmdCooldowns(int page = 1) { + if (--page < 0) + return; + var channel = (ITextChannel)ctx.Channel; - var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); + var localSet = _service.GetCommandCooldowns(ctx.Guild.Id); if (!localSet.Any()) await ReplyConfirmLocalizedAsync(strs.cmdcd_none); else { - await channel.SendTableAsync("", - localSet.Select(c => c.CommandName + ": " + c.Seconds + GetText(strs.sec)), - s => $"{s,-30}", - 2); + await ctx.SendPaginatedConfirmAsync(page, curPage => + { + var items = localSet.Skip(curPage * 15) + .Take(15) + .Select(x => $"{Format.Code(x.CommandName)}: {x.Seconds.Seconds().Humanize(maxUnit: TimeUnit.Second, culture: Culture)}"); + + return _eb.Create(ctx) + .WithOkColor() + .WithDescription(items.Join("\n")); + + }, localSet.Count, 15); } } } diff --git a/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs b/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs index 0692bbaee..cda3fd3cd 100644 --- a/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs +++ b/src/NadekoBot/Modules/Searches/Feeds/FeedCommands.cs @@ -11,7 +11,7 @@ public partial class Searches public partial class FeedCommands : NadekoModule { private static readonly Regex _ytChannelRegex = - new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?[a-zA-Z0-9\-]{1,})"); + new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?[a-zA-Z0-9\-_]{1,})"); [Cmd] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs b/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs index ef752f2a1..57330566d 100644 --- a/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs +++ b/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs @@ -286,8 +286,7 @@ public sealed class PatronageService } } - public async Task ExecPreCommandAsync( - ICommandContext ctx, + public async Task ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command) { diff --git a/src/NadekoBot/Services/CommandHandler.cs b/src/NadekoBot/Services/CommandHandler.cs index 2054b7ee4..3dc7cf26c 100644 --- a/src/NadekoBot/Services/CommandHandler.cs +++ b/src/NadekoBot/Services/CommandHandler.cs @@ -2,8 +2,6 @@ using NadekoBot.Common.Configs; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db; -using System.Collections.Immutable; -using Nadeko.Common; using ExecuteResult = Discord.Commands.ExecuteResult; using PreconditionResult = Discord.Commands.PreconditionResult; @@ -219,7 +217,7 @@ public class CommandHandler : INService, IReadyExecutor try { #if !GLOBAL_NADEKO - // track how many messagges each user is sending + // track how many messages each user is sending UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old); #endif @@ -254,7 +252,7 @@ public class CommandHandler : INService, IReadyExecutor var prefix = GetPrefix(guild?.Id); var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase); // execute the command and measure the time it took - if (messageContent.StartsWith(prefix, StringComparison.InvariantCulture) || isPrefixCommand) + if (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture)) { var context = new CommandContext(_client, usrMsg); var (success, error, info) = await ExecuteCommandAsync(context, @@ -262,6 +260,7 @@ public class CommandHandler : INService, IReadyExecutor isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best); + startTime = Environment.TickCount - startTime; // if a command is found @@ -348,11 +347,10 @@ public class CommandHandler : INService, IReadyExecutor switch (multiMatchHandling) { case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()) - .ToImmutableArray(); + argList = parseResult.ArgValues + .Map(x => x.Values.MaxBy(y => y.Score)); paramList = parseResult.ParamValues - .Select(x => x.Values.OrderByDescending(y => y.Score).First()) - .ToImmutableArray(); + .Map(x => x.Values.MaxBy(y => y.Score)); parseResult = ParseResult.FromSuccess(argList, paramList); break; } diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index f487a671d..b5da215d8 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -7,7 +7,7 @@ namespace NadekoBot.Services; public sealed class StatsService : IStatsService, IReadyExecutor, INService { - public const string BOT_VERSION = "4.3.7"; + public const string BOT_VERSION = "4.3.8"; public string Author => "Kwoth#2452";