From e01fde83c63a0eab0ae972b689395cbca4982d50 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:27:00 +0300 Subject: [PATCH] Use custom TimeSpanParser (#223) Closes #154 --------- Signed-off-by: mctaylors --- locale/Messages.resx | 3 ++ locale/Messages.ru.resx | 3 ++ locale/Messages.tt-ru.resx | 3 ++ src/Commands/BanCommandGroup.cs | 23 ++++++++- src/Commands/MuteCommandGroup.cs | 17 ++++++- src/Commands/RemindCommandGroup.cs | 29 +++++++++-- src/Commands/ToolsCommandGroup.cs | 29 +++++++++-- src/Data/Options/TimeSpanOption.cs | 6 +-- src/Messages.Designer.cs | 8 +++ src/Parsers/TimeSpanParser.cs | 78 ++++++++++++++++++++++++++++++ 10 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 src/Parsers/TimeSpanParser.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 1387edf..adc9f6d 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -585,6 +585,9 @@ Report an issue + + Time specified incorrectly! + Kicked diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 572c0b2..de2158d 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -585,6 +585,9 @@ Сообщить о проблеме + + Неправильно указано время! + Выгнан diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index d20c358..ca3c19d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -585,6 +585,9 @@ зарепортить баг + + ты там правильно напиши таймспан + кикнут diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index bbcf459..e72a43c 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -53,7 +54,7 @@ public class BanCommandGroup : CommandGroup /// A slash command that bans a Discord user with the specified reason. /// /// The user to ban. - /// The duration for this ban. The user will be automatically unbanned after this duration. + /// The duration for this ban. The user will be automatically unbanned after this duration. /// /// The reason for this ban. Must be encoded with when passed to /// . @@ -75,7 +76,8 @@ public class BanCommandGroup : CommandGroup [Description("User to ban")] IUser target, [Description("Ban reason")] [MaxLength(256)] string reason, - [Description("Ban duration")] TimeSpan? duration = null) + [Description("Ban duration")] [Option("duration")] + string? stringDuration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,6 +106,23 @@ public class BanCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + if (stringDuration is null) + { + return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot, + CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(stringDuration); + if (!parseResult.IsDefined(out var duration)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken); } diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c7b21f6..0156f82 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Octobot.Services.Update; using Remora.Commands.Attributes; @@ -50,7 +51,7 @@ public class MuteCommandGroup : CommandGroup /// A slash command that mutes a Discord member with the specified reason. /// /// The member to mute. - /// The duration for this mute. The member will be automatically unmuted after this duration. + /// The duration for this mute. The member will be automatically unmuted after this duration. /// /// The reason for this mute. Must be encoded with when passed to /// . @@ -72,7 +73,8 @@ public class MuteCommandGroup : CommandGroup [Description("Member to mute")] IUser target, [Description("Mute reason")] [MaxLength(256)] string reason, - [Description("Mute duration")] TimeSpan duration) + [Description("Mute duration")] [Option("duration")] + string stringDuration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,6 +106,17 @@ public class MuteCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken); } + var parseResult = TimeSpanParser.TryParse(stringDuration); + if (!parseResult.IsDefined(out var duration)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); } diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 67e7910..5e8c9c5 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -17,6 +17,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using Octobot.Parsers; namespace Octobot.Commands; @@ -110,7 +111,7 @@ public class RemindCommandGroup : CommandGroup /// /// A slash command that schedules a reminder with the specified text. /// - /// The period of time which must pass before the reminder will be sent. + /// The period of time which must pass before the reminder will be sent. /// The text of the reminder. /// A feedback sending result which may or may not have succeeded. [Command("remind")] @@ -120,7 +121,8 @@ public class RemindCommandGroup : CommandGroup [UsedImplicitly] public async Task ExecuteReminderAsync( [Description("After what period of time mention the reminder")] - TimeSpan @in, + [Option("in")] + string timeSpanString, [Description("Reminder text")] [MaxLength(512)] string text) { @@ -129,6 +131,12 @@ public class RemindCommandGroup : CommandGroup return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { @@ -138,14 +146,25 @@ public class RemindCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken); + var parseResult = TimeSpanParser.TryParse(timeSpanString); + if (!parseResult.IsDefined(out var timeSpan)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + + return await AddReminderAsync(timeSpan, text, data, channelId, executor, CancellationToken); } - private async Task AddReminderAsync(TimeSpan @in, string text, GuildData data, + private async Task AddReminderAsync(TimeSpan timeSpan, string text, GuildData data, Snowflake channelId, IUser executor, CancellationToken ct = default) { var memberData = data.GetOrCreateMemberData(executor.ID); - var remindAt = DateTimeOffset.UtcNow.Add(@in); + var remindAt = DateTimeOffset.UtcNow.Add(timeSpan); var responseResult = await _interactionApi.GetOriginalInteractionResponseAsync(_context.Interaction.ApplicationID, _context.Interaction.Token, ct); if (!responseResult.IsDefined(out var response)) { diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 1dbf72d..b539355 100644 --- a/src/Commands/ToolsCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; +using Octobot.Parsers; using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -418,7 +419,7 @@ public class ToolsCommandGroup : CommandGroup /// /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. /// - /// The offset for the current timestamp. + /// The offset for the current timestamp. /// /// A feedback sending result which may or may not have succeeded. /// @@ -427,14 +428,20 @@ public class ToolsCommandGroup : CommandGroup [Description("Shows a timestamp in all styles")] [UsedImplicitly] public async Task ExecuteTimestampAsync( - [Description("Offset from current time")] - TimeSpan? offset = null) + [Description("Offset from current time")] [Option("offset")] + string? stringOffset = null) { if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); } + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); if (!executorResult.IsDefined(out var executor)) { @@ -444,6 +451,22 @@ public class ToolsCommandGroup : CommandGroup var data = await _guildData.GetData(guildId, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + if (stringOffset is null) + { + return await SendTimestampAsync(null, executor, CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(stringOffset); + if (!parseResult.IsDefined(out var offset)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await SendTimestampAsync(offset, executor, CancellationToken); } diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 7f60ebb..b9b405f 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -1,13 +1,11 @@ using System.Text.Json.Nodes; -using Remora.Commands.Parsers; +using Octobot.Parsers; using Remora.Results; namespace Octobot.Data.Options; public sealed class TimeSpanOption : Option { - private static readonly TimeSpanParser Parser = new(); - public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } public override TimeSpan Get(JsonNode settings) @@ -29,6 +27,6 @@ public sealed class TimeSpanOption : Option private static Result ParseTimeSpan(string from) { - return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult(); + return TimeSpanParser.TryParse(from); } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 8dad3dc..f5a06c0 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1037,6 +1037,14 @@ namespace Octobot { } } + internal static string InvalidTimeSpan + { + get + { + return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); + } + } + internal static string UserInfoKicked { get diff --git a/src/Parsers/TimeSpanParser.cs b/src/Parsers/TimeSpanParser.cs new file mode 100644 index 0000000..1f44d46 --- /dev/null +++ b/src/Parsers/TimeSpanParser.cs @@ -0,0 +1,78 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Remora.Commands.Parsers; +using Remora.Results; + +namespace Octobot.Parsers; + +/// +/// Parses s. +/// +[PublicAPI] +public partial class TimeSpanParser : AbstractTypeParser +{ + private static readonly Regex Pattern = ParseRegex(); + + /// + /// Parses a from the . + /// + /// + /// The parsed , or if parsing failed. + /// + public static Result TryParse(string timeSpanString) + { + if (timeSpanString.StartsWith('-')) + { + return new ArgumentInvalidError(nameof(timeSpanString), "TimeSpans cannot be negative."); + } + + if (TimeSpan.TryParse(timeSpanString, DateTimeFormatInfo.InvariantInfo, out var parsedTimeSpan)) + { + return parsedTimeSpan; + } + + var matches = ParseRegex().Matches(timeSpanString); + return matches.Count > 0 + ? ParseFromRegex(matches) + : new ArgumentInvalidError(nameof(timeSpanString), "The regex did not produce any matches."); + } + + private static Result ParseFromRegex(MatchCollection matches) + { + var timeSpan = TimeSpan.Zero; + + foreach (var groups in matches.Select(match => match.Groups + .Cast() + .Where(g => g.Success) + .Skip(1) + .Select(g => (g.Name, g.Value)))) + { + foreach ((var key, var groupValue) in groups) + { + if (!int.TryParse(groupValue, out var parsedIntegerValue)) + { + return new ArgumentInvalidError(nameof(groupValue), "The input value was not an integer."); + } + + var now = DateTimeOffset.UtcNow; + timeSpan += key switch + { + "Years" => now.AddYears(parsedIntegerValue) - now, + "Months" => now.AddMonths(parsedIntegerValue) - now, + "Weeks" => TimeSpan.FromDays(parsedIntegerValue * 7), + "Days" => TimeSpan.FromDays(parsedIntegerValue), + "Hours" => TimeSpan.FromHours(parsedIntegerValue), + "Minutes" => TimeSpan.FromMinutes(parsedIntegerValue), + "Seconds" => TimeSpan.FromSeconds(parsedIntegerValue), + _ => throw new ArgumentOutOfRangeException(key) + }; + } + } + + return timeSpan; + } + + [GeneratedRegex("(?\\d+(?=y|л|г))|(?\\d+(?=mo|мес))|(?\\d+(?=w|н|нед))|(?\\d+(?=d|д|дн))|(?\\d+(?=h|ч))|(?\\d+(?=m|min|мин|м))|(?\\d+(?=s|sec|с|сек))")] + private static partial Regex ParseRegex(); +}