1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-19 16:33:36 +03:00
This commit is contained in:
Macintxsh 2024-07-20 14:48:57 +05:00 committed by GitHub
commit 5b38cdd4cd
Signed by: GitHub
GPG key ID: B5690EEEBB952194
15 changed files with 1201 additions and 206 deletions

View file

@ -128,7 +128,7 @@ public sealed 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, IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data,
Snowflake channelId, Snowflake channelId,
IUser bot, CancellationToken ct = default) IUser bot, CancellationToken ct = default)

View file

@ -111,7 +111,7 @@ public sealed 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

@ -123,7 +123,7 @@ public sealed class MuteCommandGroup : CommandGroup
CancellationToken); 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,6 +38,7 @@ public sealed class SettingsCommandGroup : CommandGroup
private static readonly IGuildOption[] AllOptions = private static readonly IGuildOption[] AllOptions =
[ [
GuildSettings.Language, GuildSettings.Language,
GuildSettings.WarnPunishment,
GuildSettings.WelcomeMessage, GuildSettings.WelcomeMessage,
GuildSettings.LeaveMessage, GuildSettings.LeaveMessage,
GuildSettings.ReceiveStartupMessages, GuildSettings.ReceiveStartupMessages,
@ -45,6 +46,7 @@ public sealed class SettingsCommandGroup : CommandGroup
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,
@ -53,7 +55,8 @@ public sealed class SettingsCommandGroup : CommandGroup
GuildSettings.MuteRole, GuildSettings.MuteRole,
GuildSettings.ModeratorRole, GuildSettings.ModeratorRole,
GuildSettings.EventNotificationRole, GuildSettings.EventNotificationRole,
GuildSettings.EventEarlyNotificationOffset GuildSettings.EventEarlyNotificationOffset,
GuildSettings.WarnPunishmentDuration
]; ];
private readonly ICommandContext _context; private readonly ICommandContext _context;

View file

@ -0,0 +1,560 @@
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Json.Nodes;
using JetBrains.Annotations;
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.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
using TeamOctolings.Octobot.Data;
using TeamOctolings.Octobot.Extensions;
using TeamOctolings.Octobot.Services;
using static System.DateTimeOffset;
namespace TeamOctolings.Octobot.Commands;
[UsedImplicitly]
public class WarnCommandGroup : CommandGroup
{
private readonly AccessControlService _access;
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, AccessControlService access)
{
_context = context;
_channelApi = channelApi;
_guildData = guildData;
_feedback = feedback;
_guildApi = guildApi;
_userApi = userApi;
_utility = utility;
_access = access;
}
[Command("warn")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.KickMembers,
DiscordPermission.ModerateMembers, DiscordPermission.BanMembers)]
[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);
var interactionResult
= await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", 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);
}
return await WarnPreparationAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken);
}
private async Task<Result> 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 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
&& warnThreshold is not 0)
{
var errorEmbed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.WarnThresholdExceeded, warnThreshold), bot)
.WithDescription(Messages.WarnThresholdExceededDescription)
.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 + 1 < warnThreshold || warnThreshold is 0)
{
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);
}
private async Task<Result> 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,
At = UtcNow,
Reason = reason
});
var builder = new StringBuilder()
.AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
.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();
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(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 && warnThreshold is not 0)
{
return await PunishUserAsync(target, guild, data, channelId, bot, warns, warnPunishment, warnDuration, CancellationToken);
}
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
}
private async Task<Result> PunishUserAsync(IUser target, IGuild guild, GuildData data,
Snowflake channelId, IUser bot, IList warns, string punishment, TimeSpan duration, CancellationToken ct)
{
if (punishment is "ban" && duration != TimeSpan.Zero)
{
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 (punishment is "kick")
{
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 (punishment is "mute" && duration != TimeSpan.Zero)
{
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);
}
warns.Clear();
return Result.FromSuccess();
}
[Command("unwarn")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ManageMessages)]
[Description("Remove warns from user")]
[UsedImplicitly]
public async Task<Result> ExecuteUnwarnAsync(
[Description("User to remove warns from")]
IUser target,
[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))
{
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);
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,
CancellationToken);
}
return await RemoveUserWarnsAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken);
}
private async Task<Result> RemoveUserWarnAsync(IUser executor, IUser target, string reason, int warnNumber,
IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default)
{
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 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<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);
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();
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);
}
[Command("listwarn")]
[DiscordDefaultDMPermission(false)]
[Ephemeral]
[Description("(Ephemeral) Get current warns")]
[UsedImplicitly]
public async Task<Result> ExecuteListWarnsAsync(
[Description("(Moderator-only) Get target's current warns")]
IUser? target = null)
{
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
{
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
}
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
return Result.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
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);
if (target is not null)
{
return await ListTargetWarnsAsync(executor, target, guild, data, bot, CancellationToken);
}
return await ListExecutorWarnsAsync(executor, data, bot, CancellationToken);
}
private async Task<Result> 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<Result> ListExecutorWarnsAsync(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 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.ListExecutorWarnsTitle, executor.GetTag()), executor)
.WithDescription(description.ToString())
.WithColour(ColorsList.Default).Build();
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 PunishmentOption WarnPunishment = new("WarnPunishment", "disabled");
/// <summary> /// <summary>
/// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the guild. /// Controls what message should be sent in <see cref="PublicFeedbackChannel" /> when a new member joins the guild.
/// </summary> /// </summary>
@ -58,6 +60,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>
@ -84,4 +88,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

@ -5,13 +5,18 @@ namespace TeamOctolings.Octobot.Data;
/// </summary> /// </summary>
public sealed class MemberData public sealed class MemberData
{ {
public MemberData(ulong id, List<Reminder>? reminders = null) public MemberData(ulong id, List<Reminder>? reminders = null, List<Warn>? warns = null)
{ {
Id = id; Id = id;
if (reminders is not null) if (reminders is not null)
{ {
Reminders = reminders; Reminders = reminders;
} }
if (warns is not null)
{
Warns = warns;
}
} }
public ulong Id { get; } public ulong Id { get; }
@ -20,4 +25,5 @@ public sealed class MemberData
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; } = [];
public List<Warn> Warns { get; } = [];
} }

View file

@ -13,6 +13,7 @@ namespace TeamOctolings.Octobot.Data.Options;
public enum AllOptionsEnum public enum AllOptionsEnum
{ {
[UsedImplicitly] Language, [UsedImplicitly] Language,
[UsedImplicitly] WarnPunishment,
[UsedImplicitly] WelcomeMessage, [UsedImplicitly] WelcomeMessage,
[UsedImplicitly] LeaveMessage, [UsedImplicitly] LeaveMessage,
[UsedImplicitly] ReceiveStartupMessages, [UsedImplicitly] ReceiveStartupMessages,
@ -20,6 +21,7 @@ public enum AllOptionsEnum
[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,
@ -28,5 +30,6 @@ public enum AllOptionsEnum
[UsedImplicitly] MuteRole, [UsedImplicitly] MuteRole,
[UsedImplicitly] ModeratorRole, [UsedImplicitly] ModeratorRole,
[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 TeamOctolings.Octobot.Data.Options;
public sealed class IntOption : GuildOption<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

@ -0,0 +1,23 @@
using System.Text.Json.Nodes;
using Remora.Results;
namespace TeamOctolings.Octobot.Data.Options;
/// <inheritdoc />
public sealed class PunishmentOption : GuildOption<string>
{
private static readonly List<string> AllowedValues =
[
"ban", "kick", "mute", "off", "disable", "disabled"
];
public PunishmentOption(string name, string defaultValue) : base(name, defaultValue) { }
/// <inheritdoc />
public override Result Set(JsonNode settings, string from)
{
return AllowedValues.Contains(from.ToLowerInvariant())
? base.Set(settings, from.ToLowerInvariant())
: new ArgumentInvalidError(nameof(from), Messages.InvalidWarnPunishment);
}
}

View file

@ -0,0 +1,8 @@
namespace TeamOctolings.Octobot.Data;
public struct Warn
{
public ulong WarnedBy { get; init; }
public DateTimeOffset At { get; init; }
public string Reason { get; init; }
}

View file

@ -1179,6 +1179,53 @@ namespace TeamOctolings.Octobot {
} }
} }
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 DescriptionWarns {
get {
return ResourceManager.GetString("DescriptionWarns", resourceCulture);
}
}
internal static string UserHasNoWarnings {
get {
return ResourceManager.GetString("UserHasNoWarnings", resourceCulture);
}
}
internal static string YouHaveNoWarnings {
get {
return ResourceManager.GetString("YouHaveNoWarnings", resourceCulture);
}
}
internal static string ReceivedTooManyWarnings {
get {
return ResourceManager.GetString("ReceivedTooManyWarnings", resourceCulture);
}
}
internal static string ButtonDirty { internal static string ButtonDirty {
get { get {
return ResourceManager.GetString("ButtonDirty", resourceCulture); return ResourceManager.GetString("ButtonDirty", resourceCulture);
@ -1196,5 +1243,71 @@ namespace TeamOctolings.Octobot {
return ResourceManager.GetString("SettingsModeratorRole", resourceCulture); return ResourceManager.GetString("SettingsModeratorRole", resourceCulture);
} }
} }
internal static string ListTargetWarnsTitle {
get {
return ResourceManager.GetString("ListTargetWarnsTitle", 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 ListExecutorWarnsTitle {
get {
return ResourceManager.GetString("ListExecutorWarnsTitle", resourceCulture);
}
}
internal static string DescriptionPunishmentType {
get {
return ResourceManager.GetString("DescriptionPunishmentType", resourceCulture);
}
}
internal static string WarnThresholdExceeded {
get {
return ResourceManager.GetString("WarnThresholdExceeded", resourceCulture);
}
}
internal static string WarnPunishmentDurationNotSet {
get {
return ResourceManager.GetString("WarnPunishmentDurationNotSet", resourceCulture);
}
}
internal static string WarnThresholdExceededDescription {
get {
return ResourceManager.GetString("WarnThresholdExceededDescription", resourceCulture);
}
}
internal static string InvalidWarnPunishment {
get {
return ResourceManager.GetString("InvalidWarnPunishment", resourceCulture);
}
}
} }
} }

View file

@ -681,4 +681,124 @@
<data name="SettingsModeratorRole" xml:space="preserve"> <data name="SettingsModeratorRole" xml:space="preserve">
<value>Moderator role</value> <value>Moderator role</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="DescriptionWarns" 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>
<data name="ListExecutorWarnsTitle" xml:space="preserve">
<value>Here's your warnings, {0}:</value>
</data>
<data name="YouHaveNoWarnings" xml:space="preserve">
<value>You have no warnings!</value>
</data>
<data name="ReceivedOn" xml:space="preserve">
<value>Received on {0}</value>
</data>
<data name="UserWarnRemoved" xml:space="preserve">
<value>Warning #{0} has been removed from {1}</value>
</data>
<data name="YourWarningHasBeenRevoked" xml:space="preserve">
<value>Your warning has been revoked</value>
</data>
<data name="WrongWarningNumberSelected" xml:space="preserve">
<value>Wrong warning number selected!</value>
</data>
<data name="UserCannotWarnBot" xml:space="preserve">
<value>You cannot warn me!</value>
</data>
<data name="UserCannotWarnOwner" xml:space="preserve">
<value>You cannot warn the owner of this guild!</value>
</data>
<data name="UserCannotWarnTarget" xml:space="preserve">
<value>You cannot warn this member!</value>
</data>
<data name="UserCannotWarnThemselves" xml:space="preserve">
<value>You cannot warn yourself!</value>
</data>
<data name="UserCannotUnwarnBot" xml:space="preserve">
<value>You cannot unwarn me!</value>
</data>
<data name="UserCannotUnwarnOwner" xml:space="preserve">
<value>You cannot unwarn the owner of this guild!</value>
</data>
<data name="UserCannotUnwarnTarget" xml:space="preserve">
<value>You cannot unwarn this member!</value>
</data>
<data name="UserCannotUnwarnThemselves" xml:space="preserve">
<value>You cannot unwarn yourself!</value>
</data>
<data name="BotCannotWarnMembers" xml:space="preserve">
<value>I cannot warn members from this guild!</value>
</data>
<data name="BotCannotWarnTarget" xml:space="preserve">
<value>I cannot warn this member!</value>
</data>
<data name="BotCannotUnwarnTarget" xml:space="preserve">
<value>I cannot unwarn this member!</value>
</data>
<data name="UserCannotGetWarnsBot" xml:space="preserve">
<value>You cannot get my warns!</value>
</data>
<data name="UserCannotGetWarnsOwner" xml:space="preserve">
<value>You cannot get owner's warns!</value>
</data>
<data name="UserCannotGetWarnsTarget" xml:space="preserve">
<value>You cannot get warns of this member!</value>
</data>
<data name="UserCannotGetWarnsThemselves" xml:space="preserve">
<value>Use this command without options instead.</value>
</data>
<data name="UserCannotWarnMembers" xml:space="preserve">
<value>You cannot warn members in this guild!</value>
</data>
<data name="UserCannotUnwarnMembers" xml:space="preserve">
<value>You cannot unwarn members in this guild!</value>
</data>
<data name="UserCannotGetWarnsMembers" xml:space="preserve">
<value>You cannot get warns of other members in this guild!</value>
</data>
<data name="ListTargetWarnsTitle" xml:space="preserve">
<value>Warnings given to {0}:</value>
</data>
<data name="DescriptionPunishmentType" xml:space="preserve">
<value>Punishment type: {0}</value>
</data>
<data name="WarnThresholdExceeded" xml:space="preserve">
<value>Warn threshold has been exceeded. ({0})</value>
</data>
<data name="WarnPunishmentDurationNotSet" xml:space="preserve">
<value>Warn Punishment Duration is not set for the current punishment type.</value>
</data>
<data name="WarnThresholdExceededDescription" xml:space="preserve">
<value>Increase the Warn Threshold or set a Warn Punishment.</value>
</data>
<data name="InvalidWarnPunishment" xml:space="preserve">
<value>Invalid Warn Punishment is set.</value>
</data>
</root> </root>

View file

@ -681,4 +681,124 @@
<data name="SettingsModeratorRole" xml:space="preserve"> <data name="SettingsModeratorRole" 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="DescriptionWarns" 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>
<data name="ListExecutorWarnsTitle" xml:space="preserve">
<value>Вот ваши предупреждения, {0}:</value>
</data>
<data name="YouHaveNoWarnings" xml:space="preserve">
<value>У вас нет предупреждений!</value>
</data>
<data name="ReceivedOn" xml:space="preserve">
<value>Получено {0}</value>
</data>
<data name="UserWarnRemoved" xml:space="preserve">
<value>Предупреждение №{0} было снято с {1}</value>
</data>
<data name="YourWarningHasBeenRevoked" xml:space="preserve">
<value>Ваше предупреждение было отозвано</value>
</data>
<data name="WrongWarningNumberSelected" xml:space="preserve">
<value>Выбрано неверное число предупреждения!</value>
</data>
<data name="UserCannotWarnBot" xml:space="preserve">
<value>Ты не можешь меня предупредить!</value>
</data>
<data name="UserCannotWarnOwner" xml:space="preserve">
<value>Ты не можешь предупредить владельца этого сервера!</value>
</data>
<data name="UserCannotWarnTarget" xml:space="preserve">
<value>Ты не можешь предупредить этого участника!</value>
</data>
<data name="UserCannotWarnThemselves" xml:space="preserve">
<value>Ты не можешь себя предупредить!</value>
</data>
<data name="UserCannotUnwarnBot" xml:space="preserve">
<value>Ты не можешь снять с меня предупреждения!</value>
</data>
<data name="UserCannotUnwarnOwner" xml:space="preserve">
<value>Ты не можешь снять предупреждения с владельца этого сервера!</value>
</data>
<data name="UserCannotUnwarnTarget" xml:space="preserve">
<value>Ты не можешь снять предупреждения с этого участника!</value>
</data>
<data name="UserCannotUnwarnThemselves" xml:space="preserve">
<value>Ты не можешь снять с себя предупреждения!</value>
</data>
<data name="BotCannotUnwarnTarget" xml:space="preserve">
<value>Я не могу снимать предупреждения этого участника!</value>
</data>
<data name="BotCannotWarnTarget" xml:space="preserve">
<value>Я не могу предупредить этого участника!</value>
</data>
<data name="BotCannotWarnMembers" xml:space="preserve">
<value>Я не могу предупреждать участников этого сервера!</value>
</data>
<data name="UserCannotGetWarnsBot" xml:space="preserve">
<value>Ты не можешь просмотреть мои предупреждения!</value>
</data>
<data name="UserCannotGetWarnsOwner" xml:space="preserve">
<value>Ты не можешь просмотреть предупреждения владельца этого сервера!</value>
</data>
<data name="UserCannotGetWarnsTarget" xml:space="preserve">
<value>Ты не можешь просмотреть предупреждения этого участника!</value>
</data>
<data name="UserCannotGetWarnsThemselves" xml:space="preserve">
<value>Вместо этого, используйте эту команду без параметров.</value>
</data>
<data name="UserCannotUnwarnMembers" xml:space="preserve">
<value>Ты не можешь снимать предупреждения с участников этого сервера!</value>
</data>
<data name="UserCannotWarnMembers" xml:space="preserve">
<value>Ты не можешь предупреждать участников этого сервера!</value>
</data>
<data name="UserCannotGetWarnsMembers" xml:space="preserve">
<value>Ты не можешь просматривать предупреждения участников этого сервера!</value>
</data>
<data name="ListTargetWarnsTitle" xml:space="preserve">
<value>Предупреждения пользователя {0}:</value>
</data>
<data name="DescriptionPunishmentType" xml:space="preserve">
<value>Тип наказания: {0}</value>
</data>
<data name="WarnThresholdExceeded" xml:space="preserve">
<value>Превышен порог предупреждений. ({0})</value>
</data>
<data name="WarnPunishmentDurationNotSet" xml:space="preserve">
<value>Длительность наказания предупреждения не установлена для текущего типа наказания.</value>
</data>
<data name="WarnThresholdExceededDescription" xml:space="preserve">
<value>Увеличьте порог предупреждения или установите наказание за предупреждение.</value>
</data>
<data name="InvalidWarnPunishment" xml:space="preserve">
<value>Установлено неверное наказание за предупреждение.</value>
</data>
</root> </root>

View file

@ -100,7 +100,8 @@ public sealed class AccessControlService
{ {
"Ban" => DiscordPermission.BanMembers, "Ban" => DiscordPermission.BanMembers,
"Kick" => DiscordPermission.KickMembers, "Kick" => DiscordPermission.KickMembers,
"Mute" or "Unmute" => DiscordPermission.ModerateMembers, "Mute" or "Unmute" or "Warn" or "Unwarn" or "GetWarns"
=> DiscordPermission.ModerateMembers,
_ => throw new Exception() _ => throw new Exception()
}); });