diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 643492d..93a190c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -* @LabsDevelopment/octobot -/docs/ @LabsDevelopment/octobot-docs +* @TeamOctolings/octobot +/docs/ @TeamOctolings/octobot-docs diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index b697dac..8002f6f 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.1 + uses: muno92/resharper_inspectcode@1.11.7 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 0a1ec81..bf444a9 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -18,3 +18,5 @@ P:System.DateTime.Now;Use System.DateTime.UtcNow instead. P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead. P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead. M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead. +M:System.Threading.Thread.Sleep(System.Int32);Use Task.Delay(int, CancellationToken) instead. +M:System.Threading.Thread.Sleep(System.TimeSpan);Use Task.Delay(TimeSpan, CancellationToken) instead. diff --git a/Octobot.csproj b/Octobot.csproj index e8f0dfa..ab76400 100644 --- a/Octobot.csproj +++ b/Octobot.csproj @@ -9,11 +9,11 @@ Octobot Octol1ttle, mctaylors, neroduckale AGPLv3 - https://github.com/LabsDevelopment/Octobot - https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE - https://github.com/LabsDevelopment/Octobot + https://github.com/TeamOctolings/Octobot + https://github.com/TeamOctolings/Octobot/blob/master/LICENSE + https://github.com/TeamOctolings/Octobot github - LabsDevelopment + TeamOctolings en A general-purpose Discord bot for moderation written in C# docs/octobot.ico @@ -26,10 +26,10 @@ - - - - + + + + diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2a15ef2..dc5a793 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,7 +29,7 @@ While pull requests from unaffiliated contributors are welcome, please note that internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. -The [issue tracker](https://github.com/LabsDevelopment/Octobot/issues) should provide plenty of issues to start with. +The [issue tracker](https://github.com/TeamOctolings/Octobot/issues) should provide plenty of issues to start with. Make sure to check that an issue you're planning to resolve does not already have people working on it and that there are no PRs associated with it diff --git a/docs/README.md b/docs/README.md index 5be0bd8..7056857 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,12 +1,12 @@

- Octobot banner + Octobot banner

- + - + -Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord +Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Team Octolings](https://github.com/TeamOctolings) in C# and Remora.Discord ## Features @@ -19,25 +19,13 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr *...a-a-and more!* -[//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki) - -## Invite Octobot - -Did you know that Octobot is a public bot? You can invite it to your server and use it without building it! -

- -

- -> [!IMPORTANT] -> The bot will not be able to respond in private channels unless you have configured permissions for the bot in those channels. - ## Building Octobot 1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! 3. Clone this repository and open `Octobot` folder. ``` -git clone https://github.com/LabsDevelopment/Octobot +git clone https://github.com/TeamOctolings/Octobot cd Octobot ``` 4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. diff --git a/docs/octobot-banner.png b/docs/octobot-banner.png new file mode 100644 index 0000000..2ab5f5b Binary files /dev/null and b/docs/octobot-banner.png differ diff --git a/locale/Messages.resx b/locale/Messages.resx index e49a3e3..02c7230 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -591,7 +591,79 @@ Leave message + + Time specified incorrectly! + Kicked + + Reminder edited + + + It is certain + + + It is decidedly so + + + Without a doubt + + + Yes — definitely + + + You may rely on it + + + As I see it, yes + + + Most likely + + + Outlook good + + + Signs point to yes + + + Yes + + + Reply hazy, try again + + + Ask again later + + + Better not tell you now + + + Cannot predict now + + + Concentrate and ask again + + + Don’t count on it + + + My reply is no + + + My sources say no + + + Outlook not so good + + + Very doubtful + + + Example of a valid input: `1h30m` + + + Welcome messages channel + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 35ea613..87a7bc8 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -591,7 +591,79 @@ Сообщение о выходе + + Неправильно указано время! + Выгнан + + Напоминание отредактировано + + + Бесспорно + + + Предрешено + + + Никаких сомнений + + + Определённо да + + + Можешь быть уверен в этом + + + Мне кажется — «да» + + + Вероятнее всего + + + Хорошие перспективы + + + Знаки говорят — «да» + + + Да + + + Пока не ясно, попробуй снова + + + Спроси позже + + + Лучше не рассказывать + + + Сейчас нельзя предсказать + + + Сконцентрируйся и спроси снова + + + Даже не думай + + + Мой ответ — «нет» + + + По моим данным — «нет» + + + Перспективы не очень хорошие + + + Весьма сомнительно + + + Пример правильного ввода: `1ч30м` + + + Канал для приветствий + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 48196c6..722a34d 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -591,7 +591,79 @@ до свидания (типо настройка) + + ты там правильно напиши таймспан + кикнут + + напоминалка подправлена + + + абсолютли + + + заявлено + + + ваще не сомневайся + + + 100% да + + + будь в этом уверен + + + я считаю что да + + + ну вполне вероятно + + + ну выглядит нормально + + + мне сказали ок + + + мгм + + + ну-ка попробуй снова + + + давай позже + + + щас пока не скажу + + + я не могу сейчас предсказать + + + ну сконцентрируйся и давай еще раз + + + даже не думай + + + мое завление это нет + + + я тут посчитал, короче нет + + + выглядит такое себе + + + чот сомневаюсь + + + правильно пишут так: `1h30m` + + + канал куда говорить здравствуйте + diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 4c396d9..e978ec9 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -100,7 +100,7 @@ public class AboutCommandGroup : CommandGroup .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png") + .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") .Build(); var repositoryButton = new ButtonComponent( diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index bbcf459..c350729 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; @@ -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 (e.g. 1h30m)")] + string? duration = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,7 +106,25 @@ public class BanCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken); + if (duration is null) + { + return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot, + CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(duration); + if (!parseResult.IsDefined(out var timeSpan)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + + return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken); } private async Task BanUserAsync( @@ -178,12 +198,8 @@ public class BanCommandGroup : CommandGroup title, target) .WithColour(ColorsList.Green).Build(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } @@ -269,12 +285,9 @@ public class BanCommandGroup : CommandGroup var title = string.Format(Messages.UserUnbanned, target.GetTag()); var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index 1d0ad64..395810f 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -136,12 +136,8 @@ public class ClearCommandGroup : CommandGroup return Result.FromError(deleteResult.Error); } - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithColour(ColorsList.Green).Build(); diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index a278fb4..0faa1d3 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -157,12 +157,9 @@ public class KickCommandGroup : CommandGroup var title = string.Format(Messages.UserKicked, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserKicked, target.GetTag()), target) diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c7b21f6..c2542e8 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 (e.g. 1h30m)")] [Option("duration")] + string stringDuration) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -104,6 +106,18 @@ 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) + .WithDescription(Messages.TimeSpanExample) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); } @@ -140,12 +154,8 @@ public class MuteCommandGroup : CommandGroup .AppendBulletPoint(string.Format( Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserMuted, target.GetTag()), target) @@ -326,12 +336,9 @@ public class MuteCommandGroup : CommandGroup var title = string.Format(Messages.UserUnmuted, target.GetTag()); var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)); - var logResult = _utility.LogActionAsync( + + _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.UserUnmuted, target.GetTag()), target) diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 67e7910..f9c006e 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")] @@ -119,8 +120,9 @@ public class RemindCommandGroup : CommandGroup [RequireContext(ChannelContext.Guild)] [UsedImplicitly] public async Task ExecuteReminderAsync( - [Description("After what period of time mention the reminder")] - TimeSpan @in, + [Description("After what period of time mention the reminder (e.g. 1h30m)")] + [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,26 @@ 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) + .WithDescription(Messages.TimeSpanExample) + .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)) { @@ -174,6 +194,133 @@ public class RemindCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + public enum Parameters + { + [UsedImplicitly] Time, + [UsedImplicitly] Text + } + + /// + /// A slash command that edits a scheduled reminder using the specified text or time. + /// + /// The list position of the reminder to edit. + /// The reminder's parameter to edit. + /// The new value for the reminder as a text or time. + /// A feedback sending result which may or may not have succeeded. + [Command("editremind")] + [Description("Edit a reminder")] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [UsedImplicitly] + public async Task ExecuteEditReminderAsync( + [Description("Position in list")] [MinValue(1)] + int position, + [Description("Parameter to edit")] Parameters parameter, + [Description("Parameter's new value")] string value) + { + 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)) + { + return Result.FromError(executorResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + var memberData = data.GetOrCreateMemberData(executor.ID); + + if (parameter is Parameters.Time) + { + return await EditReminderTimeAsync(position - 1, value, memberData, bot, executor, CancellationToken); + } + + return await EditReminderTextAsync(position - 1, value, memberData, bot, executor, CancellationToken); + } + + private async Task EditReminderTimeAsync(int index, string value, MemberData data, + IUser bot, IUser executor, CancellationToken ct = default) + { + if (index >= data.Reminders.Count) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var parseResult = TimeSpanParser.TryParse(value); + if (!parseResult.IsDefined(out var timeSpan)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var oldReminder = data.Reminders[index]; + var remindAt = DateTimeOffset.UtcNow.Add(timeSpan); + + data.Reminders.Add(oldReminder with { At = remindAt }); + data.Reminders.RemoveAt(index); + + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text))) + .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderEdited, executor.GetTag()), executor) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count)) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private async Task EditReminderTextAsync(int index, string value, MemberData data, + IUser bot, IUser executor, CancellationToken ct = default) + { + if (index >= data.Reminders.Count) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var oldReminder = data.Reminders[index]; + + data.Reminders.Add(oldReminder with { Text = value }); + data.Reminders.RemoveAt(index); + + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value))) + .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At))); + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.ReminderEdited, executor.GetTag()), executor) + .WithDescription(builder.ToString()) + .WithColour(ColorsList.Cyan) + .WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count)) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + /// /// A slash command that deletes a reminder using its list position. /// diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 7d4a72e..97ebc32 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -47,6 +47,7 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.RenameHoistedUsers, GuildSettings.PublicFeedbackChannel, GuildSettings.PrivateFeedbackChannel, + GuildSettings.WelcomeMessagesChannel, GuildSettings.EventNotificationChannel, GuildSettings.DefaultRole, GuildSettings.MuteRole, @@ -219,12 +220,8 @@ public class SettingsCommandGroup : CommandGroup var title = Messages.SettingSuccessfullyChanged; var description = builder.ToString(); - var logResult = _utility.LogActionAsync( + _utility.LogAction( data.Settings, channelId, executor, title, description, bot, ColorsList.Magenta, false, ct); - if (!logResult.IsSuccess) - { - return Result.FromError(logResult.Error); - } var embed = new EmbedBuilder().WithSmallTitle(title, bot) .WithDescription(description) diff --git a/src/Commands/ToolsCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs index 1dbf72d..ea91e1e 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; @@ -20,7 +21,7 @@ using Remora.Results; namespace Octobot.Commands; /// -/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp. +/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball. /// [UsedImplicitly] public class ToolsCommandGroup : CommandGroup @@ -100,10 +101,10 @@ public class ToolsCommandGroup : CommandGroup { var builder = new StringBuilder().AppendLine($"### <@{target.ID}>"); - if (target.GlobalName is not null) + if (target.GlobalName.IsDefined(out var globalName)) { builder.AppendBulletPointLine(Messages.UserInfoDisplayName) - .AppendLine(Markdown.InlineCode(target.GlobalName)); + .AppendLine(Markdown.InlineCode(globalName)); } builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince) @@ -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,23 @@ 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) + .WithDescription(Messages.TimeSpanExample) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + return await SendTimestampAsync(offset, executor, CancellationToken); } @@ -473,4 +497,65 @@ public class ToolsCommandGroup : CommandGroup return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + /// + /// A slash command that shows a random answer from the Magic 8-Ball. + /// + /// Unused input. + /// + /// The 8-Ball answers were taken from Wikipedia. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("8ball")] + [DiscordDefaultDMPermission(false)] + [Description("Ask the Magic 8-Ball a question")] + [UsedImplicitly] + public async Task ExecuteEightBallAsync( + // let the user think he's actually asking the ball a question + string question) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + 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 data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await AnswerEightBallAsync(bot, CancellationToken); + } + + private static readonly string[] AnswerTypes = + [ + "Positive", "Questionable", "Neutral", "Negative" + ]; + + private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) + { + var typeNumber = Random.Shared.Next(0, 4); + var embedColor = typeNumber switch + { + 0 => ColorsList.Blue, + 1 => ColorsList.Green, + 2 => ColorsList.Yellow, + 3 => ColorsList.Red, + _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber)) + }; + + var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized(); + + var embed = new EmbedBuilder().WithSmallTitle(answer, bot) + .WithColour(embedColor) + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } } diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index fa21db9..518465b 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -68,6 +68,11 @@ public static class GuildSettings /// public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel"); + /// + /// Controls what channel should welcome messages be sent to. + /// + public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel"); + public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel"); public static readonly SnowflakeOption DefaultRole = new("DefaultRole"); public static readonly SnowflakeOption MuteRole = new("MuteRole"); diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index b33cbb1..6932822 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -22,6 +22,7 @@ public enum AllOptionsEnum [UsedImplicitly] RenameHoistedUsers, [UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel, + [UsedImplicitly] WelcomeMessagesChannel, [UsedImplicitly] EventNotificationChannel, [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 7f60ebb..c81a02d 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -1,24 +1,22 @@ 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) { var property = settings[Name]; - return property != null ? ParseTimeSpan(property.GetValue()).Entity : DefaultValue; + return property != null ? TimeSpanParser.TryParse(property.GetValue()).Entity : DefaultValue; } public override Result Set(JsonNode settings, string from) { - if (!ParseTimeSpan(from).IsDefined(out var span)) + if (!TimeSpanParser.TryParse(from).IsDefined(out var span)) { return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); } @@ -26,9 +24,4 @@ public sealed class TimeSpanOption : Option settings[Name] = span.ToString(); return Result.FromSuccess(); } - - private static Result ParseTimeSpan(string from) - { - return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult(); - } } diff --git a/src/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs index 3805cea..9df90b8 100644 --- a/src/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -35,6 +35,7 @@ public static class LoggerExtensions return; } - logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message); + logger.LogWarning("{UserMessage}{NewLine}{ResultErrorMessage}", message, Environment.NewLine, + result.Error.Message); } } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index d780d0a..3c7ad6f 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1046,15 +1046,158 @@ namespace Octobot { internal static string SettingsLeaveMessage { get { return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); - } + } } - internal static string UserInfoKicked - { - get - { + internal static string InvalidTimeSpan { + get { + return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); + } + } + + internal static string UserInfoKicked { + get { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } + + internal static string ReminderEdited { + get { + return ResourceManager.GetString("ReminderEdited", resourceCulture); + } + } + + internal static string EightBallPositive1 { + get { + return ResourceManager.GetString("EightBallPositive1", resourceCulture); + } + } + + internal static string EightBallPositive2 { + get { + return ResourceManager.GetString("EightBallPositive2", resourceCulture); + } + } + + internal static string EightBallPositive3 { + get { + return ResourceManager.GetString("EightBallPositive3", resourceCulture); + } + } + + internal static string EightBallPositive4 { + get { + return ResourceManager.GetString("EightBallPositive4", resourceCulture); + } + } + + internal static string EightBallPositive5 { + get { + return ResourceManager.GetString("EightBallPositive5", resourceCulture); + } + } + + internal static string EightBallQuestionable1 { + get { + return ResourceManager.GetString("EightBallQuestionable1", resourceCulture); + } + } + + internal static string EightBallQuestionable2 { + get { + return ResourceManager.GetString("EightBallQuestionable2", resourceCulture); + } + } + + internal static string EightBallQuestionable3 { + get { + return ResourceManager.GetString("EightBallQuestionable3", resourceCulture); + } + } + + internal static string EightBallQuestionable4 { + get { + return ResourceManager.GetString("EightBallQuestionable4", resourceCulture); + } + } + + internal static string EightBallQuestionable5 { + get { + return ResourceManager.GetString("EightBallQuestionable5", resourceCulture); + } + } + + internal static string EightBallNeutral1 { + get { + return ResourceManager.GetString("EightBallNeutral1", resourceCulture); + } + } + + internal static string EightBallNeutral2 { + get { + return ResourceManager.GetString("EightBallNeutral2", resourceCulture); + } + } + + internal static string EightBallNeutral3 { + get { + return ResourceManager.GetString("EightBallNeutral3", resourceCulture); + } + } + + internal static string EightBallNeutral4 { + get { + return ResourceManager.GetString("EightBallNeutral4", resourceCulture); + } + } + + internal static string EightBallNeutral5 { + get { + return ResourceManager.GetString("EightBallNeutral5", resourceCulture); + } + } + + internal static string EightBallNegative1 { + get { + return ResourceManager.GetString("EightBallNegative1", resourceCulture); + } + } + + internal static string EightBallNegative2 { + get { + return ResourceManager.GetString("EightBallNegative2", resourceCulture); + } + } + + internal static string EightBallNegative3 { + get { + return ResourceManager.GetString("EightBallNegative3", resourceCulture); + } + } + + internal static string EightBallNegative4 { + get { + return ResourceManager.GetString("EightBallNegative4", resourceCulture); + } + } + + internal static string EightBallNegative5 { + get { + return ResourceManager.GetString("EightBallNegative5", resourceCulture); + } + } + + internal static string TimeSpanExample { + get { + return ResourceManager.GetString("TimeSpanExample", resourceCulture); + } + } + + internal static string SettingsWelcomeMessagesChannel + { + get { + return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); + } + } } } diff --git a/src/Octobot.cs b/src/Octobot.cs index 063bd14..1ebf7c3 100644 --- a/src/Octobot.cs +++ b/src/Octobot.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Octobot.Commands.Events; using Octobot.Services; -using Octobot.Services.Profiler; using Octobot.Services.Update; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; @@ -23,7 +22,7 @@ namespace Octobot; public sealed class Octobot { - public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot"; + public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; public static readonly AllowedMentions NoMentions = new( @@ -87,8 +86,6 @@ public sealed class Octobot .AddPreparationErrorEvent() .AddPostExecutionEvent() // Services - .AddTransient() - .AddSingleton() .AddSingleton() .AddSingleton() .AddHostedService(provider => provider.GetRequiredService()) 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(); +} diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs index eee93b6..012bfad 100644 --- a/src/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -51,7 +51,7 @@ public class GuildMemberJoinedResponder : IResponder return Result.FromError(returnRolesResult.Error); } - if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.FromSuccess(); @@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder .Build(); return await _channelApi.CreateMessageWithEmbedResultAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed, + GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, allowedMentions: Octobot.NoMentions, ct: ct); } diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs index f5c65f4..6ab7199 100644 --- a/src/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -28,9 +28,10 @@ public class MessageCreateResponder : IResponder "whoami" => "`nobody`", "сука !!" => "`root`", "воооо" => "`removing /...`", - "пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg", + "пон" => "https://i.ibb.co/Kw6QVcw/parry.jpg", "++++" => "#", "осу" => "https://github.com/ppy/osu", + "лан" => "https://i.ibb.co/VYH2QLc/lan.jpg", _ => default(Optional) }); return Task.FromResult(Result.FromSuccess()); diff --git a/src/Services/Profiler/Profiler.cs b/src/Services/Profiler/Profiler.cs deleted file mode 100644 index 8d4ca98..0000000 --- a/src/Services/Profiler/Profiler.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Diagnostics; -using System.Text; -using Microsoft.Extensions.Logging; -using Remora.Results; - -// TODO: remove in future profiler PRs -// ReSharper disable All - -namespace Octobot.Services.Profiler; - -/// -/// Provides the ability to profile how long certain parts of code take to complete using es. -/// -/// Resolve instead in singletons. -public sealed class Profiler -{ - private const int MaxProfilerTime = 1000; // milliseconds - private readonly List _events = []; - private readonly ILogger _logger; - - public Profiler(ILogger logger) - { - _logger = logger; - } - - /// - /// Pushes an event to the profiler. - /// - /// The ID of the event. - public void Push(string id) - { - _events.Add(new ProfilerEvent - { - Id = id, - Stopwatch = Stopwatch.StartNew() - }); - } - - /// - /// Pops the last pushed event from the profiler. - /// - /// Thrown if the profiler contains no events. - public void Pop() - { - if (_events.Count is 0) - { - throw new InvalidOperationException("Nothing to pop"); - } - - _events.Last().Stopwatch.Stop(); - } - - /// - /// If the profiler took too long to execute, this will log a warning with per-event time usage - /// - /// - private void Report() - { - var main = _events[0]; - if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime) - { - return; - } - - var unprofiled = main.Stopwatch.ElapsedMilliseconds; - var builder = new StringBuilder().AppendLine(); - for (var i = 1; i < _events.Count; i++) - { - var profilerEvent = _events[i]; - if (profilerEvent.Stopwatch.IsRunning) - { - throw new InvalidOperationException( - $"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}"); - } - - builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms"); - unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds; - } - - builder.AppendLine($": {unprofiled}ms"); - - _logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id, - main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString()); - } - - /// - /// the profiler and on it afterwards. - /// - public void PopAndReport() - { - Pop(); - Report(); - } - - /// - /// on the profiler and return a . - /// - /// - /// - public Result ReportWithResult(Result result) - { - PopAndReport(); - return result; - } - - /// - /// Calls with - /// - /// A successful result. - public Result ReportWithSuccess() - { - return ReportWithResult(Result.FromSuccess()); - } -} diff --git a/src/Services/Profiler/ProfilerEvent.cs b/src/Services/Profiler/ProfilerEvent.cs deleted file mode 100644 index f655fc4..0000000 --- a/src/Services/Profiler/ProfilerEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Diagnostics; - -namespace Octobot.Services.Profiler; - -public struct ProfilerEvent -{ - public string Id { get; init; } - public Stopwatch Stopwatch { get; init; } -} diff --git a/src/Services/Profiler/ProfilerFactory.cs b/src/Services/Profiler/ProfilerFactory.cs deleted file mode 100644 index 0135771..0000000 --- a/src/Services/Profiler/ProfilerFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Octobot.Services.Profiler; - -/// -/// Provides a method to create a . Useful in singletons. -/// -public sealed class ProfilerFactory -{ - private readonly IServiceScopeFactory _scopeFactory; - - public ProfilerFactory(IServiceScopeFactory scopeFactory) - { - _scopeFactory = scopeFactory; - } - - /// - /// Creates a new . - /// - /// A new . - // TODO: remove in future profiler PRs - // ReSharper disable once UnusedMember.Global - public Profiler Create() - { - return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - } -} diff --git a/src/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs index 06e531f..7674bbe 100644 --- a/src/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -185,7 +185,7 @@ public sealed partial class MemberUpdateService : BackgroundService { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname - : user.GlobalName ?? user.Username; + : user.GlobalName.OrDefault(user.Username); var characterList = currentNickname.ToList(); var usernameChanged = false; foreach (var character in currentNickname) diff --git a/src/Services/Utility.cs b/src/Services/Utility.cs index 401b067..ad06315 100644 --- a/src/Services/Utility.cs +++ b/src/Services/Utility.cs @@ -196,7 +196,7 @@ public sealed class Utility /// /// The cancellation token for this operation. /// A result which has succeeded. - public Result LogActionAsync( + public void LogAction( JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar, Color color, bool isPublic = true, CancellationToken ct = default) { @@ -205,7 +205,7 @@ public sealed class Utility if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId) && GuildSettings.PrivateFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)) { - return Result.FromSuccess(); + return; } var logEmbed = new EmbedBuilder().WithSmallTitle(title, avatar) @@ -230,8 +230,6 @@ public sealed class Utility privateChannel, embedResult: logEmbed, ct: ct); } - - return Result.FromSuccess(); } public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct)