diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleBuilder.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleBuilder.cs new file mode 100644 index 000000000..17b789877 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleBuilder.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Discord; + +/// +/// A builder used to create a . +/// +public class AutoModRuleBuilder +{ + private const int MaxKeywordCount = 1000; + private const int MaxKeywordLength = 30; + + private const int MaxRegexPatternCount = 10; + private const int MaxRegexPatternLength = 260; + + private const int MaxAllowListCountKeyword = 100; + private const int MaxAllowListCountKeywordPreset = 1000; + private const int MaxAllowListEntryLength = 30; + + private const int MaxMentionLimit = 50; + + private const int MaxExemptRoles = 20; + private const int MaxExemptChannels = 50; + + private List _keywordFilter = new (); + private List _regexPatterns = new (); + private List _allowList = new (); + private List _actions = new (); + + private List _exemptRoles = new(); + private List _exemptChannels = new(); + + private int? _mentionLimit; + + /// + /// + /// + public List KeywordFilter + { + get { return _keywordFilter; } + set + { + if (TriggerType != AutoModTriggerType.Keyword) + throw new ArgumentException(message: $"Keyword filter can only be used with 'Keyword' trigger type.", paramName: nameof(KeywordFilter)); + + if (value.Count > MaxKeywordCount) + throw new ArgumentException(message: $"Keyword count must be less than or equal to {MaxKeywordCount}.", paramName: nameof(KeywordFilter)); + + if (value.Any(x => x.Length > MaxKeywordLength)) + throw new ArgumentException(message: $"Keyword length must be less than or equal to {MaxKeywordLength}.", paramName: nameof(KeywordFilter)); + + _keywordFilter = value; + } + } + + /// + /// + /// + public List RegexPatterns + { + get { return _regexPatterns; } + set + { + if (TriggerType != AutoModTriggerType.Keyword) + throw new ArgumentException(message: $"Regex patterns can only be used with 'Keyword' trigger type.", paramName: nameof(RegexPatterns)); + + if (value.Count > MaxRegexPatternCount) + throw new ArgumentException(message: $"Regex pattern count must be less than or equal to {MaxRegexPatternCount}.", paramName: nameof(RegexPatterns)); + + if (value.Any(x => x.Length > MaxRegexPatternLength)) + throw new ArgumentException(message: $"Regex pattern must be less than or equal to {MaxRegexPatternLength}.", paramName: nameof(RegexPatterns)); + + _regexPatterns = value; + } + } + + /// + /// + /// + public List AllowList + { + get { return _allowList; } + set + { + if (TriggerType is not AutoModTriggerType.Keyword or AutoModTriggerType.KeywordPreset) + throw new ArgumentException(message: $"Allow list can only be used with 'Keyword' or 'KeywordPreset' trigger type.", paramName: nameof(AllowList)); + + if (TriggerType == AutoModTriggerType.Keyword && value.Count > MaxAllowListCountKeyword) + throw new ArgumentException(message: $"Allow list entry count must be less than or equal to {MaxAllowListCountKeyword}.", paramName: nameof(AllowList)); + + if (TriggerType == AutoModTriggerType.KeywordPreset && value.Count > MaxAllowListCountKeywordPreset) + throw new ArgumentException(message: $"Allow list entry count must be less than or equal to {MaxAllowListCountKeywordPreset}.", paramName: nameof(AllowList)); + + if (value.Any(x => x.Length > MaxAllowListEntryLength)) + throw new ArgumentException(message: $"Allow list entry length must be less than or equal to {MaxAllowListEntryLength}.", paramName: nameof(AllowList)); + + _allowList = value; + } + } + + /// + /// + /// + public List ExemptRoles + { + get { return _exemptRoles; } + set + { + if (value.Count > MaxExemptRoles) + throw new ArgumentException(message: $"Exempt roles count must be less than or equal to {MaxExemptRoles}.", paramName: nameof(RegexPatterns)); + + _exemptRoles = value; + } + } + + /// + /// + /// + public List ExemptChannels + { + get { return _exemptChannels; } + set + { + if (value.Count > MaxExemptRoles) + throw new ArgumentException(message: $"Exempt channels count must be less than or equal to {MaxExemptRoles}.", paramName: nameof(RegexPatterns)); + + _exemptChannels = value; + } + } + + /// + /// + /// + public HashSet Presets = new (); + + /// + /// + /// + public string Name { get; set; } + + /// + /// + /// + public AutoModEventType EventType { get; set; } + + /// + /// + /// + public AutoModTriggerType TriggerType { get; } + + /// + /// + /// + public int? MentionLimit + { + get => _mentionLimit; + set + { + if (TriggerType != AutoModTriggerType.Keyword) + throw new ArgumentException(message: $"MentionLimit can only be used with 'MentionSpam' trigger type.", paramName: nameof(MentionLimit)); + + if (value is not null && value > MaxMentionLimit) + throw new ArgumentException(message: $"Mention limit must be less than or equal to {MaxMentionLimit}.", paramName: nameof(MentionLimit)); + _mentionLimit = value; + } + + } + + /// + /// + /// + public List Actions; + + /// + /// + /// + public bool Enabled { get; set; } = false; + + /// + /// + /// + /// + public AutoModRuleBuilder(AutoModTriggerType type) + { + TriggerType = type; + } +} + +/// +/// Represents an action that will be preformed if a user breaks an . +/// +public class AutoModRuleActionBuilder +{ + private const int MaxTimeoutSeconds = 2419200; + + private TimeSpan? _timeoutDuration; + + /// + /// Gets or sets the type for this action. + /// + public AutoModActionType Type { get; } + + /// + /// Get or sets the channel id on which to post alerts. + /// + public ulong? ChannelId { get; set; } + + /// + /// Gets or sets the duration of which a user will be timed out for breaking this rule. + /// + public TimeSpan? TimeoutDuration + { + get => _timeoutDuration; + set + { + if(value is { TotalSeconds: > MaxTimeoutSeconds }) + throw new ArgumentException(message: $"Field count must be less than or equal to {MaxTimeoutSeconds}.", paramName: nameof(TimeoutDuration)); + + _timeoutDuration = value; + } + } + + /// + /// + /// + public AutoModRuleActionBuilder() + { + + } + + /// + /// + /// + public AutoModRuleActionBuilder(AutoModActionType type, ulong? channelId, TimeSpan? duration) + { + Type = type; + ChannelId = channelId; + TimeoutDuration = duration; + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index deb8a010f..80330b042 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1290,6 +1290,6 @@ namespace Discord /// /// A task that represents the asynchronous creation operation. The task result contains the created . /// - Task CreateAutoModRuleAsync(RequestOptions options = null); + Task CreateAutoModRuleAsync(AutoModRuleBuilder builder, RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs b/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs index d126d4078..5438700c6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; @@ -8,13 +9,28 @@ namespace Discord.API.Rest { internal class CreateAutoModRuleParams { + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("event_type")] public AutoModEventType EventType { get; set; } + + [JsonProperty("trigger_type")] public AutoModTriggerType TriggerType { get; set; } + + [JsonProperty("trigger_metadata")] public Optional TriggerMetadata { get; set; } + + [JsonProperty("actions")] public AutoModAction[] Actions { get; set; } + + [JsonProperty("enabled")] public Optional Enabled { get; set; } + + [JsonProperty("exempt_roles")] public Optional ExemptRoles { get; set; } + + [JsonProperty("exempt_channels")] public Optional ExemptChannels { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 6a78cbd53..1706608c6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1064,10 +1064,8 @@ namespace Discord.Rest #region Auto Mod - public static Task CreateAutoModRuleAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) - { - throw new NotImplementedException(); - } + public static async Task CreateAutoModRuleAsync(IGuild guild, CreateAutoModRuleParams args, BaseDiscordClient client, RequestOptions options) + => await client.ApiClient.CreateGuildAutoModRuleAsync(guild.Id, args, options); public static async Task GetAutoModRuleAsync(ulong ruleId, IGuild guild, BaseDiscordClient client, RequestOptions options) => await client.ApiClient.GetGuildAutoModRuleAsync(guild.Id, ruleId, options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs b/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs index e88f7fe61..f2dc4c9b7 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs @@ -89,8 +89,13 @@ public class RestAutoModRule : RestEntity, IAutoModRule } /// - public Task ModifyAsync(Action func, RequestOptions options = null) => throw new NotImplementedException(); + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyRuleAsync(Discord, this, func, options); + Update(model); + } /// - public Task DeleteAsync(RequestOptions options = null) => throw new NotImplementedException(); + public Task DeleteAsync(RequestOptions options = null) + => GuildHelper.DeleteRuleAsync(Discord, this, options); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index ef6754b8c..a8d337b2a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1217,10 +1217,11 @@ namespace Discord.Rest } /// - public async Task CreateAutoModRuleAsync(RequestOptions options = null) + public async Task CreateAutoModRuleAsync(AutoModRuleBuilder builder, RequestOptions options = null) { - var rule = await GuildHelper.CreateAutoModRuleAsync(this, Discord, options); - throw new NotImplementedException(); + var rule = await GuildHelper.CreateAutoModRuleAsync(this, builder.ToProperties(), Discord, options); + + return RestAutoModRule.Create(Discord, rule); } @@ -1580,8 +1581,8 @@ namespace Discord.Rest => await GetAutoModRulesAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateAutoModRuleAsync(RequestOptions options) - => await CreateAutoModRuleAsync(options).ConfigureAwait(false); + async Task IGuild.CreateAutoModRuleAsync(AutoModRuleBuilder builder, RequestOptions options) + => await CreateAutoModRuleAsync(builder, options).ConfigureAwait(false); #endregion } diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index f5a88486b..cc65a2356 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -214,5 +214,34 @@ namespace Discord.Rest Id = interaction.Id, }; } + + public static API.Rest.CreateAutoModRuleParams ToProperties(this AutoModRuleBuilder builder) + { + return new API.Rest.CreateAutoModRuleParams + { + TriggerMetadata = new API.TriggerMetadata + { + KeywordFilter = builder.KeywordFilter?.ToArray(), + AllowList = builder.AllowList?.ToArray(), + MentionLimit = builder.MentionLimit ?? Optional.Unspecified, + Presets = builder.Presets?.ToArray(), + RegexPatterns = builder.RegexPatterns?.ToArray(), + }, + TriggerType = builder.TriggerType, + Actions = builder.Actions?.Select(x => new API.AutoModAction + { + Metadata = new API.ActionMetadata + { + ChannelId = x.ChannelId ?? Optional.Unspecified, + DurationSeconds = (int?)x.TimeoutDuration?.TotalSeconds ?? Optional.Unspecified + }, + Type = x.Type, + }).ToArray(), + Enabled = builder.Enabled, + EventType = builder.EventType, + ExemptChannels = builder.ExemptChannels?.ToArray() ?? Optional.Unspecified, + ExemptRoles = builder.ExemptRoles?.ToArray() ?? Optional .Unspecified, + }; + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs index c9f92eb70..d562c5a2d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text; using System.Threading.Tasks; using Model = Discord.API.AutoModerationRule; @@ -101,8 +100,11 @@ namespace Discord.WebSocket } /// - public Task ModifyAsync(Action func, RequestOptions options = null) - => GuildHelper.ModifyRuleAsync(Discord, this, func, options); + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await GuildHelper.ModifyRuleAsync(Discord, this, func, options); + Guild.AddOrUpdateAutoModRule(model); + } /// public Task DeleteAsync(RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c5c99f009..271cbb36b 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1827,7 +1827,10 @@ namespace Discord.WebSocket return socketRule; } - internal SocketAutoModRule GetAutoModRule(ulong id) + /// + /// Gets a single rule configured in a guild from cache. Returns if the rule was not found. + /// + public SocketAutoModRule GetAutoModRule(ulong id) { return _automodRules.TryGetValue(id, out var rule) ? rule : null; } @@ -1864,10 +1867,11 @@ namespace Discord.WebSocket } /// - public async Task CreateAutoModRuleAsync(RequestOptions options = null) + public async Task CreateAutoModRuleAsync(AutoModRuleBuilder builder, RequestOptions options = null) { - var rule = await GuildHelper.CreateAutoModRuleAsync(this, Discord, options); - throw new NotImplementedException(); + var rule = await GuildHelper.CreateAutoModRuleAsync(this, builder.ToProperties(), Discord, options); + + return AddOrUpdateAutoModRule(rule); } #endregion @@ -2126,8 +2130,8 @@ namespace Discord.WebSocket => await GetAutoModRulesAsync(options).ConfigureAwait(false); /// - async Task IGuild.CreateAutoModRuleAsync(RequestOptions options) - => await CreateAutoModRuleAsync(options).ConfigureAwait(false); + async Task IGuild.CreateAutoModRuleAsync(AutoModRuleBuilder builder, RequestOptions options) + => await CreateAutoModRuleAsync(builder, options).ConfigureAwait(false); #endregion }