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); + } + } } }