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