From ab2158a6487005811f328f898645e2e74903a7cd Mon Sep 17 00:00:00 2001 From: mctaylors Date: Tue, 19 Mar 2024 18:46:56 +0300 Subject: [PATCH 01/33] Initial implementation of /warn Signed-off-by: mctaylors --- locale/Messages.resx | 30 ++++ locale/Messages.ru.resx | 30 ++++ locale/Messages.tt-ru.resx | 30 ++++ src/Commands/BanCommandGroup.cs | 2 +- src/Commands/KickCommandGroup.cs | 2 +- src/Commands/MuteCommandGroup.cs | 2 +- src/Commands/SettingsCommandGroup.cs | 5 +- src/Commands/WarnCommandGroup.cs | 253 +++++++++++++++++++++++++++ src/Data/GuildSettings.cs | 7 + src/Data/MemberData.cs | 1 + src/Data/Options/AllOptionsEnum.cs | 5 +- src/Data/Options/IntOption.cs | 31 ++++ src/Messages.Designer.cs | 49 ++++++ 13 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 src/Commands/WarnCommandGroup.cs create mode 100644 src/Data/Options/IntOption.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index c8ef510..49103c8 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -660,4 +660,34 @@ Welcome messages channel + + {0} received a warning + + + {0} no longer has warnings + + + You have been warned + + + Your warnings have been revoked + + + Warns: {0} + + + This user has no warnings! + + + Received too many warnings + + + Punishment type for warnings + + + Warnings threshold + + + Punishment duration for warnings + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index eb8e57b..3933a1a 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -660,4 +660,34 @@ Канал для приветствий + + {0} получил предупреждение + + + {0} больше не имеет предупреждений + + + Вы получили предупреждение + + + Ваши предупреждения были отозваны + + + Предупреждений: {0} + + + Этот пользователь не имеет предупреждений! + + + Получил слишком много предупреждений + + + Тип наказания для предупреждений + + + Порог предупреждений + + + Длительность наказания для предупреждений + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index df50d8d..b108287 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -660,4 +660,34 @@ канал куда говорить здравствуйте + + {0} схлопотал варн + + + {0} получил амнистию + + + вы схлопотали варн + + + вы получили амнистию + + + варнов: {0} + + + его еще никто ни в чем не обвинял + + + схлопотал много варнов + + + сколько времени держать варновый бан + + + тип варнового бана + + + сколько варнов чтобы потом бан + diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index c350729..6c8b004 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -127,7 +127,7 @@ public class BanCommandGroup : CommandGroup return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken); } - private async Task BanUserAsync( + public async Task BanUserAsync( IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index 0faa1d3..4262487 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -110,7 +110,7 @@ public class KickCommandGroup : CommandGroup return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken); } - private async Task KickUserAsync( + public async Task KickUserAsync( IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot, CancellationToken ct = default) { diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index c2542e8..43eddf1 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -121,7 +121,7 @@ public class MuteCommandGroup : CommandGroup return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); } - private async Task MuteUserAsync( + public async Task MuteUserAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 86f031f..9dd9e04 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -38,12 +38,14 @@ public class SettingsCommandGroup : CommandGroup private static readonly IOption[] AllOptions = [ GuildSettings.Language, + GuildSettings.WarnPunishment, GuildSettings.WelcomeMessage, GuildSettings.ReceiveStartupMessages, GuildSettings.RemoveRolesOnMute, GuildSettings.ReturnRolesOnRejoin, GuildSettings.AutoStartEvents, GuildSettings.RenameHoistedUsers, + GuildSettings.WarnsThreshold, GuildSettings.PublicFeedbackChannel, GuildSettings.PrivateFeedbackChannel, GuildSettings.WelcomeMessagesChannel, @@ -51,7 +53,8 @@ public class SettingsCommandGroup : CommandGroup GuildSettings.DefaultRole, GuildSettings.MuteRole, GuildSettings.EventNotificationRole, - GuildSettings.EventEarlyNotificationOffset + GuildSettings.EventEarlyNotificationOffset, + GuildSettings.WarnPunishmentDuration ]; private readonly ICommandContext _context; diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs new file mode 100644 index 0000000..a712857 --- /dev/null +++ b/src/Commands/WarnCommandGroup.cs @@ -0,0 +1,253 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text; +using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Rest.Core; +using Remora.Results; + +namespace Octobot.Commands; + +[UsedImplicitly] +public class WarnCommandGroup : CommandGroup +{ + private readonly IDiscordRestChannelAPI _channelApi; + private readonly ICommandContext _context; + private readonly IFeedbackService _feedback; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly GuildDataService _guildData; + private readonly IDiscordRestUserAPI _userApi; + private readonly Utility _utility; + + public WarnCommandGroup( + ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, + IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + Utility utility) + { + _context = context; + _channelApi = channelApi; + _guildData = guildData; + _feedback = feedback; + _guildApi = guildApi; + _userApi = userApi; + _utility = utility; + } + + [Command("warn")] + [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] + [Description("Warn user")] + [UsedImplicitly] + public async Task ExecuteWarnAsync( + [Description("User to warn")] IUser target, + [Description("Warn reason")] [MaxLength(256)] + string reason) + { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var data = await _guildData.GetData(guild.ID, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken); + } + + private async Task WarnUserAsync(IUser executor, IUser target, string reason, IGuild guild, + GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) + { + var memberData = data.GetOrCreateMemberData(target.ID); + memberData.Warns++; + + var warnsThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + + var builder = new StringBuilder() + .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) + .AppendBulletPointLine(string.Format(Messages.DescriptionActionWarns, + warnsThreshold is 0 ? memberData.Warns : $"{memberData.Warns}/{warnsThreshold}")); + + var title = string.Format(Messages.UserWarned, target.GetTag()); + var description = builder.ToString(); + + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); + if (dmChannelResult.IsDefined(out var dmChannel)) + { + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YouHaveBeenWarned) + .WithDescription(description) + .WithActionFooter(executor) + .WithCurrentTimestamp() + .WithColour(ColorsList.Yellow) + .Build(); + + await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); + } + + _utility.LogAction( + data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); + + if (memberData.Warns >= warnsThreshold && + GuildSettings.WarnPunishment.Get(data.Settings) is not "off" and not "disable" and not "disabled") + { + memberData.Warns = 0; + return await PunishUserAsync(target, guild, data, channelId, bot, CancellationToken); + } + + var embed = new EmbedBuilder().WithSmallTitle( + title, target) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private async Task PunishUserAsync(IUser target, IGuild guild, + GuildData data, Snowflake channelId, IUser bot, CancellationToken ct) + { + var settings = data.Settings; + var duration = GuildSettings.WarnPunishmentDuration.Get(settings); + + if (GuildSettings.WarnPunishment.Get(settings) is "ban" + && duration != TimeSpan.Zero) + { + var banCommandGroup = new BanCommandGroup(_context, _channelApi, _guildData, _feedback, _guildApi, _userApi, _utility); + await banCommandGroup.BanUserAsync(bot, target, Messages.ReceivedTooManyWarnings, + duration, guild, data, channelId, bot, ct); + } + + if (GuildSettings.WarnPunishment.Get(settings) is "kick") + { + var kickCommandGroup = new KickCommandGroup(_context, _channelApi, _guildData, _feedback, _guildApi, _userApi, _utility); + await kickCommandGroup.KickUserAsync(bot, target, Messages.ReceivedTooManyWarnings, + guild, channelId, data, bot, ct); + } + + if (GuildSettings.WarnPunishment.Get(settings) is "mute" + && duration != TimeSpan.Zero) + { + var muteCommandGroup = new MuteCommandGroup(_context, _guildData, _feedback, _guildApi, _userApi, _utility); + await muteCommandGroup.MuteUserAsync(bot, target, Messages.ReceivedTooManyWarnings, + duration, guild.ID, data, channelId, bot, ct); + } + + return Result.FromSuccess(); + } + + [Command("unwarn")] + [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultDMPermission(false)] + [RequireContext(ChannelContext.Guild)] + [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] + [Description("Remove warns from user")] + [UsedImplicitly] + public async Task ExecuteUnwarnAsync( + [Description("User to remove warns from")] + IUser target, + [Description("Warns remove reason")] [MaxLength(256)] + string reason) + { + if (!_context.TryGetContextIDs(out var guildId, out var channelId, 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var data = await _guildData.GetData(guild.ID, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await RemoveUserWarnsAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken); + } + + private async Task RemoveUserWarnsAsync(IUser executor, IUser target, string reason, IGuild guild, + GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) + { + var memberData = data.GetOrCreateMemberData(target.ID); + if (memberData.Warns is 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + memberData.Warns = 0; + + var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); + + var title = string.Format(Messages.UserWarnsRemoved, target.GetTag()); + var description = builder.ToString(); + + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); + if (dmChannelResult.IsDefined(out var dmChannel)) + { + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YourWarningsHaveBeenRevoked) + .WithDescription(description) + .WithActionFooter(executor) + .WithCurrentTimestamp() + .WithColour(ColorsList.Green) + .Build(); + + await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); + } + + var embed = new EmbedBuilder().WithSmallTitle( + title, target) + .WithColour(ColorsList.Green).Build(); + + _utility.LogAction( + data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } +} diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 5a99505..4e46be4 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -12,6 +12,8 @@ public static class GuildSettings { public static readonly LanguageOption Language = new("Language", "en"); + public static readonly Option WarnPunishment = new("WarnPunishment", "disabled"); + /// /// Controls what message should be sent in when a new member joins the server. /// @@ -46,6 +48,8 @@ public static class GuildSettings /// public static readonly BoolOption RenameHoistedUsers = new("RenameHoistedUsers", false); + public static readonly IntOption WarnsThreshold = new("WarnsThreshold", 0); + /// /// Controls what channel should all public messages be sent to. /// @@ -71,4 +75,7 @@ public static class GuildSettings /// public static readonly TimeSpanOption EventEarlyNotificationOffset = new( "EventEarlyNotificationOffset", TimeSpan.Zero); + + public static readonly TimeSpanOption WarnPunishmentDuration = new( + "WarnPunishmentDuration", TimeSpan.Zero); } diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 8e23e54..2701be7 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -18,6 +18,7 @@ public sealed class MemberData public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; } + public int Warns { get; set; } public bool Kicked { get; set; } public List Roles { get; set; } = []; public List Reminders { get; } = []; diff --git a/src/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs index e9637d6..f3ef3eb 100644 --- a/src/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -13,12 +13,14 @@ namespace Octobot.Data.Options; public enum AllOptionsEnum { [UsedImplicitly] Language, + [UsedImplicitly] WarnPunishment, [UsedImplicitly] WelcomeMessage, [UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] RemoveRolesOnMute, [UsedImplicitly] ReturnRolesOnRejoin, [UsedImplicitly] AutoStartEvents, [UsedImplicitly] RenameHoistedUsers, + [UsedImplicitly] WarnsThreshold, [UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel, [UsedImplicitly] WelcomeMessagesChannel, @@ -26,5 +28,6 @@ public enum AllOptionsEnum [UsedImplicitly] DefaultRole, [UsedImplicitly] MuteRole, [UsedImplicitly] EventNotificationRole, - [UsedImplicitly] EventEarlyNotificationOffset + [UsedImplicitly] EventEarlyNotificationOffset, + [UsedImplicitly] WarnPunishmentDuration } diff --git a/src/Data/Options/IntOption.cs b/src/Data/Options/IntOption.cs new file mode 100644 index 0000000..62b2883 --- /dev/null +++ b/src/Data/Options/IntOption.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Nodes; +using Remora.Results; + +namespace Octobot.Data.Options; + +public sealed class IntOption : Option +{ + public IntOption(string name, int defaultValue) : base(name, defaultValue) { } + + public override string Display(JsonNode settings) + { + return settings[Name]?.GetValue() ?? "0"; + } + + public override Result Set(JsonNode settings, string from) + { + if (!int.TryParse(from, out _)) + { + return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue); + } + + settings[Name] = from; + return Result.FromSuccess(); + } + + public override int Get(JsonNode settings) + { + var property = settings[Name]; + return property != null ? Convert.ToInt32(property.GetValue()) : DefaultValue; + } +} diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 5ad741e..3d3c0d8 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1191,5 +1191,54 @@ namespace Octobot { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } } + + internal static string UserWarned + { + get { + return ResourceManager.GetString("UserWarned", resourceCulture); + } + } + + internal static string UserWarnsRemoved + { + get { + return ResourceManager.GetString("UserWarnsRemoved", resourceCulture); + } + } + + internal static string YouHaveBeenWarned + { + get { + return ResourceManager.GetString("YouHaveBeenWarned", resourceCulture); + } + } + + internal static string YourWarningsHaveBeenRevoked + { + get { + return ResourceManager.GetString("YourWarningsHaveBeenRevoked", resourceCulture); + } + } + + internal static string DescriptionActionWarns + { + get { + return ResourceManager.GetString("DescriptionActionWarns", resourceCulture); + } + } + + internal static string UserHasNoWarnings + { + get { + return ResourceManager.GetString("UserHasNoWarnings", resourceCulture); + } + } + + internal static string ReceivedTooManyWarnings + { + get { + return ResourceManager.GetString("ReceivedTooManyWarnings", resourceCulture); + } + } } } From 88448cdb49c315d2b8909eee6830c6446f3b193d Mon Sep 17 00:00:00 2001 From: mctaylors Date: Sun, 24 Mar 2024 15:55:26 +0300 Subject: [PATCH 02/33] Better implementation of /warn Signed-off-by: mctaylors --- locale/Messages.resx | 51 ++++++++ locale/Messages.ru.resx | 51 ++++++++ locale/Messages.tt-ru.resx | 53 +++++++- src/Commands/WarnCommandGroup.cs | 217 +++++++++++++++++++++++++++++-- src/Data/MemberData.cs | 9 +- src/Data/Warn.cs | 10 ++ src/Messages.Designer.cs | 119 +++++++++++++++++ 7 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 src/Data/Warn.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 09bb4db..27c0d3f 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -705,4 +705,55 @@ Punishment duration for warnings + + Here's your warnings, {0}: + + + You have no warnings! + + + Received on {0} + + + Warning #{0} has been removed from {1} + + + Your warning has been revoked + + + Wrong warning number selected! + + + You cannot warn me! + + + You cannot warn the owner of this guild! + + + You cannot warn this member! + + + You cannot warn yourself! + + + You cannot unwarn me! + + + You cannot unwarn the owner of this guild! + + + You cannot unwarn this member! + + + You cannot unwarn yourself! + + + I cannot warn members from this guild! + + + I cannot warn this member! + + + I cannot unwarn this member! + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index b37ea64..eec45aa 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -705,4 +705,55 @@ Длительность наказания для предупреждений + + Вот ваши предупреждения, {0}: + + + У вас нет предупреждений! + + + Получено {0} + + + Предупреждение №{0} было снято с {1} + + + Ваше предупреждение было отозвано + + + Выбрано неверное число предупреждения! + + + Ты не можешь меня предупредить! + + + Ты не можешь предупредить владельца этого сервера! + + + Ты не можешь предупредить этого участника! + + + Ты не можешь себя предупредить! + + + Ты не можешь снять с меня предупреждения! + + + Ты не можешь снять предупреждения с владельца этого сервера! + + + Ты не можешь снять предупреждения с этого участника! + + + Ты не можешь снять с себя предупреждения! + + + Я не могу снимать предупреждения этого участника! + + + Я не могу предупредить этого участника! + + + Я не могу предупреждать участников этого сервера! + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 050dc6d..178e83e 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -685,7 +685,7 @@ вы схлопотали варн - вы получили амнистию + вы получили карт-бланш варнов: {0} @@ -705,4 +705,55 @@ сколько варнов чтобы потом бан + + хаха, смотри {0}, это все ты заслужил: + + + ого, да на тебя еще нет претензий! + + + получил {0} + + + варн {0} снят с {1} + + + вы получили амнистию + + + да нету такого варна вроде + + + ээбля френдли фаер огонь по своим + + + самовар нельзя + + + варн этому шизику нельзя + + + варн админу нельзя + + + ну, к сожалению тебе этого не дано + + + разварн админу нельзя + + + разварн этому шизику нельзя + + + саморазвар нельзя + + + я не могу его заварить... + + + я не могу его разварить... + + + я не могу ваще никого варить... + diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index a712857..9c08aa2 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -14,8 +14,10 @@ using Remora.Discord.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; +using static System.DateTimeOffset; namespace Octobot.Commands; @@ -49,7 +51,8 @@ public class WarnCommandGroup : CommandGroup [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.KickMembers)] - [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] + [RequireBotDiscordPermissions(DiscordPermission.KickMembers, + DiscordPermission.ModerateMembers, DiscordPermission.BanMembers)] [Description("Warn user")] [UsedImplicitly] public async Task ExecuteWarnAsync( @@ -89,15 +92,37 @@ public class WarnCommandGroup : CommandGroup private async Task WarnUserAsync(IUser executor, IUser target, string reason, IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { + var interactionResult + = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", ct); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + var memberData = data.GetOrCreateMemberData(target.ID); - memberData.Warns++; + var warns = memberData.Warns; + + warns.Add(new Warn + { + WarnedBy = executor.ID.Value, + At = UtcNow, + Reason = reason + }); var warnsThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); var builder = new StringBuilder() .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) .AppendBulletPointLine(string.Format(Messages.DescriptionActionWarns, - warnsThreshold is 0 ? memberData.Warns : $"{memberData.Warns}/{warnsThreshold}")); + warnsThreshold is 0 ? warns.Count : $"{warns.Count}/{warnsThreshold}")); var title = string.Format(Messages.UserWarned, target.GetTag()); var description = builder.ToString(); @@ -119,10 +144,10 @@ public class WarnCommandGroup : CommandGroup _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); - if (memberData.Warns >= warnsThreshold && + if (warns.Count >= warnsThreshold && GuildSettings.WarnPunishment.Get(data.Settings) is not "off" and not "disable" and not "disabled") { - memberData.Warns = 0; + warns.Clear(); return await PunishUserAsync(target, guild, data, channelId, bot, CancellationToken); } @@ -170,14 +195,15 @@ public class WarnCommandGroup : CommandGroup [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] [RequireDiscordPermission(DiscordPermission.KickMembers)] - [RequireBotDiscordPermissions(DiscordPermission.KickMembers)] [Description("Remove warns from user")] [UsedImplicitly] public async Task ExecuteUnwarnAsync( [Description("User to remove warns from")] IUser target, - [Description("Warns remove reason")] [MaxLength(256)] - string reason) + [Description("Warn remove reason")] [MaxLength(256)] + string reason, + [Description("Number of the warning to be deleted")] + int? number = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) { @@ -205,14 +231,37 @@ public class WarnCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + if (number is not null) + { + return await RemoveUserWarnAsync(executor, target, reason, number.Value, guild, data, channelId, bot, + CancellationToken); + } + return await RemoveUserWarnsAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken); } - private async Task RemoveUserWarnsAsync(IUser executor, IUser target, string reason, IGuild guild, - GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) + private async Task RemoveUserWarnAsync(IUser executor, IUser target, string reason, int warnNumber, + IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { + var interactionResult + = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + var memberData = data.GetOrCreateMemberData(target.ID); - if (memberData.Warns is 0) + var warns = memberData.Warns; + + if (warns.Count is 0) { var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot) .WithColour(ColorsList.Red).Build(); @@ -220,10 +269,82 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } - memberData.Warns = 0; + var index = warnNumber - 1; + + if (index >= warns.Count || index < 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.WrongWarningNumberSelected, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var builder = new StringBuilder() + .Append("> ").AppendLine(warns[index].Reason) + .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); + + warns.RemoveAt(index); + + var title = string.Format(Messages.UserWarnRemoved, warnNumber, target.GetTag()); + var description = builder.ToString(); + + var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct); + if (dmChannelResult.IsDefined(out var dmChannel)) + { + var dmEmbed = new EmbedBuilder().WithGuildTitle(guild) + .WithTitle(Messages.YourWarningHasBeenRevoked) + .WithDescription(description) + .WithActionFooter(executor) + .WithCurrentTimestamp() + .WithColour(ColorsList.Green) + .Build(); + + await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); + } + + var embed = new EmbedBuilder().WithSmallTitle( + title, target) + .WithColour(ColorsList.Green).Build(); + + _utility.LogAction( + data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private async Task RemoveUserWarnsAsync(IUser executor, IUser target, string reason, + IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) + { + var interactionResult + = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + + var memberData = data.GetOrCreateMemberData(target.ID); + var warns = memberData.Warns; + + if (warns.Count is 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)); + warns.Clear(); + var title = string.Format(Messages.UserWarnsRemoved, target.GetTag()); var description = builder.ToString(); @@ -250,4 +371,76 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + [Command("listwarn")] + [DiscordDefaultDMPermission(false)] + [Ephemeral] + [Description("(Ephemeral) Get your current warns")] + [UsedImplicitly] + public async Task ExecuteListWarnsAsync() + { + 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken); + if (!guildResult.IsDefined(out var guild)) + { + return Result.FromError(guildResult); + } + + var data = await _guildData.GetData(guild.ID, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await ListWarnsAsync(executor, data, bot, CancellationToken); + } + + private async Task ListWarnsAsync(IUser executor, GuildData data, IUser bot, CancellationToken ct = default) + { + var memberData = data.GetOrCreateMemberData(executor.ID); + var warns = memberData.Warns; + + if (warns.Count is 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.YouHaveNoWarnings, bot) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var warnsThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + + var description = new StringBuilder() + .AppendLine(string.Format(Messages.DescriptionActionWarns, + warnsThreshold is 0 ? warns.Count : $"{warns.Count}/{warnsThreshold}")); + + var warnCount = 0; + foreach (var warn in warns) + { + warnCount++; + description.Append(warnCount).Append(". ").AppendLine(warn.Reason) + .AppendSubBulletPoint(Messages.IssuedBy).Append(' ').AppendLine(Mention.User(warn.WarnedBy.ToSnowflake())) + .AppendSubBulletPointLine(string.Format(Messages.ReceivedOn, Markdown.Timestamp(warn.At))); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.ListWarnTitle, executor.GetTag()), executor) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Default).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } } diff --git a/src/Data/MemberData.cs b/src/Data/MemberData.cs index 8975a25..fe29e7f 100644 --- a/src/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -5,20 +5,25 @@ namespace Octobot.Data; /// public sealed class MemberData { - public MemberData(ulong id, List? reminders = null) + public MemberData(ulong id, List? reminders = null, List? warns = null) { Id = id; if (reminders is not null) { Reminders = reminders; } + + if (warns is not null) + { + Warns = warns; + } } public ulong Id { get; } public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; } - public int Warns { get; set; } public bool Kicked { get; set; } public List Roles { get; set; } = []; public List Reminders { get; } = []; + public List Warns { get; } = []; } diff --git a/src/Data/Warn.cs b/src/Data/Warn.cs new file mode 100644 index 0000000..97e93f0 --- /dev/null +++ b/src/Data/Warn.cs @@ -0,0 +1,10 @@ +using Remora.Rest.Core; + +namespace Octobot.Data; + +public struct Warn +{ + public ulong WarnedBy { get; init; } + public DateTimeOffset At { get; init; } + public string Reason { get; init; } +} diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 16cd3eb..9c50043 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1247,6 +1247,13 @@ namespace Octobot { } } + internal static string YouHaveNoWarnings + { + get { + return ResourceManager.GetString("YouHaveNoWarnings", resourceCulture); + } + } + internal static string ReceivedTooManyWarnings { get { @@ -1265,5 +1272,117 @@ namespace Octobot { return ResourceManager.GetString("ButtonOpenWiki", resourceCulture); } } + + internal static string ListWarnTitle + { + get { + return ResourceManager.GetString("ListWarnTitle", resourceCulture); + } + } + + internal static string ReceivedOn + { + get { + return ResourceManager.GetString("ReceivedOn", resourceCulture); + } + } + + internal static string UserWarnRemoved + { + get { + return ResourceManager.GetString("UserWarnRemoved", resourceCulture); + } + } + + internal static string YourWarningHasBeenRevoked + { + get { + return ResourceManager.GetString("YourWarningHasBeenRevoked", resourceCulture); + } + } + + internal static string WrongWarningNumberSelected + { + get { + return ResourceManager.GetString("WrongWarningNumberSelected", resourceCulture); + } + } + + internal static string UserCannotWarnBot + { + get { + return ResourceManager.GetString("UserCannotWarnBot", resourceCulture); + } + } + + internal static string UserCannotWarnOwner + { + get { + return ResourceManager.GetString("UserCannotWarnOwner", resourceCulture); + } + } + + internal static string UserCannotWarnTarget + { + get { + return ResourceManager.GetString("UserCannotWarnTarget", resourceCulture); + } + } + + internal static string UserCannotWarnThemselves + { + get { + return ResourceManager.GetString("UserCannotWarnThemselves", resourceCulture); + } + } + + internal static string UserCannotUnwarnBot + { + get { + return ResourceManager.GetString("UserCannotUnwarnBot", resourceCulture); + } + } + + internal static string UserCannotUnwarnOwner + { + get { + return ResourceManager.GetString("UserCannotUnwarnOwner", resourceCulture); + } + } + + internal static string UserCannotUnwarnTarget + { + get { + return ResourceManager.GetString("UserCannotUnwarnTarget", resourceCulture); + } + } + + internal static string UserCannotUnwarnThemselves + { + get { + return ResourceManager.GetString("UserCannotUnwarnThemselves", resourceCulture); + } + } + + internal static string BotCannotWarnTarget + { + get { + return ResourceManager.GetString("BotCannotWarnTarget", resourceCulture); + } + } + + internal static string BotCannotWarnMembers + { + get { + return ResourceManager.GetString("BotCannotWarnMembers", resourceCulture); + } + } + + internal static string BotCannotUnwarnTarget + { + get { + return ResourceManager.GetString("BotCannotUnwarnTarget", resourceCulture); + } + } } } From dbd14a458a41a36e1c8716cf1c54a184f7579d85 Mon Sep 17 00:00:00 2001 From: mctaylors Date: Sun, 24 Mar 2024 16:09:25 +0300 Subject: [PATCH 03/33] what Signed-off-by: mctaylors --- src/Commands/WarnCommandGroup.cs | 2 +- src/Data/Warn.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index 9c08aa2..a57b079 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -375,7 +375,7 @@ public class WarnCommandGroup : CommandGroup [Command("listwarn")] [DiscordDefaultDMPermission(false)] [Ephemeral] - [Description("(Ephemeral) Get your current warns")] + [Description("(Ephemeral) Get current warns")] [UsedImplicitly] public async Task ExecuteListWarnsAsync() { diff --git a/src/Data/Warn.cs b/src/Data/Warn.cs index 97e93f0..f70d46e 100644 --- a/src/Data/Warn.cs +++ b/src/Data/Warn.cs @@ -1,6 +1,4 @@ -using Remora.Rest.Core; - -namespace Octobot.Data; +namespace Octobot.Data; public struct Warn { From 152682e4568cea5169c4cae15647b027a5fba4eb Mon Sep 17 00:00:00 2001 From: mctaylors Date: Tue, 26 Mar 2024 20:59:56 +0300 Subject: [PATCH 04/33] (Almost) Final implementation of /warn Signed-off-by: mctaylors --- locale/Messages.resx | 37 +++++- locale/Messages.ru.resx | 37 +++++- locale/Messages.tt-ru.resx | 37 +++++- src/Commands/WarnCommandGroup.cs | 165 +++++++++++++++++++++------ src/Messages.Designer.cs | 76 +++--------- src/Services/AccessControlService.cs | 3 +- 6 files changed, 253 insertions(+), 102 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 5379ab6..326e1b8 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -693,7 +693,7 @@ Your warnings have been revoked - + Warns: {0} @@ -711,7 +711,7 @@ Punishment duration for warnings - + Here's your warnings, {0}: @@ -762,4 +762,37 @@ I cannot unwarn this member! + + You cannot get my warns! + + + You cannot get owner's warns! + + + You cannot get warns of this member! + + + Use this command without options instead. + + + You cannot warn members in this guild! + + + You cannot unwarn members in this guild! + + + You cannot get warns of other members in this guild! + + + Warnings given to {0}: + + + Punishment type: {0} + + + Warn threshold has been exceeded. ({0}) + + + The WarnPunishmentDuration setting is not set for the current punishment type. + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 072a2e8..d28b2ef 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -693,7 +693,7 @@ Ваши предупреждения были отозваны - + Предупреждений: {0} @@ -711,7 +711,7 @@ Длительность наказания для предупреждений - + Вот ваши предупреждения, {0}: @@ -762,4 +762,37 @@ Я не могу предупреждать участников этого сервера! + + Ты не можешь просмотреть мои предупреждения! + + + Ты не можешь просмотреть предупреждения владельца этого сервера! + + + Ты не можешь просмотреть предупреждения этого участника! + + + Вместо этого, используйте эту команду без параметров. + + + Ты не можешь снимать предупреждения с участников этого сервера! + + + Ты не можешь предупреждать участников этого сервера! + + + Ты не можешь просматривать предупреждения участников этого сервера! + + + Предупреждения пользователя {0}: + + + Тип наказания: {0} + + + Превышен порог предупреждений. ({0}) + + + Настройка WarnPunishmentDuration не установлена для текущего типа наказания. + diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 8f56514..9dbf513 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -693,7 +693,7 @@ вы получили карт-бланш - + варнов: {0} @@ -711,7 +711,7 @@ сколько варнов чтобы потом бан - + хаха, смотри {0}, это все ты заслужил: @@ -762,4 +762,37 @@ я не могу ваще никого варить... + + тебе нельзя варить шизоидов + + + тебе нельзя разваривать шизоидов + + + а ты специально указал себя когда мог этого не делать? + + + тебе нельзя чекать варны шизоидов + + + чекать варны админа нельзя + + + чекать варны бота нельзя + + + чекать варты этого шизика нельзя + + + самовары {0}: + + + тип бана: {0} + + + мы дошли до лимита варнов. ({0}) + + + наказание что было установлено хочет установленного WarnPunishmentDuration + diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index a57b079..74b9040 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; @@ -24,6 +25,7 @@ namespace Octobot.Commands; [UsedImplicitly] public class WarnCommandGroup : CommandGroup { + private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -35,7 +37,7 @@ public class WarnCommandGroup : CommandGroup public WarnCommandGroup( ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData, IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, - Utility utility) + Utility utility, AccessControlService access) { _context = context; _channelApi = channelApi; @@ -44,13 +46,14 @@ public class WarnCommandGroup : CommandGroup _guildApi = guildApi; _userApi = userApi; _utility = utility; + _access = access; } [Command("warn")] - [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.KickMembers, DiscordPermission.ModerateMembers, DiscordPermission.BanMembers)] [Description("Warn user")] @@ -93,7 +96,7 @@ public class WarnCommandGroup : CommandGroup GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -110,6 +113,17 @@ public class WarnCommandGroup : CommandGroup var memberData = data.GetOrCreateMemberData(target.ID); var warns = memberData.Warns; + var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + + if (warns.Count >= warnThreshold && warnThreshold is not 0) + { + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.WarnThresholdExceeded, warnThreshold), bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + warns.Add(new Warn { WarnedBy = executor.ID.Value, @@ -117,12 +131,10 @@ public class WarnCommandGroup : CommandGroup Reason = reason }); - var warnsThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); - var builder = new StringBuilder() .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason)) - .AppendBulletPointLine(string.Format(Messages.DescriptionActionWarns, - warnsThreshold is 0 ? warns.Count : $"{warns.Count}/{warnsThreshold}")); + .AppendBulletPointLine(string.Format(Messages.DescriptionWarns, + warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}")); var title = string.Format(Messages.UserWarned, target.GetTag()); var description = builder.ToString(); @@ -144,11 +156,10 @@ public class WarnCommandGroup : CommandGroup _utility.LogAction( data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); - if (warns.Count >= warnsThreshold && + if (warns.Count >= warnThreshold && GuildSettings.WarnPunishment.Get(data.Settings) is not "off" and not "disable" and not "disabled") { - warns.Clear(); - return await PunishUserAsync(target, guild, data, channelId, bot, CancellationToken); + return await PunishUserAsync(target, guild, data, channelId, bot, warns, CancellationToken); } var embed = new EmbedBuilder().WithSmallTitle( @@ -159,42 +170,56 @@ public class WarnCommandGroup : CommandGroup } private async Task PunishUserAsync(IUser target, IGuild guild, - GuildData data, Snowflake channelId, IUser bot, CancellationToken ct) + GuildData data, Snowflake channelId, IUser bot, IList warns, CancellationToken ct) { var settings = data.Settings; + var warnPunishment = GuildSettings.WarnPunishment.Get(settings); var duration = GuildSettings.WarnPunishmentDuration.Get(settings); - if (GuildSettings.WarnPunishment.Get(settings) is "ban" - && duration != TimeSpan.Zero) + if (warnPunishment is "ban" && duration != TimeSpan.Zero) { - var banCommandGroup = new BanCommandGroup(_context, _channelApi, _guildData, _feedback, _guildApi, _userApi, _utility); + warns.Clear(); + var banCommandGroup = new BanCommandGroup( + _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await banCommandGroup.BanUserAsync(bot, target, Messages.ReceivedTooManyWarnings, duration, guild, data, channelId, bot, ct); } - if (GuildSettings.WarnPunishment.Get(settings) is "kick") + if (warnPunishment is "kick") { - var kickCommandGroup = new KickCommandGroup(_context, _channelApi, _guildData, _feedback, _guildApi, _userApi, _utility); + warns.Clear(); + var kickCommandGroup = new KickCommandGroup( + _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await kickCommandGroup.KickUserAsync(bot, target, Messages.ReceivedTooManyWarnings, guild, channelId, data, bot, ct); } - if (GuildSettings.WarnPunishment.Get(settings) is "mute" - && duration != TimeSpan.Zero) + if (warnPunishment is "mute" && duration != TimeSpan.Zero) { - var muteCommandGroup = new MuteCommandGroup(_context, _guildData, _feedback, _guildApi, _userApi, _utility); + warns.Clear(); + var muteCommandGroup = new MuteCommandGroup( + _access, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await muteCommandGroup.MuteUserAsync(bot, target, Messages.ReceivedTooManyWarnings, duration, guild.ID, data, channelId, bot, ct); } - return Result.FromSuccess(); + if (warnPunishment is not ("ban" or "mute") || duration != TimeSpan.Zero) + { + return Result.FromSuccess(); + } + + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.WarnPunishmentDurationNotSet, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } [Command("unwarn")] - [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] [DiscordDefaultDMPermission(false)] [RequireContext(ChannelContext.Guild)] - [RequireDiscordPermission(DiscordPermission.KickMembers)] + [RequireDiscordPermission(DiscordPermission.ManageMessages)] [Description("Remove warns from user")] [UsedImplicitly] public async Task ExecuteUnwarnAsync( @@ -244,7 +269,7 @@ public class WarnCommandGroup : CommandGroup IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -316,7 +341,7 @@ public class WarnCommandGroup : CommandGroup IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { var interactionResult - = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -377,7 +402,9 @@ public class WarnCommandGroup : CommandGroup [Ephemeral] [Description("(Ephemeral) Get current warns")] [UsedImplicitly] - public async Task ExecuteListWarnsAsync() + public async Task ExecuteListWarnsAsync( + [Description("(Moderator-only) Get target's current warns")] + IUser? target = null) { if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) { @@ -405,10 +432,75 @@ public class WarnCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await ListWarnsAsync(executor, data, bot, CancellationToken); + if (target is not null) + { + return await ListTargetWarnsAsync(executor, target, guild, data, bot, CancellationToken); + } + + return await ListExecutorWarnsAsync(executor, data, bot, CancellationToken); } - private async Task ListWarnsAsync(IUser executor, GuildData data, IUser bot, CancellationToken ct = default) + private async Task ListTargetWarnsAsync(IUser executor, IUser target, IGuild guild, + GuildData data, IUser bot, CancellationToken ct = default) + { + var interactionResult + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "GetWarns", ct); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + + var memberData = data.GetOrCreateMemberData(target.ID); + var warns = memberData.Warns; + + if (warns.Count is 0) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot) + .WithColour(ColorsList.Green).Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + + var punishmentType = GuildSettings.WarnPunishment.Get(data.Settings); + + var description = new StringBuilder() + .AppendLine(string.Format(Messages.DescriptionWarns, + warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}")); + if (punishmentType is not "off" and not "disable" and not "disabled") + { + description.AppendLine(string.Format( + Messages.DescriptionPunishmentType, Markdown.InlineCode(punishmentType))); + } + + var warnCount = 0; + foreach (var warn in warns) + { + warnCount++; + description.Append(warnCount).Append(". ").AppendLine(warn.Reason) + .AppendSubBulletPoint(Messages.IssuedBy).Append(' ').AppendLine(Mention.User(warn.WarnedBy.ToSnowflake())) + .AppendSubBulletPointLine(string.Format(Messages.ReceivedOn, Markdown.Timestamp(warn.At))); + } + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.ListTargetWarnsTitle, target.GetTag()), target) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Default).Build(); + + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private async Task ListExecutorWarnsAsync(IUser executor, GuildData data, IUser bot, + CancellationToken ct = default) { var memberData = data.GetOrCreateMemberData(executor.ID); var warns = memberData.Warns; @@ -421,11 +513,18 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); } - var warnsThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + + var punishmentType = GuildSettings.WarnPunishment.Get(data.Settings); var description = new StringBuilder() - .AppendLine(string.Format(Messages.DescriptionActionWarns, - warnsThreshold is 0 ? warns.Count : $"{warns.Count}/{warnsThreshold}")); + .AppendLine(string.Format(Messages.DescriptionWarns, + warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}")); + if (punishmentType is not "off" and not "disable" and not "disabled") + { + description.AppendLine(string.Format( + Messages.DescriptionPunishmentType, Markdown.InlineCode(punishmentType))); + } var warnCount = 0; foreach (var warn in warns) @@ -437,7 +536,7 @@ public class WarnCommandGroup : CommandGroup } var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.ListWarnTitle, executor.GetTag()), executor) + .WithSmallTitle(string.Format(Messages.ListExecutorWarnsTitle, executor.GetTag()), executor) .WithDescription(description.ToString()) .WithColour(ColorsList.Default).Build(); diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 9c50043..cadb676 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1233,10 +1233,10 @@ namespace Octobot { } } - internal static string DescriptionActionWarns + internal static string DescriptionWarns { get { - return ResourceManager.GetString("DescriptionActionWarns", resourceCulture); + return ResourceManager.GetString("DescriptionWarns", resourceCulture); } } @@ -1273,10 +1273,10 @@ namespace Octobot { } } - internal static string ListWarnTitle + internal static string ListTargetWarnsTitle { get { - return ResourceManager.GetString("ListWarnTitle", resourceCulture); + return ResourceManager.GetString("ListTargetWarnsTitle", resourceCulture); } } @@ -1308,80 +1308,32 @@ namespace Octobot { } } - internal static string UserCannotWarnBot + internal static string ListExecutorWarnsTitle { get { - return ResourceManager.GetString("UserCannotWarnBot", resourceCulture); + return ResourceManager.GetString("ListExecutorWarnsTitle", resourceCulture); } } - internal static string UserCannotWarnOwner + internal static string DescriptionPunishmentType { get { - return ResourceManager.GetString("UserCannotWarnOwner", resourceCulture); + return ResourceManager.GetString("DescriptionPunishmentType", resourceCulture); } } - internal static string UserCannotWarnTarget + internal static string WarnThresholdExceeded { get { - return ResourceManager.GetString("UserCannotWarnTarget", resourceCulture); + return ResourceManager.GetString("WarnThresholdExceeded", resourceCulture); } } - internal static string UserCannotWarnThemselves + internal static string WarnPunishmentDurationNotSet { - get { - return ResourceManager.GetString("UserCannotWarnThemselves", resourceCulture); - } - } - - internal static string UserCannotUnwarnBot - { - get { - return ResourceManager.GetString("UserCannotUnwarnBot", resourceCulture); - } - } - - internal static string UserCannotUnwarnOwner - { - get { - return ResourceManager.GetString("UserCannotUnwarnOwner", resourceCulture); - } - } - - internal static string UserCannotUnwarnTarget - { - get { - return ResourceManager.GetString("UserCannotUnwarnTarget", resourceCulture); - } - } - - internal static string UserCannotUnwarnThemselves - { - get { - return ResourceManager.GetString("UserCannotUnwarnThemselves", resourceCulture); - } - } - - internal static string BotCannotWarnTarget - { - get { - return ResourceManager.GetString("BotCannotWarnTarget", resourceCulture); - } - } - - internal static string BotCannotWarnMembers - { - get { - return ResourceManager.GetString("BotCannotWarnMembers", resourceCulture); - } - } - - internal static string BotCannotUnwarnTarget - { - get { - return ResourceManager.GetString("BotCannotUnwarnTarget", resourceCulture); + get + { + return ResourceManager.GetString("WarnPunishmentDurationNotSet", resourceCulture); } } } diff --git a/src/Services/AccessControlService.cs b/src/Services/AccessControlService.cs index cb235f9..d164ed1 100644 --- a/src/Services/AccessControlService.cs +++ b/src/Services/AccessControlService.cs @@ -116,7 +116,8 @@ public sealed class AccessControlService { "Ban" => DiscordPermission.BanMembers, "Kick" => DiscordPermission.KickMembers, - "Mute" or "Unmute" => DiscordPermission.ModerateMembers, + "Mute" or "Unmute" or "Warn" or "Unwarn" or "GetWarns" + => DiscordPermission.ModerateMembers, _ => throw new Exception() }); From 5239b8280682b7ea4e22e9fdbb3a67fd05e36b6e Mon Sep 17 00:00:00 2001 From: mctaylors Date: Tue, 26 Mar 2024 21:54:31 +0300 Subject: [PATCH 05/33] Some workarounds... Signed-off-by: mctaylors --- locale/Messages.resx | 5 +- locale/Messages.ru.resx | 5 +- locale/Messages.tt-ru.resx | 5 +- src/Commands/WarnCommandGroup.cs | 137 ++++++++++++++++--------------- src/Messages.Designer.cs | 8 ++ 5 files changed, 89 insertions(+), 71 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 326e1b8..9f771e3 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -793,6 +793,9 @@ Warn threshold has been exceeded. ({0}) - The WarnPunishmentDuration setting is not set for the current punishment type. + Warn Punishment Duration is not set for the current punishment type. + + + Warn Threshold is set, but the Warn Punishment is not. diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index d28b2ef..1dd0ffc 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -793,6 +793,9 @@ Превышен порог предупреждений. ({0}) - Настройка WarnPunishmentDuration не установлена для текущего типа наказания. + Длительность наказания предупреждения не установлена для текущего типа наказания. + + + Порог предупреждения установлен, но наказание нет. diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx index 9dbf513..13f12bf 100644 --- a/locale/Messages.tt-ru.resx +++ b/locale/Messages.tt-ru.resx @@ -793,6 +793,9 @@ мы дошли до лимита варнов. ({0}) - наказание что было установлено хочет установленного WarnPunishmentDuration + наказание что было установлено хочет установленную ддлительность в настройках + + + лимит стоит, а наказания нет. diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index 74b9040..d7e6fc5 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; +using System.Text.Json.Nodes; using JetBrains.Annotations; using Octobot.Data; using Octobot.Extensions; @@ -89,14 +90,8 @@ public class WarnCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); - return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken); - } - - private async Task WarnUserAsync(IUser executor, IUser target, string reason, IGuild guild, - GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) - { var interactionResult - = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", ct); + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", CancellationToken); if (!interactionResult.IsSuccess) { return ResultExtensions.FromError(interactionResult); @@ -107,13 +102,41 @@ public class WarnCommandGroup : CommandGroup var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) .WithColour(ColorsList.Red).Build(); - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); } + return await WarnPreparationAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken); + } + + private async Task WarnPreparationAsync(IUser executor, IUser target, string reason, IGuild guild, + GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) + { var memberData = data.GetOrCreateMemberData(target.ID); var warns = memberData.Warns; - var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings); + var settings = data.Settings; + + var warnThreshold = GuildSettings.WarnsThreshold.Get(settings); + var warnPunishment = GuildSettings.WarnPunishment.Get(settings); + var warnDuration = GuildSettings.WarnPunishmentDuration.Get(settings); + + if (warnPunishment is "off" or "disable" or "disabled" && warns.Count - 1 >= warnThreshold) + { + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.WarnPunishmentDisabled, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); + } + + if (warnPunishment is "ban" or "mute" && warnDuration == TimeSpan.Zero) + { + var errorEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.WarnPunishmentDurationNotSet, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); + } if (warns.Count >= warnThreshold && warnThreshold is not 0) { @@ -124,6 +147,14 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); } + return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings, + warns, warnThreshold, warnPunishment, warnDuration, ct); + } + + private async Task WarnUserAsync(IUser executor, IUser target, string reason, IGuild guild, + GuildData data, Snowflake channelId, IUser bot, JsonNode settings, IList warns, int warnThreshold, + string warnPunishment, TimeSpan warnDuration, CancellationToken ct = default) + { warns.Add(new Warn { WarnedBy = executor.ID.Value, @@ -153,66 +184,51 @@ public class WarnCommandGroup : CommandGroup await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct); } - _utility.LogAction( - data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); - - if (warns.Count >= warnThreshold && - GuildSettings.WarnPunishment.Get(data.Settings) is not "off" and not "disable" and not "disabled") - { - return await PunishUserAsync(target, guild, data, channelId, bot, warns, CancellationToken); - } + _utility.LogAction(settings, channelId, executor, title, description, + target, ColorsList.Yellow, false, ct); var embed = new EmbedBuilder().WithSmallTitle( title, target) .WithColour(ColorsList.Green).Build(); + if (warns.Count >= warnThreshold) + { + return await PunishUserAsync(target, guild, data, channelId, bot, warns, warnPunishment, warnDuration, CancellationToken); + } + return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } - private async Task PunishUserAsync(IUser target, IGuild guild, - GuildData data, Snowflake channelId, IUser bot, IList warns, CancellationToken ct) + private async Task PunishUserAsync(IUser target, IGuild guild, GuildData data, + Snowflake channelId, IUser bot, IList warns, string punishment, TimeSpan duration, CancellationToken ct) { - var settings = data.Settings; - var warnPunishment = GuildSettings.WarnPunishment.Get(settings); - var duration = GuildSettings.WarnPunishmentDuration.Get(settings); + warns.Clear(); - if (warnPunishment is "ban" && duration != TimeSpan.Zero) + if (punishment is "ban" && duration != TimeSpan.Zero) { - warns.Clear(); var banCommandGroup = new BanCommandGroup( _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await banCommandGroup.BanUserAsync(bot, target, Messages.ReceivedTooManyWarnings, duration, guild, data, channelId, bot, ct); } - if (warnPunishment is "kick") + if (punishment is "kick") { - warns.Clear(); var kickCommandGroup = new KickCommandGroup( _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await kickCommandGroup.KickUserAsync(bot, target, Messages.ReceivedTooManyWarnings, guild, channelId, data, bot, ct); } - if (warnPunishment is "mute" && duration != TimeSpan.Zero) + if (punishment is "mute" && duration != TimeSpan.Zero) { - warns.Clear(); var muteCommandGroup = new MuteCommandGroup( _access, _context, _feedback, _guildApi, _guildData, _userApi, _utility); await muteCommandGroup.MuteUserAsync(bot, target, Messages.ReceivedTooManyWarnings, duration, guild.ID, data, channelId, bot, ct); } - if (warnPunishment is not ("ban" or "mute") || duration != TimeSpan.Zero) - { - return Result.FromSuccess(); - } - - var errorEmbed = new EmbedBuilder() - .WithSmallTitle(Messages.WarnPunishmentDurationNotSet, bot) - .WithColour(ColorsList.Red).Build(); - - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + return Result.FromSuccess(); } [Command("unwarn")] @@ -256,6 +272,21 @@ public class WarnCommandGroup : CommandGroup var data = await _guildData.GetData(guild.ID, CancellationToken); Messages.Culture = GuildSettings.Language.Get(data.Settings); + var interactionResult + = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", CancellationToken); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); + } + if (number is not null) { return await RemoveUserWarnAsync(executor, target, reason, number.Value, guild, data, channelId, bot, @@ -268,21 +299,6 @@ public class WarnCommandGroup : CommandGroup private async Task RemoveUserWarnAsync(IUser executor, IUser target, string reason, int warnNumber, IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { - var interactionResult - = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); - if (!interactionResult.IsSuccess) - { - return ResultExtensions.FromError(interactionResult); - } - - if (interactionResult.Entity is not null) - { - var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) - .WithColour(ColorsList.Red).Build(); - - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); - } - var memberData = data.GetOrCreateMemberData(target.ID); var warns = memberData.Warns; @@ -340,21 +356,6 @@ public class WarnCommandGroup : CommandGroup private async Task RemoveUserWarnsAsync(IUser executor, IUser target, string reason, IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default) { - var interactionResult - = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", ct); - if (!interactionResult.IsSuccess) - { - return ResultExtensions.FromError(interactionResult); - } - - if (interactionResult.Entity is not null) - { - var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) - .WithColour(ColorsList.Red).Build(); - - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); - } - var memberData = data.GetOrCreateMemberData(target.ID); var warns = memberData.Warns; diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index cadb676..8141cb0 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1336,5 +1336,13 @@ namespace Octobot { return ResourceManager.GetString("WarnPunishmentDurationNotSet", resourceCulture); } } + + internal static string WarnPunishmentDisabled + { + get + { + return ResourceManager.GetString("WarnPunishmentDisabled", resourceCulture); + } + } } } From 5ff23722ce5b51a9c0323824184eef5584d7f89b Mon Sep 17 00:00:00 2001 From: mctaylors Date: Tue, 26 Mar 2024 22:08:57 +0300 Subject: [PATCH 06/33] ...and a few small touches Signed-off-by: mctaylors --- src/Commands/WarnCommandGroup.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index d7e6fc5..7ad1f80 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -191,7 +191,7 @@ public class WarnCommandGroup : CommandGroup title, target) .WithColour(ColorsList.Green).Build(); - if (warns.Count >= warnThreshold) + if (warns.Count >= warnThreshold && warnThreshold is not 0) { return await PunishUserAsync(target, guild, data, channelId, bot, warns, warnPunishment, warnDuration, CancellationToken); } @@ -202,8 +202,6 @@ public class WarnCommandGroup : CommandGroup private async Task PunishUserAsync(IUser target, IGuild guild, GuildData data, Snowflake channelId, IUser bot, IList warns, string punishment, TimeSpan duration, CancellationToken ct) { - warns.Clear(); - if (punishment is "ban" && duration != TimeSpan.Zero) { var banCommandGroup = new BanCommandGroup( @@ -228,6 +226,7 @@ public class WarnCommandGroup : CommandGroup duration, guild.ID, data, channelId, bot, ct); } + warns.Clear(); return Result.FromSuccess(); } From 422becf6c6338db4c5dec49592019e353df69299 Mon Sep 17 00:00:00 2001 From: mctaylors Date: Mon, 8 Apr 2024 15:55:23 +0300 Subject: [PATCH 07/33] and we're back to our PR Signed-off-by: mctaylors --- locale/Messages.resx | 4 ++-- locale/Messages.ru.resx | 4 ++-- src/Commands/WarnCommandGroup.cs | 16 +++++----------- src/Messages.Designer.cs | 4 ++-- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/locale/Messages.resx b/locale/Messages.resx index 9f771e3..191e4a1 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -795,7 +795,7 @@ Warn Punishment Duration is not set for the current punishment type. - - Warn Threshold is set, but the Warn Punishment is not. + + Increase the Warn Threshold or set a Warn Punishment. diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index 1dd0ffc..f3fb587 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -795,7 +795,7 @@ Длительность наказания предупреждения не установлена для текущего типа наказания. - - Порог предупреждения установлен, но наказание нет. + + Увеличьте порог предупреждения или установите наказание за предупреждение. diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index 7ad1f80..dea3102 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -120,10 +120,13 @@ public class WarnCommandGroup : CommandGroup var warnPunishment = GuildSettings.WarnPunishment.Get(settings); var warnDuration = GuildSettings.WarnPunishmentDuration.Get(settings); - if (warnPunishment is "off" or "disable" or "disabled" && warns.Count - 1 >= warnThreshold) + if (warnPunishment is "off" or "disable" or "disabled" + && warns.Count + 1 >= warnThreshold + && warnThreshold is not 0) { var errorEmbed = new EmbedBuilder() - .WithSmallTitle(Messages.WarnPunishmentDisabled, bot) + .WithSmallTitle(string.Format(Messages.WarnThresholdExceeded, warnThreshold), bot) + .WithDescription(Messages.WarnThresholdExceededDescription) .WithColour(ColorsList.Red).Build(); return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); @@ -138,15 +141,6 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); } - if (warns.Count >= warnThreshold && warnThreshold is not 0) - { - var errorEmbed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.WarnThresholdExceeded, warnThreshold), bot) - .WithColour(ColorsList.Red).Build(); - - return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); - } - return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings, warns, warnThreshold, warnPunishment, warnDuration, ct); } diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 8141cb0..19737fd 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1337,11 +1337,11 @@ namespace Octobot { } } - internal static string WarnPunishmentDisabled + internal static string WarnThresholdExceededDescription { get { - return ResourceManager.GetString("WarnPunishmentDisabled", resourceCulture); + return ResourceManager.GetString("WarnThresholdExceededDescription", resourceCulture); } } } From 1e9e1b4bb2d011f2bde6dec7c9b8fac7e3f2dbed Mon Sep 17 00:00:00 2001 From: mctaylors Date: Mon, 8 Apr 2024 16:38:03 +0300 Subject: [PATCH 08/33] Permission validation, PunishmentOption and that's it...? Signed-off-by: mctaylors --- locale/Messages.resx | 3 +++ locale/Messages.ru.resx | 3 +++ src/Commands/WarnCommandGroup.cs | 25 +++++++++++++++++++++++-- src/Data/GuildSettings.cs | 2 +- src/Data/Options/PunishmentOption.cs | 23 +++++++++++++++++++++++ src/Messages.Designer.cs | 8 ++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/Data/Options/PunishmentOption.cs diff --git a/locale/Messages.resx b/locale/Messages.resx index 191e4a1..037ad3f 100644 --- a/locale/Messages.resx +++ b/locale/Messages.resx @@ -798,4 +798,7 @@ Increase the Warn Threshold or set a Warn Punishment. + + Invalid Warn Punishment is set. + diff --git a/locale/Messages.ru.resx b/locale/Messages.ru.resx index f3fb587..68062d4 100644 --- a/locale/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -798,4 +798,7 @@ Увеличьте порог предупреждения или установите наказание за предупреждение. + + Установлено неверное наказание за предупреждение. + diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index dea3102..f4c250a 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -141,6 +141,28 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); } + if (warns.Count + 1 < warnThreshold) + { + return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings, + warns, warnThreshold, warnPunishment, warnDuration, ct); + } + + var interactionResult + = await _access.CheckInteractionsAsync(guild.ID, bot.ID, target.ID, + $"{char.ToUpperInvariant(warnPunishment[0])}{warnPunishment[1..]}", ct); + if (!interactionResult.IsSuccess) + { + return ResultExtensions.FromError(interactionResult); + } + + if (interactionResult.Entity is not null) + { + var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot) + .WithColour(ColorsList.Red).Build(); + + return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct); + } + return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings, warns, warnThreshold, warnPunishment, warnDuration, ct); } @@ -181,8 +203,7 @@ public class WarnCommandGroup : CommandGroup _utility.LogAction(settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct); - var embed = new EmbedBuilder().WithSmallTitle( - title, target) + var embed = new EmbedBuilder().WithSmallTitle(title, target) .WithColour(ColorsList.Green).Build(); if (warns.Count >= warnThreshold && warnThreshold is not 0) diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index 83d8d2d..fc616bd 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -12,7 +12,7 @@ public static class GuildSettings { public static readonly LanguageOption Language = new("Language", "en"); - public static readonly Option WarnPunishment = new("WarnPunishment", "disabled"); + public static readonly PunishmentOption WarnPunishment = new("WarnPunishment", "disabled"); /// /// Controls what message should be sent in when a new member joins the guild. diff --git a/src/Data/Options/PunishmentOption.cs b/src/Data/Options/PunishmentOption.cs new file mode 100644 index 0000000..f8d5414 --- /dev/null +++ b/src/Data/Options/PunishmentOption.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Nodes; +using Remora.Results; + +namespace Octobot.Data.Options; + +/// +public sealed class PunishmentOption : Option +{ + private static readonly List AllowedValues = + [ + "ban", "kick", "mute", "off", "disable", "disabled" + ]; + + public PunishmentOption(string name, string defaultValue) : base(name, defaultValue) { } + + /// + public override Result Set(JsonNode settings, string from) + { + return AllowedValues.Contains(from.ToLowerInvariant()) + ? base.Set(settings, from.ToLowerInvariant()) + : new ArgumentInvalidError(nameof(from), Messages.InvalidWarnPunishment); + } +} diff --git a/src/Messages.Designer.cs b/src/Messages.Designer.cs index 19737fd..8266e7f 100644 --- a/src/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -1344,5 +1344,13 @@ namespace Octobot { return ResourceManager.GetString("WarnThresholdExceededDescription", resourceCulture); } } + + internal static string InvalidWarnPunishment + { + get + { + return ResourceManager.GetString("InvalidWarnPunishment", resourceCulture); + } + } } } From 646c80af5986b06eb247965c42e011e1d3cf4b2e Mon Sep 17 00:00:00 2001 From: mctaylors Date: Mon, 8 Apr 2024 16:46:17 +0300 Subject: [PATCH 09/33] oops Signed-off-by: mctaylors --- src/Commands/WarnCommandGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/WarnCommandGroup.cs b/src/Commands/WarnCommandGroup.cs index f4c250a..8f4eb29 100644 --- a/src/Commands/WarnCommandGroup.cs +++ b/src/Commands/WarnCommandGroup.cs @@ -141,7 +141,7 @@ public class WarnCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken); } - if (warns.Count + 1 < warnThreshold) + if (warns.Count + 1 < warnThreshold || warnThreshold is 0) { return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings, warns, warnThreshold, warnPunishment, warnDuration, ct); From 2b0c4b62d3e8af97ff96215d6e6b05aa3f44b323 Mon Sep 17 00:00:00 2001 From: Fakeintxsh <95250141+mctaylors@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:28:06 +0300 Subject: [PATCH 10/33] README: Refer to the Octobot's Wiki in Building Octobot (#316) Signed-off-by: Fakeintxsh <95250141+mctaylors@users.noreply.github.com> --- docs/README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 7056857..ccc3b83 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,23 +15,16 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr * Reminding everyone about that new event you made * Renaming those annoying self-hoisting members * Log everything from joining the server to deleting messages -* Listen to music! +* Listen to Inkantation! *...a-a-and more!* ## 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/TeamOctolings/Octobot -cd Octobot -``` -4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. -``` -dotnet run BOT_TOKEN='ENTER_TOKEN_HERE' -``` +Check out the Octobot's Wiki for details. + +| [Windows](https://github.com/TeamOctolings/Octobot/wiki/Installing-Windows) | [Linux/macOS](https://github.com/TeamOctolings/Octobot/wiki/Installing-Unix) | +| --- | --- | ## Contributing From a953053f1d7ad7012f80d2bb338c7ee107e5bf9c Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 22 Jun 2024 00:28:29 +0500 Subject: [PATCH 11/33] Handle audit log entries for message deletion being empty (#317) In order to determine who deleted a message, Octobot fetches the audit log with a filter on the action "Message Delete", gets the latest entry and uses its author if the timestamps roughly match. However, if the filter returns no entries (as in, no message deletions are present in the audit log), `Single()` will throw an exception with the message `Sequence contains no elements`. To fix this, this PR replaces `Single()` with `SingleOrDefault()` and adds a null-check on `auditLog` in the form of a pattern access Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs index 88a8de2..f0e3d22 100644 --- a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs @@ -66,10 +66,10 @@ public sealed class MessageDeletedResponder : IResponder return ResultExtensions.FromError(auditLogResult); } - var auditLog = auditLogPage.AuditLogEntries.Single(); - var deleterResult = Result.FromSuccess(message.Author); - if (auditLog.UserID is not null + + var auditLog = auditLogPage.AuditLogEntries.SingleOrDefault(); + if (auditLog is { UserID: not null } && auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { From a0e7b3a6116c6ff44e1ea2877ffb33d35b931bf8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 25 Jun 2024 15:09:45 +0500 Subject: [PATCH 12/33] Always default cancellation tokens (#319) This PR makes sure that a cancellation token is never *required* to use an `async` method. This does not affect user experience in any way, only code quality. --------- Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/Commands/InfoCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/MuteCommandGroup.cs | 6 +++--- TeamOctolings.Octobot/Commands/RemindCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs | 6 +++--- .../Responders/GuildLoadedResponder.cs | 2 +- .../Responders/GuildMemberJoinedResponder.cs | 2 +- TeamOctolings.Octobot/Services/GuildDataService.cs | 4 ++-- .../Services/Update/MemberUpdateService.cs | 12 ++++++------ .../Services/Update/ScheduledEventUpdateService.cs | 12 ++++++------ TeamOctolings.Octobot/Utility.cs | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs index d7798f3..f07b210 100644 --- a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs @@ -288,7 +288,7 @@ public sealed class InfoCommandGroup : CommandGroup return await ShowGuildInfoAsync(bot, guild, CancellationToken); } - private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) + private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct = default) { var description = new StringBuilder().AppendLine($"## {guild.Name}"); diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index 282afe8..46e8d84 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -170,7 +170,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task SelectMuteMethodAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, - IUser bot, DateTimeOffset until, CancellationToken ct) + IUser bot, DateTimeOffset until, CancellationToken ct = default) { var muteRole = GuildSettings.MuteRole.Get(data.Settings); @@ -186,7 +186,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task RoleMuteUserAsync( IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, - DateTimeOffset until, Snowflake muteRole, CancellationToken ct) + DateTimeOffset until, Snowflake muteRole, CancellationToken ct = default) { var assignRoles = new List { muteRole }; var memberData = data.GetOrCreateMemberData(target.ID); @@ -208,7 +208,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task TimeoutUserAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, - IUser bot, DateTimeOffset until, CancellationToken ct) + IUser bot, DateTimeOffset until, CancellationToken ct = default) { if (duration.TotalDays >= 28) { diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index bf59d67..be53ed7 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -78,7 +78,7 @@ public sealed class RemindCommandGroup : CommandGroup return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken); } - private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct) + private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct = default) { if (data.Reminders.Count == 0) { @@ -353,7 +353,7 @@ public sealed class RemindCommandGroup : CommandGroup } private Task DeleteReminderAsync(MemberData data, int index, IUser bot, - CancellationToken ct) + CancellationToken ct = default) { if (index >= data.Reminders.Count) { diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs index b4c3488..2936392 100644 --- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs @@ -90,7 +90,7 @@ public sealed class ToolsCommandGroup : CommandGroup } private Task SendRandomNumberAsync(long first, long? secondNullable, - IUser executor, CancellationToken ct) + IUser executor, CancellationToken ct = default) { const long secondDefault = 0; var second = secondNullable ?? secondDefault; @@ -187,7 +187,7 @@ public sealed class ToolsCommandGroup : CommandGroup return await SendTimestampAsync(offset, executor, CancellationToken); } - private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) + private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct = default) { var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); @@ -249,7 +249,7 @@ public sealed class ToolsCommandGroup : CommandGroup return await AnswerEightBallAsync(bot, CancellationToken); } - private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) + private Task AnswerEightBallAsync(IUser bot, CancellationToken ct = default) { var typeNumber = Random.Shared.Next(0, 4); var embedColor = typeNumber switch diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index cebb1ea..b420db2 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -94,7 +94,7 @@ public sealed class GuildLoadedResponder : IResponder GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct); } - private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct) + private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct = default) { var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); if (!channelResult.IsDefined(out var channel)) diff --git a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs index c1f1da0..ae9f174 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs @@ -81,7 +81,7 @@ public sealed class GuildMemberJoinedResponder : IResponder } private async Task TryReturnRolesAsync( - JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct) + JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct = default) { if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs index 866ee08..a7af7c9 100644 --- a/TeamOctolings.Octobot/Services/GuildDataService.cs +++ b/TeamOctolings.Octobot/Services/GuildDataService.cs @@ -27,7 +27,7 @@ public sealed class GuildDataService : BackgroundService return SaveAsync(ct); } - private Task SaveAsync(CancellationToken ct) + private Task SaveAsync(CancellationToken ct = default) { var tasks = new List(); var datas = _datas.Values.ToArray(); @@ -44,7 +44,7 @@ public sealed class GuildDataService : BackgroundService return Task.WhenAll(tasks); } - private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct) + private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct = default) { var tempFilePath = path + ".tmp"; await using (var tempFileStream = File.Create(tempFilePath)) diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs index 51cf647..0c49c24 100644 --- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs @@ -62,7 +62,7 @@ public sealed partial class MemberUpdateService : BackgroundService } } - private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct) + private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct = default) { var guildData = await _guildData.GetData(guildId, ct); var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings); @@ -79,7 +79,7 @@ public sealed partial class MemberUpdateService : BackgroundService private async Task TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole, MemberData data, - CancellationToken ct) + CancellationToken ct = default) { var failedResults = new List(); var id = data.Id.ToSnowflake(); @@ -144,7 +144,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnbanAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) { if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { @@ -169,7 +169,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnmuteAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) { if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { @@ -188,7 +188,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, - CancellationToken ct) + CancellationToken ct = default) { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname @@ -226,7 +226,7 @@ public sealed partial class MemberUpdateService : BackgroundService private static partial Regex IllegalChars(); private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId, - CancellationToken ct) + CancellationToken ct = default) { if (DateTimeOffset.UtcNow < reminder.At) { diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs index ce9c212..ef145aa 100644 --- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs @@ -46,7 +46,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } } - private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct) + private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct = default) { var failedResults = new List(); var data = await _guildData.GetData(guildId, ct); @@ -133,7 +133,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task TickScheduledEventAsync( Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.AutoStartEvents.Get(data.Settings) && DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime @@ -160,7 +160,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task AutoStartEventAsync( - Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct) + Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default) { return (Result)await _eventApi.ModifyGuildScheduledEventAsync( guildId, scheduledEvent.ID, @@ -319,7 +319,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -351,7 +351,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct) + CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -405,7 +405,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendEarlyEventNotificationAsync( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { diff --git a/TeamOctolings.Octobot/Utility.cs b/TeamOctolings.Octobot/Utility.cs index 463212b..f337d93 100644 --- a/TeamOctolings.Octobot/Utility.cs +++ b/TeamOctolings.Octobot/Utility.cs @@ -125,7 +125,7 @@ public sealed class Utility } } - public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct) + public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct = default) { var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); if (!privateFeedback.Empty()) From 930b7ca6eda76a09883296d8657cf82a4e3e2d21 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 3 Jul 2024 22:09:39 +0500 Subject: [PATCH 13/33] Bump InspectCode from 1.11.10 to 1.11.12 (#323) I <3 breaking changes. --- .github/workflows/build-pr.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index c92386e..b2991db 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -22,8 +22,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.301' + - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.10 + uses: muno92/resharper_inspectcode@1.11.12 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 07e8784d2e10e0db5dd8b1645b95d674dfbaa1a6 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:12:32 +0500 Subject: [PATCH 14/33] Redesign reminder-related commands (#321) In this PR, I redesigned the reminder-related commands and they will now have a quote block instead of a inline code block (to avoid some visual bugs). Except /listremind. It just has the inline code block removed. ![image](https://github.com/TeamOctolings/Octobot/assets/95250141/3521af97-ee11-405f-8cc2-7bf9a747e757) --- TeamOctolings.Octobot/Commands/RemindCommandGroup.cs | 10 +++++----- .../Extensions/MarkdownExtensions.cs | 12 ++++++++++++ .../Services/Update/MemberUpdateService.cs | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs index be53ed7..3188d27 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs @@ -94,7 +94,7 @@ public sealed class RemindCommandGroup : CommandGroup { var reminder = data.Reminders[i]; builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) - .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendSubBulletPointLine(string.Format(Messages.ReminderText, reminder.Text)) .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))) .AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); } @@ -182,7 +182,7 @@ public sealed class RemindCommandGroup : CommandGroup }); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text))) + .AppendLine(MarkdownExtensions.Quote(text)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) @@ -279,7 +279,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text))) + .AppendLine(MarkdownExtensions.Quote(oldReminder.Text)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -309,7 +309,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value))) + .AppendLine(MarkdownExtensions.Quote(value)) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -367,7 +367,7 @@ public sealed class RemindCommandGroup : CommandGroup var reminder = data.Reminders[index]; var description = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) + .AppendLine(MarkdownExtensions.Quote(reminder.Text)) .AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); data.Reminders.RemoveAt(index); diff --git a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs index 202cd37..30ddff5 100644 --- a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs @@ -13,4 +13,16 @@ public static class MarkdownExtensions { return $"- {text}"; } + + /// + /// Formats a string to use Markdown Quote formatting. + /// + /// The input text to format. + /// + /// A markdown-formatted quote string. + /// + public static string Quote(string text) + { + return $"> {text}"; + } } diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs index 0c49c24..3170060 100644 --- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs @@ -234,7 +234,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var builder = new StringBuilder() - .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) + .AppendLine(MarkdownExtensions.Quote(reminder.Text)) .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); From d6d2660fb02b8eb3979f2acc16869d037c7d8b04 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:13:29 +0300 Subject: [PATCH 15/33] Show an error when entering the same value from the settings (#326) Closes #324 --- .../Commands/SettingsCommandGroup.cs | 21 +++++++++++++++++++ .../Data/Options/BoolOption.cs | 10 +++++++++ .../Data/Options/GuildOption.cs | 12 ++++++++++- .../Data/Options/IGuildOption.cs | 1 + .../Data/Options/LanguageOption.cs | 5 ++--- .../Data/Options/TimeSpanOption.cs | 10 +++++++++ TeamOctolings.Octobot/Messages.Designer.cs | 6 ++++++ TeamOctolings.Octobot/Messages.resx | 3 +++ TeamOctolings.Octobot/Messages.ru.resx | 3 +++ 9 files changed, 67 insertions(+), 4 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs index 0acaa88..15aa42b 100644 --- a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs @@ -202,6 +202,27 @@ public sealed class SettingsCommandGroup : CommandGroup IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, CancellationToken ct = default) { + var equalsResult = option.ValueEquals(data.Settings, value); + if (!equalsResult.IsSuccess) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) + .WithDescription(equalsResult.Error.Message) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + + if (equalsResult.Entity) + { + var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) + .WithDescription(Messages.SettingValueEquals) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); + } + var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { diff --git a/TeamOctolings.Octobot/Data/Options/BoolOption.cs b/TeamOctolings.Octobot/Data/Options/BoolOption.cs index 6a3c899..3b81abb 100644 --- a/TeamOctolings.Octobot/Data/Options/BoolOption.cs +++ b/TeamOctolings.Octobot/Data/Options/BoolOption.cs @@ -12,6 +12,16 @@ public sealed class BoolOption : GuildOption return Get(settings) ? Messages.Yes : Messages.No; } + public override Result ValueEquals(JsonNode settings, string value) + { + if (!TryParseBool(value, out var boolean)) + { + return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); + } + + return Value(settings).Equals(boolean.ToString()); + } + public override Result Set(JsonNode settings, string from) { if (!TryParseBool(from, out var value)) diff --git a/TeamOctolings.Octobot/Data/Options/GuildOption.cs b/TeamOctolings.Octobot/Data/Options/GuildOption.cs index 5d9f1a2..ea9c30e 100644 --- a/TeamOctolings.Octobot/Data/Options/GuildOption.cs +++ b/TeamOctolings.Octobot/Data/Options/GuildOption.cs @@ -21,9 +21,19 @@ public class GuildOption : IGuildOption public string Name { get; } + protected virtual string Value(JsonNode settings) + { + return Get(settings).ToString() ?? throw new InvalidOperationException(); + } + public virtual string Display(JsonNode settings) { - return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException()); + return Markdown.InlineCode(Value(settings)); + } + + public virtual Result ValueEquals(JsonNode settings, string value) + { + return Value(settings).Equals(value); } /// diff --git a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs index a8c3e6e..9920281 100644 --- a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs +++ b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs @@ -7,6 +7,7 @@ public interface IGuildOption { string Name { get; } string Display(JsonNode settings); + Result ValueEquals(JsonNode settings, string value); Result Set(JsonNode settings, string from); Result Reset(JsonNode settings); } diff --git a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs index 15ab6ff..f58e011 100644 --- a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs +++ b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Text.Json.Nodes; -using Remora.Discord.Extensions.Formatting; using Remora.Results; namespace TeamOctolings.Octobot.Data.Options; @@ -16,9 +15,9 @@ public sealed class LanguageOption : GuildOption public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } - public override string Display(JsonNode settings) + protected override string Value(JsonNode settings) { - return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); + return settings[Name]?.GetValue() ?? "en"; } /// diff --git a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs index 3501f09..7e21343 100644 --- a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs +++ b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs @@ -8,6 +8,16 @@ public sealed class TimeSpanOption : GuildOption { public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } + public override Result ValueEquals(JsonNode settings, string value) + { + if (!TimeSpanParser.TryParse(value).IsDefined(out var span)) + { + return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); + } + + return Value(settings).Equals(span.ToString()); + } + public override TimeSpan Get(JsonNode settings) { var property = settings[Name]; diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs index bbc1366..ce59f1e 100644 --- a/TeamOctolings.Octobot/Messages.Designer.cs +++ b/TeamOctolings.Octobot/Messages.Designer.cs @@ -1196,5 +1196,11 @@ namespace TeamOctolings.Octobot { return ResourceManager.GetString("SettingsModeratorRole", resourceCulture); } } + + internal static string SettingValueEquals { + get { + return ResourceManager.GetString("SettingValueEquals", resourceCulture); + } + } } } diff --git a/TeamOctolings.Octobot/Messages.resx b/TeamOctolings.Octobot/Messages.resx index 47e7d4f..059584a 100644 --- a/TeamOctolings.Octobot/Messages.resx +++ b/TeamOctolings.Octobot/Messages.resx @@ -681,4 +681,7 @@ Moderator role + + The setting value is the same as the input value. + diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx index 2eef257..fc8a594 100644 --- a/TeamOctolings.Octobot/Messages.ru.resx +++ b/TeamOctolings.Octobot/Messages.ru.resx @@ -681,4 +681,7 @@ Роль модератора + + Значение настройки такое же, как и вводное значение. + From e457b4609ed7630ac8920cea90c9e1282b4914ca Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Wed, 31 Jul 2024 23:57:21 +0500 Subject: [PATCH 16/33] Don't log stack traces for cancelled operations (#327) This PR fixes an issue where the `LogResultStackTrace` method would log stack traces for results that encountered an error due to a cancelled operation/task. The `LoggerExtensions` class already skipped `TaskCanceledException`s, but didn't skip `OperationCanceledException`s (which is a parent of `TaskCanceledException`). The patch specifically does not affect *inner* results which are canceled. Skipping logging these could hide the true cause of an error which appears important --- TeamOctolings.Octobot/Extensions/LoggerExtensions.cs | 2 +- TeamOctolings.Octobot/Extensions/ResultExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs index 76fa386..fac4dda 100644 --- a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs @@ -25,7 +25,7 @@ public static class LoggerExtensions if (result.Error is ExceptionError exe) { - if (exe.Exception is TaskCanceledException) + if (exe.Exception is OperationCanceledException) { return; } diff --git a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs index d7ef7d7..6872d34 100644 --- a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs @@ -23,7 +23,7 @@ public static class ResultExtensions private static void LogResultStackTrace(Result result) { - if (result.IsSuccess) + if (result.IsSuccess || result.Error is ExceptionError { Exception: OperationCanceledException }) { return; } From d1133124b8eaddb7f9a6891ef9bd678de0a046f0 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 24 Aug 2024 20:48:47 +0500 Subject: [PATCH 17/33] Apply new inspection fixes (#329) Rider and R# 2024.2 have introduced new inspections, causing current builds to fail. This PR applies fixes for issues caught by these inspections. --- TeamOctolings.Octobot/Commands/AboutCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/BanCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Commands/ClearCommandGroup.cs | 2 +- .../Commands/Events/ErrorLoggingPostExecutionEvent.cs | 2 +- TeamOctolings.Octobot/Commands/KickCommandGroup.cs | 2 +- TeamOctolings.Octobot/Commands/MuteCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Data/Reminder.cs | 10 +++++----- .../Responders/GuildLoadedResponder.cs | 2 +- .../Services/Update/ScheduledEventUpdateService.cs | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index dbb8b12..4575259 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -131,7 +131,7 @@ public sealed class AboutCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton }) + new ActionRowComponent([repositoryButton, wikiButton, issuesButton]) }), ct); } } diff --git a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs index 69be80f..1d6b26c 100644 --- a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs @@ -62,7 +62,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was banned and vice-versa. + /// was banned and vice versa. /// /// [Command("ban", "бан")] @@ -219,7 +219,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was unbanned and vice-versa. + /// was unbanned and vice versa. /// /// /// diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs index 7c1b516..7f29581 100644 --- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs @@ -51,7 +51,7 @@ public sealed class ClearCommandGroup : CommandGroup /// The user whose messages will be cleared. /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages - /// were cleared and vice-versa. + /// were cleared and vice versa. /// [Command("clear", "очистить")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] diff --git a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs index 7409d3b..ff7339f 100644 --- a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -81,7 +81,7 @@ public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent(new[] { issuesButton }) + new ActionRowComponent([issuesButton]) }), ct) ); } diff --git a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs index a8fea2a..3011375 100644 --- a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs @@ -57,7 +57,7 @@ public sealed class KickCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was kicked and vice-versa. + /// was kicked and vice versa. /// [Command("kick", "кик")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs index 46e8d84..5dce0b6 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs @@ -59,7 +59,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was muted and vice-versa. + /// was muted and vice versa. /// /// [Command("mute", "мут")] @@ -235,7 +235,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was unmuted and vice-versa. + /// was unmuted and vice versa. /// /// /// diff --git a/TeamOctolings.Octobot/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs index c3936da..40f29e1 100644 --- a/TeamOctolings.Octobot/Data/Reminder.cs +++ b/TeamOctolings.Octobot/Data/Reminder.cs @@ -1,9 +1,9 @@ namespace TeamOctolings.Octobot.Data; -public struct Reminder +public sealed record Reminder { - public DateTimeOffset At { get; init; } - public string Text { get; init; } - public ulong ChannelId { get; init; } - public ulong MessageId { get; init; } + public required DateTimeOffset At { get; init; } + public required string Text { get; init; } + public required ulong ChannelId { get; init; } + public required ulong MessageId { get; init; } } diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs index b420db2..b24ef0b 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs @@ -120,6 +120,6 @@ public sealed class GuildLoadedResponder : IResponder ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, - components: new[] { new ActionRowComponent(new[] { issuesButton }) }, ct: ct); + components: new[] { new ActionRowComponent([issuesButton]) }, ct: ct); } } diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs index ef145aa..389a6a8 100644 --- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs @@ -229,7 +229,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(settings), roleMention, embedResult: embed, - components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); + components: new[] { new ActionRowComponent([button]) }, ct: ct); } private static Result GetExternalScheduledEventCreatedEmbedDescription( From afd0141c13808dd4f06fba9103b2aaebcb1e062a Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:50:32 +0300 Subject: [PATCH 18/33] /about: Replace repo link with website link (#328) A some sort of UX change. Repository link will be still accessible from the website. --- TeamOctolings.Octobot/BuildInfo.cs | 4 +++- TeamOctolings.Octobot/Commands/AboutCommandGroup.cs | 4 ++-- TeamOctolings.Octobot/Messages.Designer.cs | 4 ++-- TeamOctolings.Octobot/Messages.resx | 4 ++-- TeamOctolings.Octobot/Messages.ru.resx | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/TeamOctolings.Octobot/BuildInfo.cs b/TeamOctolings.Octobot/BuildInfo.cs index 4b9a09f..a91e7f3 100644 --- a/TeamOctolings.Octobot/BuildInfo.cs +++ b/TeamOctolings.Octobot/BuildInfo.cs @@ -2,7 +2,9 @@ public static class BuildInfo { - public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; + public const string WebsiteUrl = "https://teamoctolings.github.io/Octobot"; + + private const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs index 4575259..28caccf 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs @@ -106,9 +106,9 @@ public sealed class AboutCommandGroup : CommandGroup var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonOpenRepository, + Messages.ButtonOpenWebsite, new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310) - URL: BuildInfo.RepositoryUrl + URL: BuildInfo.WebsiteUrl ); var wikiButton = new ButtonComponent( diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs index ce59f1e..1a81e02 100644 --- a/TeamOctolings.Octobot/Messages.Designer.cs +++ b/TeamOctolings.Octobot/Messages.Designer.cs @@ -633,9 +633,9 @@ namespace TeamOctolings.Octobot { } } - internal static string ButtonOpenRepository { + internal static string ButtonOpenWebsite { get { - return ResourceManager.GetString("ButtonOpenRepository", resourceCulture); + return ResourceManager.GetString("ButtonOpenWebsite", resourceCulture); } } diff --git a/TeamOctolings.Octobot/Messages.resx b/TeamOctolings.Octobot/Messages.resx index 059584a..e4107fb 100644 --- a/TeamOctolings.Octobot/Messages.resx +++ b/TeamOctolings.Octobot/Messages.resx @@ -399,8 +399,8 @@ Developers: - - Octobot's source code + + Open Website About {0} diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx index fc8a594..d942cec 100644 --- a/TeamOctolings.Octobot/Messages.ru.resx +++ b/TeamOctolings.Octobot/Messages.ru.resx @@ -399,8 +399,8 @@ Разработчики: - - Исходный код Octobot + + Открыть веб-сайт О боте {0} From 0fc53990b96cf9c901790c8c936a1e70d7e0ac37 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Sun, 25 Aug 2024 10:11:54 +0300 Subject: [PATCH 19/33] =?UTF-8?q?Update=20song=20list=20with=20new=20Splat?= =?UTF-8?q?oon=E2=84=A2=20soundtracks=20(#266)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes in the list: 1. Fly Octo Fly ~ Ebb & Flow (Octo) / Off the Hook → Spectrum Obligato ~ Ebb & Flow (Out of Order) / Off the Hook feat. Dedf1sh 2. `#47` onward / Dedf1sh feat. Off the Hook 3. EchΘ Θnslaught / Free Association 4. Short Order / Off the Hook 5. Fins in the Air / Deep Cut Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com> --- TeamOctolings.Octobot/Services/Update/SongUpdateService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs index b07256f..d0b46ae 100644 --- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs @@ -29,7 +29,11 @@ public sealed class SongUpdateService : BackgroundService ("Callie", "Bomb Rush Blush", new TimeSpan(0, 2, 18)), ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), - ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) + ("Off the Hook feat. Dedf1sh", "Spectrum Obligato ~ Ebb & Flow (Out of Order)", new TimeSpan(0, 4, 30)), + ("Dedf1sh feat. Off the Hook", "#47 onward", new TimeSpan(0, 4, 40)), + ("Free Association", "EchΘ Θnslaught", new TimeSpan(0, 2, 52)), + ("Off the Hook", "Short Order", new TimeSpan(0, 3, 36)), + ("Deep Cut", "Fins in the Air", new TimeSpan(0, 3, 1)) ]; private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList = From 086cb672f001d3c194311371b9f16ea796201974 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 25 Aug 2024 16:03:31 +0500 Subject: [PATCH 20/33] Fix wrong private key file name (#330) The key used for deploy is Ed25519, not RSA Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index a757eb2..1b63a9d 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -27,8 +27,8 @@ jobs: - name: Setup SSH key run: | - install -m 600 -D /dev/null ~/.ssh/id_rsa - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + install -m 600 -D /dev/null ~/.ssh/id_ed25519 + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts shell: bash env: From 8028d47ba1b5de1d0dd5eb9c0eb52e90a6f70d51 Mon Sep 17 00:00:00 2001 From: Macintxsh <95250141+mctaylors@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:55:58 +0300 Subject: [PATCH 21/33] Fix redundant type specification (#333) This PR fixes failing checks for #332 --- TeamOctolings.Octobot/Services/Update/SongUpdateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs index d0b46ae..8eaa4c2 100644 --- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs +++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs @@ -41,7 +41,7 @@ public sealed class SongUpdateService : BackgroundService ("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47)) ]; - private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; + private readonly List _activityList = [new("with Remora.Discord", ActivityType.Game)]; private readonly DiscordGatewayClient _client; private readonly GuildDataService _guildData; From 450f1da54df36611894ce23876d9bc698efdf8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:59:32 +0000 Subject: [PATCH 22/33] Bump muno92/resharper_inspectcode from 1.11.12 to 1.12.0 (#332) --- .github/workflows/build-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index b2991db..2260918 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '8.0.301' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.11.12 + uses: muno92/resharper_inspectcode@1.12.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor From 9e44d49039792c2b94a7a87745780b108bb22a67 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Thu, 3 Oct 2024 23:18:25 +0500 Subject: [PATCH 23/33] Update dependencies (#334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this is supposed to be Dependabot's job, but ig something happened to it Bumps `JetBrains.Annotations` from `2023.3.0` to `2024.2.0`
Changelog • Added DefaultEqualityUsageAttribute for equality members usage analysis. • MustDisposeResourceAttribute is now allowed on struct types. • Added ability to specify the description for UsedImplicitlyAttribute (new 'Reason' property). • Added copyright information to nuspec.
Bumps `Remora.Commands` from `10.0.5` to `10.0.6`
Changelog Upgrade Remora.Sdk. Upgrade nuget packages.
Bumps `Remora.Discord.Extensions` from `5.3.5` to `5.3.6` Bumps `Remora.Discord.Interactivity` from `4.5.4` to `5.0.0`
Changelog Update dependencies. BREAKING: Rework deletion logic for data leases to prevent deadlocks.
The breaking change in Remora should not affect us. Signed-off-by: Octol1ttle --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 19e37f9..5c6da6d 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -24,14 +24,14 @@ - + - + - + - + From 84dc248038241c9783dfa7b034d20d505e5f2f30 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 27 Oct 2024 21:34:01 +0500 Subject: [PATCH 24/33] Add Dockerfile and compose.yaml (#335) This PR adds Dockerfile, to run Octobot within a Docker container, and compose.yaml, to run the Octobot container along with any services that the user may require. --------- Signed-off-by: Octol1ttle Signed-off-by: mctaylors Co-authored-by: mctaylors --- .github/workflows/build-push.yml | 50 ++++++++++++++++++++------------ .gitignore | 1 + Dockerfile | 15 ++++++++++ compose.example.yaml | 17 +++++++++++ 4 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 Dockerfile create mode 100644 compose.example.yaml diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 1b63a9d..ae355cd 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -8,29 +8,43 @@ on: branches: [ "master" ] jobs: - upload-solution: - name: Upload Octobot to production + upload-image: + name: Upload Octobot Docker image runs-on: ubuntu-latest permissions: - actions: read - contents: read + packages: write environment: production steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Publish solution - run: dotnet publish $PUBLISH_FLAGS - env: - PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}} + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + push: true + tags: ghcr.io/${{vars.NAMESPACE}}/${{vars.IMAGE_NAME}}:latest + build-args: | + BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 + PUBLISH_OPTIONS=${{vars.PUBLISH_OPTIONS}} + update-production: + name: Update Octobot on production + runs-on: ubuntu-latest + environment: production + needs: upload-image + + steps: - name: Setup SSH key run: | install -m 600 -D /dev/null ~/.ssh/id_ed25519 echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts - shell: bash + shell: bash {0} env: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} SSH_HOST: ${{secrets.SSH_HOST}} @@ -38,26 +52,26 @@ jobs: - name: Stop currently running instance run: | ssh $SSH_USER@$SSH_HOST $STOP_COMMAND - shell: bash + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} STOP_COMMAND: ${{vars.STOP_COMMAND}} - - name: Upload published solution + - name: Update Docker image run: | - scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO - shell: bash + ssh $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} - UPLOAD_FROM: ${{vars.UPLOAD_FROM}} - UPLOAD_TO: ${{vars.UPLOAD_TO}} + NAMESPACE: ${{vars.NAMESPACE}} + IMAGE_NAME: ${{vars.IMAGE_NAME}} - name: Start new instance run: | ssh $SSH_USER@$SSH_HOST $START_COMMAND - shell: bash + shell: bash {0} env: SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} diff --git a/.gitignore b/.gitignore index f97f6b8..fcda727 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ riderModule.iml /.vs/ GuildData/ Logs/ +compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ef831a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env +WORKDIR /Octobot + +# Copy everything +COPY . ./ +# Load build argument with publish options +ARG PUBLISH_OPTIONS="-c Release" +# Build and publish a release +RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:a335dccd3231f7f9e2122691b21c634f96e187d3840c8b7dbad61ee09500e408 +WORKDIR /Octobot +COPY --from=build-env /Octobot/out . +ENTRYPOINT ["./TeamOctolings.Octobot"] diff --git a/compose.example.yaml b/compose.example.yaml new file mode 100644 index 0000000..522281f --- /dev/null +++ b/compose.example.yaml @@ -0,0 +1,17 @@ +services: + octobot: + container_name: octobot + build: + context: . + args: + - PUBLISH_OPTIONS + environment: + - BOT_TOKEN + volumes: + - guild-data:/Octobot/GuildData + - logs:/Octobot/Logs + restart: unless-stopped + +volumes: + guild-data: + logs: From 8a42ecd1ab30172399ffd4c4f9190f75592282f8 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 28 Oct 2024 09:51:24 +0500 Subject: [PATCH 25/33] Retrieve SSH port from environment secrets (#337) Fixes current deployment failure due to use of non-standard SSH port on our production host. `ssh-keyscan` command was moved out to its own step to help troubleshooting in the future. Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index ae355cd..af3c4cb 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -39,30 +39,38 @@ jobs: needs: upload-image steps: - - name: Setup SSH key + - name: Copy SSH key run: | install -m 600 -D /dev/null ~/.ssh/id_ed25519 echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 - ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts - shell: bash {0} + shell: bash env: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} + + - name: Generate SSH known hosts file + run: | + ssh-keyscan -H $SSH_HOST -p $SSH_PORT > ~/.ssh/known_hosts + shell: bash + env: SSH_HOST: ${{secrets.SSH_HOST}} - + SSH_PORT: ${{secrets.SSH_PORT}} + - name: Stop currently running instance run: | - ssh $SSH_USER@$SSH_HOST $STOP_COMMAND - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $STOP_COMMAND + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} STOP_COMMAND: ${{vars.STOP_COMMAND}} - name: Update Docker image run: | - ssh $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} NAMESPACE: ${{vars.NAMESPACE}} @@ -70,9 +78,10 @@ jobs: - name: Start new instance run: | - ssh $SSH_USER@$SSH_HOST $START_COMMAND - shell: bash {0} + ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $START_COMMAND + shell: bash env: + SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} START_COMMAND: ${{vars.START_COMMAND}} From bfee88914951dce168f9e4f02797b60932d60814 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 28 Oct 2024 10:04:08 +0500 Subject: [PATCH 26/33] ssh-keyscan is a dumb command (#338) Signed-off-by: Octol1ttle --- .github/workflows/build-push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index af3c4cb..e7afe8e 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -5,7 +5,7 @@ concurrency: on: push: - branches: [ "master" ] + branches: [ "master", "deploy-test" ] jobs: upload-image: @@ -49,7 +49,7 @@ jobs: - name: Generate SSH known hosts file run: | - ssh-keyscan -H $SSH_HOST -p $SSH_PORT > ~/.ssh/known_hosts + ssh-keyscan -H -p $SSH_PORT $SSH_HOST > ~/.ssh/known_hosts shell: bash env: SSH_HOST: ${{secrets.SSH_HOST}} From 9fc97bc9089239d1559f1bfae55dba1ac591075f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:35:06 +0000 Subject: [PATCH 27/33] Bump GitInfo from 3.3.5 to 3.5.0 (#340) --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 5c6da6d..a8bcbc5 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -22,7 +22,7 @@ - + From ed298202fc07d98d07b2b571287c7aa3dc7f5bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:41:36 +0000 Subject: [PATCH 28/33] Bump JetBrains.Annotations from 2024.2.0 to 2024.3.0 (#339) --- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index a8bcbc5..ec09b3d 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -24,7 +24,7 @@ - + From 5c235b9f981714513af76b9e219540979a4afd1a Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 1 Dec 2024 21:48:25 +0500 Subject: [PATCH 29/33] Downgrade GitInfo from 3.5.0 to 3.3.5 (#343) This pull request downgrades GitInfo from 3.5.0 to 3.3.5 due to a decision from the maintainers to lock features behind a paywall. ![image](https://github.com/user-attachments/assets/f90eba84-1a1e-43eb-950e-e233a02feb9a) --- .github/dependabot.yml | 1 + TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 57eea90..b961b81 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,5 +36,6 @@ updates: - "Remora.Discord.*" # For all packages, ignore all patch updates ignore: + - dependency-name: "GitInfo" - dependency-name: "*" update-types: [ "version-update:semver-patch" ] diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index ec09b3d..858d9d2 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -22,7 +22,7 @@ - + From bf818401d829e54b3dbcdc630d27bad9f80e6c25 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sun, 1 Dec 2024 21:52:47 +0500 Subject: [PATCH 30/33] Bump TargetFramework from 8.0 to 9.0 (#344) Simple and self-explanatory. Closes #342 --------- Signed-off-by: Octol1ttle --- .github/workflows/build-pr.yml | 4 ++-- Dockerfile | 4 ++-- TeamOctolings.Octobot/TeamOctolings.Octobot.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 2260918..84ec975 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -25,10 +25,10 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.301' + dotnet-version: '9.0.x' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.12.0 + uses: muno92/resharper_inspectcode@1.12.3 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/Dockerfile b/Dockerfile index 0ef831a..6cfeac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:7d24e90a392e88eb56093e4eb325ff883ad609382a55d42f17fd557b997022ca AS build-env WORKDIR /Octobot # Copy everything @@ -9,7 +9,7 @@ ARG PUBLISH_OPTIONS="-c Release" RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out # Build runtime image -FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:a335dccd3231f7f9e2122691b21c634f96e187d3840c8b7dbad61ee09500e408 +FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:1e5eb0ed94ca96a34a914456db80e48bd1bb7bc3e3c8eda5e2c3d89c153c3081 WORKDIR /Octobot COPY --from=build-env /Octobot/out . ENTRYPOINT ["./TeamOctolings.Octobot"] diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 858d9d2..418803a 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable 2.0.0 @@ -26,7 +26,7 @@ - + From 4785d162a23653f7ed122248c4e2a5a39fc1e75e Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Mon, 3 Feb 2025 16:58:57 +0500 Subject: [PATCH 31/33] Handle temporary files being present when loading guild data (#345) This PR fixes catastrophic guild data loading errors that appear when there are lingering temporary files. In normal operation, temporary files are deleted as soon as they are copied to the main file. It is also expected that temporary files are valid JSON files. However, due to a yesterday's DoS attack, something:tm: happened and a bunch of empty temporary files got written to disk. When Octobot recovered from the attack, it was unable to load any guild data because of the temporary files. This PR addresses this issue by changing the data loading logic: 1) Check if there's a temporary file. If it exists, try loading it. 2) If it is successfully loaded, move the temp file to the main file and resume operation as normal 3) If it could not be loaded, try loading the main file 4) If it is successfully loaded, delete the temporary file and resume operation as normal 5) If it is not, throw an error (like before) This PR was tested on production data and managed to load every guild without errors. Signed-off-by: Octol1ttle --- .../GuildScheduledEventExtensions.cs | 2 +- .../Services/GuildDataService.cs | 189 +++++++++++++----- 2 files changed, 144 insertions(+), 47 deletions(-) diff --git a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs index b8eb2d1..7822d9b 100644 --- a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs +++ b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs @@ -10,7 +10,7 @@ public static class GuildScheduledEventExtensions out string? location) { endTime = default; - location = default; + location = null; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs index a7af7c9..88edb5f 100644 --- a/TeamOctolings.Octobot/Services/GuildDataService.cs +++ b/TeamOctolings.Octobot/Services/GuildDataService.cs @@ -75,78 +75,48 @@ public sealed class GuildDataService : BackgroundService { var path = $"GuildData/{guildId}"; var memberDataPath = $"{path}/MemberData"; + var settingsPath = $"{path}/Settings.json"; + var scheduledEventsPath = $"{path}/ScheduledEvents.json"; MigrateDataDirectory(guildId, path); Directory.CreateDirectory(path); - if (!File.Exists(settingsPath)) - { - await File.WriteAllTextAsync(settingsPath, "{}", ct); - } - - if (!File.Exists(scheduledEventsPath)) - { - await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); - } - var dataLoadFailed = false; - await using var settingsStream = File.OpenRead(settingsPath); - JsonNode? jsonSettings = null; - try - { - jsonSettings = await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); - dataLoadFailed = true; - } - + var jsonSettings = await LoadGuildSettings(settingsPath, ct); if (jsonSettings is not null) { FixJsonSettings(jsonSettings); } - - await using var eventsStream = File.OpenRead(scheduledEventsPath); - Dictionary? events = null; - try + else { - events = await JsonSerializer.DeserializeAsync>( - eventsStream, cancellationToken: ct); + dataLoadFailed = true; } - catch (Exception e) + + var events = await LoadScheduledEvents(scheduledEventsPath, ct); + if (events is null) { - _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); dataLoadFailed = true; } var memberData = new Dictionary(); - foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) + foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles() + .Where(dataFileInfo => + !memberData.ContainsKey( + ulong.Parse(dataFileInfo.Name.Replace(".json", "").Replace(".tmp", ""))))) { - await using var dataStream = dataFileInfo.OpenRead(); - MemberData? data; - try + var data = await LoadMemberData(dataFileInfo, memberDataPath, true, ct); + + if (data == null) { - data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, - dataFileInfo.Name); dataLoadFailed = true; continue; } - if (data is null) - { - continue; - } - - memberData.Add(data.Id, data); + memberData.TryAdd(data.Id, data); } var finalData = new GuildData( @@ -160,6 +130,133 @@ public sealed class GuildDataService : BackgroundService return finalData; } + private async Task LoadMemberData(FileInfo dataFileInfo, string memberDataPath, bool loadTmp, + CancellationToken ct = default) + { + MemberData? data; + var temporaryPath = $"{dataFileInfo.FullName}.tmp"; + var usedInfo = loadTmp && File.Exists(temporaryPath) ? new FileInfo(temporaryPath) : dataFileInfo; + + var isTmp = usedInfo.Extension is ".tmp"; + try + { + await using var dataStream = usedInfo.OpenRead(); + data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + if (isTmp) + { + usedInfo.CopyTo(usedInfo.FullName.Replace(".tmp", ""), true); + usedInfo.Delete(); + } + } + catch (Exception e) + { + if (isTmp) + { + _logger.LogWarning(e, + "Unable to load temporary member data file, deleting: {MemberDataPath}/{FileName}", memberDataPath, + usedInfo.Name); + usedInfo.Delete(); + return await LoadMemberData(dataFileInfo, memberDataPath, false, ct); + } + + _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, + usedInfo.Name); + return null; + } + + return data; + } + + private async Task?> LoadScheduledEvents(string scheduledEventsPath, + CancellationToken ct = default) + { + var tempScheduledEventsPath = $"{scheduledEventsPath}.tmp"; + + if (!File.Exists(scheduledEventsPath) && !File.Exists(tempScheduledEventsPath)) + { + return new Dictionary(); + } + + if (File.Exists(tempScheduledEventsPath)) + { + _logger.LogWarning("Found temporary scheduled events file, will try to parse and copy to main: ${Path}", + tempScheduledEventsPath); + try + { + await using var tempEventsStream = File.OpenRead(tempScheduledEventsPath); + var events = await JsonSerializer.DeserializeAsync>( + tempEventsStream, cancellationToken: ct); + File.Copy(tempScheduledEventsPath, scheduledEventsPath, true); + File.Delete(tempScheduledEventsPath); + + _logger.LogInformation("Successfully loaded temporary scheduled events file: ${Path}", + tempScheduledEventsPath); + return events; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load temporary scheduled events file: {Path}, deleting", + tempScheduledEventsPath); + File.Delete(tempScheduledEventsPath); + } + } + + try + { + await using var eventsStream = File.OpenRead(scheduledEventsPath); + return await JsonSerializer.DeserializeAsync>( + eventsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); + return null; + } + } + + private async Task LoadGuildSettings(string settingsPath, CancellationToken ct = default) + { + var tempSettingsPath = $"{settingsPath}.tmp"; + + if (!File.Exists(settingsPath) && !File.Exists(tempSettingsPath)) + { + return new JsonObject(); + } + + if (File.Exists(tempSettingsPath)) + { + _logger.LogWarning("Found temporary settings file, will try to parse and copy to main: ${Path}", + tempSettingsPath); + try + { + await using var tempSettingsStream = File.OpenRead(tempSettingsPath); + var jsonSettings = await JsonNode.ParseAsync(tempSettingsStream, cancellationToken: ct); + + File.Copy(tempSettingsPath, settingsPath, true); + File.Delete(tempSettingsPath); + + _logger.LogInformation("Successfully loaded temporary settings file: ${Path}", tempSettingsPath); + return jsonSettings; + } + catch (Exception e) + { + _logger.LogError(e, "Unable to load temporary settings file: {Path}, deleting", tempSettingsPath); + File.Delete(tempSettingsPath); + } + } + + try + { + await using var settingsStream = File.OpenRead(settingsPath); + return await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); + return null; + } + } + private void MigrateDataDirectory(Snowflake guildId, string newPath) { var oldPath = $"{guildId}"; From f3330c47cc7958e09d27d80124c45476069d3162 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:50:08 +0500 Subject: [PATCH 32/33] Bump Remora.Discord with 5 updates (#346) Bumps the remora group with 5 updates: | Package | From | To | | --- | --- | --- | | [Remora.Discord.Caching](https://github.com/Remora/Remora.Discord) | `39.0.0` | `40.0.0` | | [Remora.Commands](https://github.com/Remora/Remora.Commands) | `10.0.6` | `11.0.1` | | [Remora.Discord.Extensions](https://github.com/Remora/Remora.Discord) | `5.3.6` | `6.0.0` | | [Remora.Discord.Interactivity](https://github.com/Remora/Remora.Discord) | `5.0.0` | `6.0.0` | | [Remora.Discord.Hosting](https://github.com/Remora/Remora.Discord) | `6.0.10` | `7.0.0` | Signed-off-by: dependabot[bot] Signed-off-by: Octol1ttle Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Octol1ttle --- .../Responders/MessageEditedResponder.cs | 31 ++++++------------- .../TeamOctolings.Octobot.csproj | 10 +++--- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs index 2968562..e3d1c58 100644 --- a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs +++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs @@ -36,40 +36,29 @@ public sealed class MessageEditedResponder : IResponder public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.ID.IsDefined(out var messageId)) - { - return new ArgumentNullError(nameof(gatewayEvent.ID)); - } - - if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) - { - return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); - } - if (!gatewayEvent.GuildID.IsDefined(out var guildId) - || !gatewayEvent.Author.IsDefined(out var author) - || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp) - || !gatewayEvent.Content.IsDefined(out var newContent)) + || !gatewayEvent.EditedTimestamp.HasValue + || gatewayEvent.Author.IsBot.OrDefault(false)) { return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); - if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { return Result.Success; } - var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); + var cacheKey = new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID); var messageResult = await _cacheService.TryGetValueAsync( cacheKey, ct); if (!messageResult.IsDefined(out var message)) { - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); return Result.Success; } - if (message.Content == newContent) + if (message.Content == gatewayEvent.Content) { return Result.Success; } @@ -83,22 +72,22 @@ public sealed class MessageEditedResponder : IResponder // We don't need to await this since the result is not needed // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); - var diff = InlineDiffBuilder.Diff(message.Content, newContent); + var diff = InlineDiffBuilder.Diff(message.Content, gatewayEvent.Content); Messages.Culture = GuildSettings.Language.Get(cfg); var builder = new StringBuilder() .AppendLine(diff.AsMarkdown()) .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage, - $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") + $"https://discord.com/channels/{guildId}/{gatewayEvent.ChannelID}/{gatewayEvent.ID}") ); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) .WithDescription(builder.ToString()) - .WithTimestamp(timestamp.Value) + .WithTimestamp(gatewayEvent.EditedTimestamp.Value) .WithColour(ColorsList.Yellow) .Build(); diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj index 418803a..b67eaf8 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj @@ -27,11 +27,11 @@ - - - - - + + + + + From 5a351cbd97ff4ce11c628960ab8b7386c9f9228f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 08:33:14 +0000 Subject: [PATCH 33/33] Bump muno92/resharper_inspectcode from 1.12.3 to 1.13.0 (#348) --- .github/workflows/build-pr.yml | 2 +- TeamOctolings.Octobot/Program.cs | 51 +++++++++---------- .../Responders/GuildMemberLeftResponder.cs | 10 ++-- TeamOctolings.Octobot/Utility.cs | 4 +- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 84ec975..07d5b90 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '9.0.x' - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.12.3 + uses: muno92/resharper_inspectcode@1.13.0 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/TeamOctolings.Octobot/Program.cs b/TeamOctolings.Octobot/Program.cs index d1d6220..8cdbdcf 100644 --- a/TeamOctolings.Octobot/Program.cs +++ b/TeamOctolings.Octobot/Program.cs @@ -39,8 +39,7 @@ public sealed class Program private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .AddDiscordService( - services => + .AddDiscordService(services => { var configuration = services.GetRequiredService(); @@ -49,25 +48,22 @@ public sealed class Program "No bot token has been provided. Set the " + "BOT_TOKEN environment variable to a valid token."); } - ).ConfigureServices( - (_, services) => + ).ConfigureServices((_, services) => { - services.Configure( - options => - { - options.Intents |= GatewayIntents.MessageContents - | GatewayIntents.GuildMembers - | GatewayIntents.GuildPresences - | GatewayIntents.GuildScheduledEvents; - }); - services.Configure( - cSettings => - { - cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); - cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); - cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); - cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); - }); + services.Configure(options => + { + options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildPresences + | GatewayIntents.GuildScheduledEvents; + }); + services.Configure(cSettings => + { + cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); + }); services.AddTransient() // Init @@ -87,14 +83,13 @@ public sealed class Program .AddHostedService() .AddHostedService(); } - ).ConfigureLogging( - c => c.AddConsole() - .AddFile("Logs/Octobot-{Date}.log", - outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + ).ConfigureLogging(c => c.AddConsole() + .AddFile("Logs/Octobot-{Date}.log", + outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) ); } } diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs index 9774899..957a107 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs +++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs @@ -36,13 +36,9 @@ public sealed class GuildMemberLeftResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); - if (memberData.BannedUntil is not null || memberData.Kicked) - { - return Result.Success; - } - - if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() - || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") + if (memberData.BannedUntil is not null || memberData.Kicked + || GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() + || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.Success; } diff --git a/TeamOctolings.Octobot/Utility.cs b/TeamOctolings.Octobot/Utility.cs index f337d93..a2f7aca 100644 --- a/TeamOctolings.Octobot/Utility.cs +++ b/TeamOctolings.Octobot/Utility.cs @@ -67,8 +67,8 @@ public sealed class Utility builder.Append($"{Mention.Role(role)} "); } - builder = subscribers.Where( - subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) + builder = subscribers.Where(subscriber => + !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); }