1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-01-31 09:09:00 +03:00

Initial implementation of /warn

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
This commit is contained in:
Macintxsh 2024-03-19 18:46:56 +03:00
parent 5105b43eff
commit ab2158a648
Signed by: mctaylors
GPG key ID: 7181BEBE676903C1
13 changed files with 442 additions and 5 deletions

View file

@ -660,4 +660,34 @@
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve"> <data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
<value>Welcome messages channel</value> <value>Welcome messages channel</value>
</data> </data>
<data name="UserWarned" xml:space="preserve">
<value>{0} received a warning</value>
</data>
<data name="UserWarnsRemoved" xml:space="preserve">
<value>{0} no longer has warnings</value>
</data>
<data name="YouHaveBeenWarned" xml:space="preserve">
<value>You have been warned</value>
</data>
<data name="YourWarningsHaveBeenRevoked" xml:space="preserve">
<value>Your warnings have been revoked</value>
</data>
<data name="DescriptionActionWarns" xml:space="preserve">
<value>Warns: {0}</value>
</data>
<data name="UserHasNoWarnings" xml:space="preserve">
<value>This user has no warnings!</value>
</data>
<data name="ReceivedTooManyWarnings" xml:space="preserve">
<value>Received too many warnings</value>
</data>
<data name="SettingsWarnPunishment" xml:space="preserve">
<value>Punishment type for warnings</value>
</data>
<data name="SettingsWarnThreshold" xml:space="preserve">
<value>Warnings threshold</value>
</data>
<data name="SettingsWarnPunishmentDuration" xml:space="preserve">
<value>Punishment duration for warnings</value>
</data>
</root> </root>

View file

@ -660,4 +660,34 @@
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve"> <data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
<value>Канал для приветствий</value> <value>Канал для приветствий</value>
</data> </data>
<data name="UserWarned" xml:space="preserve">
<value>{0} получил предупреждение</value>
</data>
<data name="UserWarnsRemoved" xml:space="preserve">
<value>{0} больше не имеет предупреждений</value>
</data>
<data name="YouHaveBeenWarned" xml:space="preserve">
<value>Вы получили предупреждение</value>
</data>
<data name="YourWarningsHaveBeenRevoked" xml:space="preserve">
<value>Ваши предупреждения были отозваны</value>
</data>
<data name="DescriptionActionWarns" xml:space="preserve">
<value>Предупреждений: {0}</value>
</data>
<data name="UserHasNoWarnings" xml:space="preserve">
<value>Этот пользователь не имеет предупреждений!</value>
</data>
<data name="ReceivedTooManyWarnings" xml:space="preserve">
<value>Получил слишком много предупреждений</value>
</data>
<data name="SettingsWarnPunishment" xml:space="preserve">
<value>Тип наказания для предупреждений</value>
</data>
<data name="SettingsWarnThreshold" xml:space="preserve">
<value>Порог предупреждений</value>
</data>
<data name="SettingsWarnPunishmentDuration" xml:space="preserve">
<value>Длительность наказания для предупреждений</value>
</data>
</root> </root>

View file

@ -660,4 +660,34 @@
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve"> <data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
<value>канал куда говорить здравствуйте</value> <value>канал куда говорить здравствуйте</value>
</data> </data>
<data name="UserWarned" xml:space="preserve">
<value>{0} схлопотал варн</value>
</data>
<data name="UserWarnsRemoved" xml:space="preserve">
<value>{0} получил амнистию</value>
</data>
<data name="YouHaveBeenWarned" xml:space="preserve">
<value>вы схлопотали варн</value>
</data>
<data name="YourWarningsHaveBeenRevoked" xml:space="preserve">
<value>вы получили амнистию</value>
</data>
<data name="DescriptionActionWarns" xml:space="preserve">
<value>варнов: {0}</value>
</data>
<data name="UserHasNoWarnings" xml:space="preserve">
<value>его еще никто ни в чем не обвинял</value>
</data>
<data name="ReceivedTooManyWarnings" xml:space="preserve">
<value>схлопотал много варнов</value>
</data>
<data name="SettingsWarnPunishmentDuration" xml:space="preserve">
<value>сколько времени держать варновый бан</value>
</data>
<data name="SettingsWarnPunishment" xml:space="preserve">
<value>тип варнового бана</value>
</data>
<data name="SettingsWarnThreshold" xml:space="preserve">
<value>сколько варнов чтобы потом бан</value>
</data>
</root> </root>

View file

@ -127,7 +127,7 @@ public class BanCommandGroup : CommandGroup
return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken); return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken);
} }
private async Task<Result> BanUserAsync( public async Task<Result> BanUserAsync(
IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId, IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId,
IUser bot, CancellationToken ct = default) IUser bot, CancellationToken ct = default)
{ {

View file

@ -110,7 +110,7 @@ public class KickCommandGroup : CommandGroup
return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken); return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken);
} }
private async Task<Result> KickUserAsync( public async Task<Result> KickUserAsync(
IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot, IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot,
CancellationToken ct = default) CancellationToken ct = default)
{ {

View file

@ -121,7 +121,7 @@ public class MuteCommandGroup : CommandGroup
return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken); return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken);
} }
private async Task<Result> MuteUserAsync( public async Task<Result> MuteUserAsync(
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data,
Snowflake channelId, IUser bot, CancellationToken ct = default) Snowflake channelId, IUser bot, CancellationToken ct = default)
{ {

View file

@ -38,12 +38,14 @@ public class SettingsCommandGroup : CommandGroup
private static readonly IOption[] AllOptions = private static readonly IOption[] AllOptions =
[ [
GuildSettings.Language, GuildSettings.Language,
GuildSettings.WarnPunishment,
GuildSettings.WelcomeMessage, GuildSettings.WelcomeMessage,
GuildSettings.ReceiveStartupMessages, GuildSettings.ReceiveStartupMessages,
GuildSettings.RemoveRolesOnMute, GuildSettings.RemoveRolesOnMute,
GuildSettings.ReturnRolesOnRejoin, GuildSettings.ReturnRolesOnRejoin,
GuildSettings.AutoStartEvents, GuildSettings.AutoStartEvents,
GuildSettings.RenameHoistedUsers, GuildSettings.RenameHoistedUsers,
GuildSettings.WarnsThreshold,
GuildSettings.PublicFeedbackChannel, GuildSettings.PublicFeedbackChannel,
GuildSettings.PrivateFeedbackChannel, GuildSettings.PrivateFeedbackChannel,
GuildSettings.WelcomeMessagesChannel, GuildSettings.WelcomeMessagesChannel,
@ -51,7 +53,8 @@ public class SettingsCommandGroup : CommandGroup
GuildSettings.DefaultRole, GuildSettings.DefaultRole,
GuildSettings.MuteRole, GuildSettings.MuteRole,
GuildSettings.EventNotificationRole, GuildSettings.EventNotificationRole,
GuildSettings.EventEarlyNotificationOffset GuildSettings.EventEarlyNotificationOffset,
GuildSettings.WarnPunishmentDuration
]; ];
private readonly ICommandContext _context; private readonly ICommandContext _context;

View file

@ -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<Result> 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<Result> 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<Result> 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<Result> 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<Result> 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);
}
}

View file

@ -12,6 +12,8 @@ public static class GuildSettings
{ {
public static readonly LanguageOption Language = new("Language", "en"); public static readonly LanguageOption Language = new("Language", "en");
public static readonly Option<string> WarnPunishment = new("WarnPunishment", "disabled");
/// <summary> /// <summary>
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server. /// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the server.
/// </summary> /// </summary>
@ -46,6 +48,8 @@ public static class GuildSettings
/// </summary> /// </summary>
public static readonly BoolOption RenameHoistedUsers = new("RenameHoistedUsers", false); public static readonly BoolOption RenameHoistedUsers = new("RenameHoistedUsers", false);
public static readonly IntOption WarnsThreshold = new("WarnsThreshold", 0);
/// <summary> /// <summary>
/// Controls what channel should all public messages be sent to. /// Controls what channel should all public messages be sent to.
/// </summary> /// </summary>
@ -71,4 +75,7 @@ public static class GuildSettings
/// </summary> /// </summary>
public static readonly TimeSpanOption EventEarlyNotificationOffset = new( public static readonly TimeSpanOption EventEarlyNotificationOffset = new(
"EventEarlyNotificationOffset", TimeSpan.Zero); "EventEarlyNotificationOffset", TimeSpan.Zero);
public static readonly TimeSpanOption WarnPunishmentDuration = new(
"WarnPunishmentDuration", TimeSpan.Zero);
} }

View file

@ -18,6 +18,7 @@ public sealed class MemberData
public ulong Id { get; } public ulong Id { get; }
public DateTimeOffset? BannedUntil { get; set; } public DateTimeOffset? BannedUntil { get; set; }
public DateTimeOffset? MutedUntil { get; set; } public DateTimeOffset? MutedUntil { get; set; }
public int Warns { get; set; }
public bool Kicked { get; set; } public bool Kicked { get; set; }
public List<ulong> Roles { get; set; } = []; public List<ulong> Roles { get; set; } = [];
public List<Reminder> Reminders { get; } = []; public List<Reminder> Reminders { get; } = [];

View file

@ -13,12 +13,14 @@ namespace Octobot.Data.Options;
public enum AllOptionsEnum public enum AllOptionsEnum
{ {
[UsedImplicitly] Language, [UsedImplicitly] Language,
[UsedImplicitly] WarnPunishment,
[UsedImplicitly] WelcomeMessage, [UsedImplicitly] WelcomeMessage,
[UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] ReceiveStartupMessages,
[UsedImplicitly] RemoveRolesOnMute, [UsedImplicitly] RemoveRolesOnMute,
[UsedImplicitly] ReturnRolesOnRejoin, [UsedImplicitly] ReturnRolesOnRejoin,
[UsedImplicitly] AutoStartEvents, [UsedImplicitly] AutoStartEvents,
[UsedImplicitly] RenameHoistedUsers, [UsedImplicitly] RenameHoistedUsers,
[UsedImplicitly] WarnsThreshold,
[UsedImplicitly] PublicFeedbackChannel, [UsedImplicitly] PublicFeedbackChannel,
[UsedImplicitly] PrivateFeedbackChannel, [UsedImplicitly] PrivateFeedbackChannel,
[UsedImplicitly] WelcomeMessagesChannel, [UsedImplicitly] WelcomeMessagesChannel,
@ -26,5 +28,6 @@ public enum AllOptionsEnum
[UsedImplicitly] DefaultRole, [UsedImplicitly] DefaultRole,
[UsedImplicitly] MuteRole, [UsedImplicitly] MuteRole,
[UsedImplicitly] EventNotificationRole, [UsedImplicitly] EventNotificationRole,
[UsedImplicitly] EventEarlyNotificationOffset [UsedImplicitly] EventEarlyNotificationOffset,
[UsedImplicitly] WarnPunishmentDuration
} }

View file

@ -0,0 +1,31 @@
using System.Text.Json.Nodes;
using Remora.Results;
namespace Octobot.Data.Options;
public sealed class IntOption : Option<int>
{
public IntOption(string name, int defaultValue) : base(name, defaultValue) { }
public override string Display(JsonNode settings)
{
return settings[Name]?.GetValue<string>() ?? "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<string>()) : DefaultValue;
}
}

View file

@ -1191,5 +1191,54 @@ namespace Octobot {
return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); 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);
}
}
} }
} }