diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModActionType.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModActionType.cs new file mode 100644 index 000000000..29e95d918 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModActionType.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public enum AutoModActionType + { + /// + /// Blocks the content of a message according to the rule. + /// + BlockMessage = 1, + + /// + /// Logs user content to a specified channel. + /// + SendAlertMessage = 2, + + /// + /// Timeout user for a specified duration. + /// + Timeout = 3, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModEventType.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModEventType.cs new file mode 100644 index 000000000..6ed93d53b --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModEventType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// An enum indecating in what event context a rule should be checked. + /// + public enum AutoModEventType + { + /// + /// When a member sends or edits a message in the guild. + /// + MessageSend = 1 + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleAction.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleAction.cs new file mode 100644 index 000000000..531d435a6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleAction.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents an action that will be preformed if a user breaks an . + /// + public class AutoModRuleAction + { + /// + /// Gets the type for this action. + /// + public AutoModActionType Type { get; } + + /// + /// Get the channel id on which to post alerts. if no channel has been provided. + /// + public ulong? ChannelId { get; } + + /// + /// Gets the duration of which a user will be timed out for breaking this rule. if no timeout duration has been provided. + /// + public TimeSpan? TimeoutDuration { get; } + + internal AutoModRuleAction(AutoModActionType type, ulong? channelId, int? duration) + { + Type = type; + ChannelId = channelId; + TimeoutDuration = duration.HasValue ? TimeSpan.FromSeconds(duration.Value) : null; + } + } +} 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..e69de29bb diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleProperties.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleProperties.cs new file mode 100644 index 000000000..9af9832e5 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleProperties.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Provides properties used to modify a . + /// + public class AutoModRuleProperties + { + /// + /// Returns the max keyword count for an AutoMod rule allowed by Discord. + /// + public const int MaxKeywordCount = 1000; + + /// + /// Returns the max keyword length for an AutoMod rule allowed by Discord. + /// + public const int MaxKeywordLength = 60; + + /// + /// Returns the max regex pattern count for an AutoMod rule allowed by Discord. + /// + public const int MaxRegexPatternCount = 10; + + /// + /// Returns the max regex pattern length for an AutoMod rule allowed by Discord. + /// + public const int MaxRegexPatternLength = 260; + + /// + /// Returns the max allowlist keyword count for a AutoMod rule allowed by Discord. + /// + public const int MaxAllowListCountKeyword = 100; + + /// + /// Returns the max allowlist keyword count for a AutoMod rule allowed by Discord. + /// + public const int MaxAllowListCountKeywordPreset = 1000; + + /// + /// Returns the max allowlist keyword length for an AutoMod rule allowed by Discord. + /// + public const int MaxAllowListEntryLength = 60; + + /// + /// Returns the max mention limit for an AutoMod rule allowed by Discord. + /// + public const int MaxMentionLimit = 50; + + /// + /// Returns the max exempt role count for an AutoMod rule allowed by Discord. + /// + public const int MaxExemptRoles = 20; + + /// + /// Returns the max exempt channel count for an AutoMod rule allowed by Discord. + /// + public const int MaxExemptChannels = 50; + + /// + /// Returns the max timeout duration in seconds for an auto moderation rule action. + /// + public const int MaxTimeoutSeconds = 2419200; + + /// + /// Gets or sets the name for the rule. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the event type for the rule. + /// + public Optional EventType { get; set; } + + /// + /// Gets or sets the trigger type for the rule. + /// + public Optional TriggerType { get; set; } + + /// + /// Gets or sets the keyword filter for the rule. + /// + public Optional KeywordFilter { get; set; } + + /// + /// Gets or sets regex patterns for the rule. + /// + public Optional RegexPatterns { get; set; } + + /// + /// Gets or sets the allow list for the rule. + /// + public Optional AllowList { get; set; } + + /// + /// Gets or sets total mention limit for the rule. + /// + public Optional MentionLimit { get; set; } + + /// + /// Gets or sets the presets for the rule. Empty if the rule has no presets. + /// + public Optional Presets { get; set; } + + /// + /// Gets or sets the actions for the rule. + /// + public Optional Actions { get; set; } + + /// + /// Gets or sets whether or not the rule is enabled. + /// + public Optional Enabled { get; set; } + + /// + /// Gets or sets the exempt roles for the rule. Empty if the rule has no exempt roles. + /// + public Optional ExemptRoles { get; set; } + + /// + /// Gets or sets the exempt channels for the rule. Empty if the rule has no exempt channels. + /// + public Optional ExemptChannels { get; set; } + } + + /// + /// Provides properties used to modify a . + /// + public class AutoModRuleActionProperties + { + /// + /// Gets or sets the type for this action. + /// + public AutoModActionType Type { get; set; } + + /// + /// 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; set; } + } + +} diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModTriggerType.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModTriggerType.cs new file mode 100644 index 000000000..0c3eb2322 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModTriggerType.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// An enum representing the type of content which can trigger the rule. + /// + public enum AutoModTriggerType + { + /// + /// Check if content contains words from a user defined list of keywords. + /// + Keyword = 1, + + /// + /// Check if content contains any harmful links. + /// + HarmfulLink = 2, + + /// + /// Check if content represents generic spam. + /// + Spam = 3, + + /// + /// Check if content contains words from internal pre-defined wordsets. + /// + KeywordPreset = 4, + + /// + /// Check if content contains more unique mentions than allowed. + /// + MentionSpam = 5, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/IAutoModRule.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/IAutoModRule.cs new file mode 100644 index 000000000..c87ecf3dc --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/IAutoModRule.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a auto mod rule within a guild. + /// + public interface IAutoModRule : ISnowflakeEntity, IDeletable + { + /// + /// Gets the guild id on which this rule exists. + /// + ulong GuildId { get; } + + /// + /// Get the name of this rule. + /// + string Name { get; } + + /// + /// Gets the id of the user who created this use. + /// + ulong CreatorId { get; } + + /// + /// Gets the event type on which this rule is triggered. + /// + AutoModEventType EventType { get; } + + /// + /// Gets the trigger type on which this rule executes. + /// + AutoModTriggerType TriggerType { get; } + + /// + /// Gets the keyword filter for this rule. + /// + /// + /// This collection will be empty if is not + /// . + /// + public IReadOnlyCollection KeywordFilter { get; } + + /// + /// Gets regex patterns for this rule. Empty if the rule has no regexes. + /// + /// + /// This collection will be empty if is not + /// . + /// + public IReadOnlyCollection RegexPatterns { get; } + + /// + /// Gets the allow list patterns for this rule. Empty if the rule has no allowed terms. + /// + /// + /// This collection will be empty if is not + /// . + /// + public IReadOnlyCollection AllowList { get; } + + /// + /// Gets the preset keyword types for this rule. Empty if the rule has no presets. + /// + /// + /// This collection will be empty if is not + /// . + /// + public IReadOnlyCollection Presets { get; } + + /// + /// Gets the total mention limit for this rule. + /// + /// + /// This property will be if is not + /// . + /// + public int? MentionTotalLimit { get; } + + /// + /// Gets a collection of actions that will be preformed if a user breaks this rule. + /// + IReadOnlyCollection Actions { get; } + + /// + /// Gets whether or not this rule is enabled. + /// + bool Enabled { get; } + + /// + /// Gets a collection of role ids that are exempt from this rule. Empty if the rule has no exempt roles. + /// + IReadOnlyCollection ExemptRoles { get; } + + /// + /// Gets a collection of channel ids that are exempt from this rule. Empty if the rule has no exempt channels. + /// + IReadOnlyCollection ExemptChannels { get; } + + /// + /// Modifies this rule. + /// + /// The delegate containing the properties to modify the rule with. + /// The options to be used when sending the request. + Task ModifyAsync(Action func, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/AutoModeration/KeywordPresetTypes.cs b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/KeywordPresetTypes.cs new file mode 100644 index 000000000..a399cdc6f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/AutoModeration/KeywordPresetTypes.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// An enum representing preset filter types. + /// + public enum KeywordPresetTypes + { + /// + /// Words that may be considered forms of swearing or cursing. + /// + Profanity = 1, + + /// + /// Words that refer to sexually explicit behavior or activity. + /// + SexualContent = 2, + + /// + /// Personal insults or words that may be considered hate speech. + /// + Slurs = 3, + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 63f4a2280..a0b95759d 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1267,5 +1267,29 @@ namespace Discord /// A task that represents the asynchronous creation operation. The task result contains a . /// Task ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null); + + /// + /// Get a list of all rules currently configured for the guild. + /// + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of . + /// + Task GetAutoModRulesAsync(RequestOptions options = null); + + /// + /// Gets a single rule configured in a guild. Returns if the rule was not found. + /// + /// + /// A task that represents the asynchronous creation operation. The task result contains a . + /// + Task GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null); + + /// + /// Creates a new auto moderation rule. + /// + /// + /// A task that represents the asynchronous creation operation. The task result contains the created . + /// + Task CreateAutoModRuleAsync(Action props, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs index e9dd8f814..b9d715659 100644 --- a/src/Discord.Net.Core/GatewayIntents.cs +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -48,13 +48,25 @@ namespace Discord /// This intent includes GUILD_SCHEDULED_EVENT_CREATE, GUILD_SCHEDULED_EVENT_UPDATE, GUILD_SCHEDULED_EVENT_DELETE, GUILD_SCHEDULED_EVENT_USER_ADD, GUILD_SCHEDULED_EVENT_USER_REMOVE /// GuildScheduledEvents = 1 << 16, + + /// + /// This intent includes AUTO_MODERATION_RULE_CREATE, AUTO_MODERATION_RULE_UPDATE, AUTO_MODERATION_RULE_DELETE + /// + AutoModerationConfiguration = 1 << 20, + + /// + /// This intent includes AUTO_MODERATION_ACTION_EXECUTION + /// + AutoModerationActionExecution = 1 << 21, + /// /// This intent includes all but and /// which are privileged and must be enabled in the Developer Portal. /// AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | - DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, + DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents | AutoModerationConfiguration | + AutoModerationActionExecution, /// /// This intent includes all of them, including privileged ones. /// diff --git a/src/Discord.Net.Rest/API/Common/ActionMetadata.cs b/src/Discord.Net.Rest/API/Common/ActionMetadata.cs new file mode 100644 index 000000000..148c54cdd --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ActionMetadata.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ActionMetadata + { + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + + [JsonProperty("duration_seconds")] + public Optional DurationSeconds { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AutoModAction.cs b/src/Discord.Net.Rest/API/Common/AutoModAction.cs new file mode 100644 index 000000000..a6fec3c01 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AutoModAction.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class AutoModAction + { + [JsonProperty("type")] + public AutoModActionType Type { get; set; } + + [JsonProperty("metadata")] + public Optional Metadata { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/AutoModerationRule.cs b/src/Discord.Net.Rest/API/Common/AutoModerationRule.cs new file mode 100644 index 000000000..f30af5bee --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/AutoModerationRule.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class AutoModerationRule + { + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("creator_id")] + public ulong CreatorId { get; set; } + + [JsonProperty("event_type")] + public AutoModEventType EventType { get; set; } + + [JsonProperty("trigger_type")] + public AutoModTriggerType TriggerType { get; set; } + + [JsonProperty("trigger_metadata")] + public TriggerMetadata TriggerMetadata { get; set; } + + [JsonProperty("actions")] + public AutoModAction[] Actions { get; set; } + + [JsonProperty("enabled")] + public bool Enabled { get; set; } + + [JsonProperty("exempt_roles")] + public ulong[] ExemptRoles { get; set; } + + [JsonProperty("exempt_channels")] + public ulong[] ExemptChannels { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/TriggerMetadata.cs b/src/Discord.Net.Rest/API/Common/TriggerMetadata.cs new file mode 100644 index 000000000..214e464c9 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/TriggerMetadata.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class TriggerMetadata + { + [JsonProperty("keyword_filter")] + public Optional KeywordFilter { get; set; } + + [JsonProperty("regex_patterns")] + public Optional RegexPatterns { get; set; } + + [JsonProperty("presets")] + public Optional Presets { get; set; } + + [JsonProperty("allow_list")] + public Optional AllowList { get; set; } + + [JsonProperty("mention_total_limit")] + public Optional MentionLimit { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs b/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs new file mode 100644 index 000000000..5438700c6 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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/API/Rest/ModifyAutoModRuleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyAutoModRuleParams.cs new file mode 100644 index 000000000..3cdb4eac5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyAutoModRuleParams.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ModifyAutoModRuleParams + { + public Optional Name { get; set; } + public Optional EventType { get; set; } + public Optional TriggerType { get; set; } + public Optional TriggerMetadata { get; set; } + public Optional Actions { get; set; } + public Optional Enabled { get; set; } + public Optional ExemptRoles { get; set; } + public Optional ExemptChannels { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c6e5c495e..23328ba24 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -2119,6 +2119,58 @@ namespace Discord.API #endregion + #region Guild AutoMod + + public async Task GetGuildAutoModRulesAsync(ulong guildId, RequestOptions options) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"guilds/{guildId}/auto-moderation/rules", new BucketIds(guildId: guildId), options: options); + } + + public async Task GetGuildAutoModRuleAsync(ulong guildId, ulong ruleId, RequestOptions options) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(ruleId, 0, nameof(ruleId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendAsync("GET", () => $"guilds/{guildId}/auto-moderation/rules/{ruleId}", new BucketIds(guildId), options: options); + } + + public async Task CreateGuildAutoModRuleAsync(ulong guildId, CreateAutoModRuleParams args, RequestOptions options) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("POST", () => $"guilds/{guildId}/auto-moderation/rules", args, new BucketIds(guildId: guildId), options: options); + } + + public async Task ModifyGuildAutoModRuleAsync(ulong guildId, ulong ruleId, ModifyAutoModRuleParams args, RequestOptions options) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(ruleId, 0, nameof(ruleId)); + + options = RequestOptions.CreateOrClone(options); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/auto-moderation/rules/{ruleId}", args, new BucketIds(guildId: guildId), options: options); + } + + public async Task DeleteGuildAutoModRuleAsync(ulong guildId, ulong ruleId, RequestOptions options) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(ruleId, 0, nameof(ruleId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", () => $"guilds/{guildId}/auto-moderation/rules/{ruleId}", new BucketIds(guildId: guildId), options: options); + } + + #endregion + #region Guild Welcome Screen public async Task GetGuildWelcomeScreenAsync(ulong guildId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 5407f0ed0..552cb9284 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1062,5 +1062,170 @@ namespace Discord.Rest } #endregion + + #region Auto Mod + + public static async Task CreateAutoModRuleAsync(IGuild guild, Action func, BaseDiscordClient client, RequestOptions options) + { + var args = new AutoModRuleProperties(); + func(args); + + if (!args.TriggerType.IsSpecified) + throw new ArgumentException(message: $"AutoMod rule must have a specified type.", paramName: nameof(args.TriggerType)); + + if (!args.Name.IsSpecified || string.IsNullOrWhiteSpace(args.Name.Value)) + throw new ArgumentException("Name of the rule must not be empty", paramName: nameof(args.Name)); + + Preconditions.AtLeast(1, args.Actions.GetValueOrDefault(Array.Empty()).Length, nameof(args.Actions), "Auto moderation rule must have at least 1 action"); + + #region Keyword Validations + + if (args.RegexPatterns.IsSpecified) + { + if (args.TriggerType.Value is not AutoModTriggerType.Keyword) + throw new ArgumentException(message: $"Regex patterns can only be used with 'Keyword' trigger type.", paramName: nameof(args.RegexPatterns)); + + Preconditions.AtMost(args.RegexPatterns.Value.Length, AutoModRuleProperties.MaxRegexPatternCount, nameof(args.RegexPatterns), $"Regex pattern count must be less than or equal to {AutoModRuleProperties.MaxRegexPatternCount}."); + + if (args.RegexPatterns.Value.Any(x => x.Length > AutoModRuleProperties.MaxRegexPatternLength)) + throw new ArgumentException(message: $"Regex pattern must be less than or equal to {AutoModRuleProperties.MaxRegexPatternLength}.", paramName: nameof(args.RegexPatterns)); + } + + if (args.KeywordFilter.IsSpecified) + { + if (args.TriggerType.Value != AutoModTriggerType.Keyword) + throw new ArgumentException(message: $"Keyword filter can only be used with 'Keyword' trigger type.", paramName: nameof(args.KeywordFilter)); + + Preconditions.AtMost(args.KeywordFilter.Value.Length, AutoModRuleProperties.MaxKeywordCount, nameof(args.KeywordFilter), $"Keyword count must be less than or equal to {AutoModRuleProperties.MaxKeywordCount}"); + + if (args.KeywordFilter.Value.Any(x => x.Length > AutoModRuleProperties.MaxKeywordLength)) + throw new ArgumentException(message: $"Keyword length must be less than or equal to {AutoModRuleProperties.MaxKeywordLength}.", paramName: nameof(args.KeywordFilter)); + } + + if (args.TriggerType.Value is AutoModTriggerType.Keyword) + Preconditions.AtLeast(args.KeywordFilter.GetValueOrDefault(Array.Empty()).Length + args.RegexPatterns.GetValueOrDefault(Array.Empty()).Length, 1, "KeywordFilter & RegexPatterns","Auto moderation rule must have at least 1 keyword or regex pattern"); + + if (args.AllowList.IsSpecified) + { + if (args.TriggerType.Value 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(args.AllowList)); + + if (args.TriggerType.Value is AutoModTriggerType.Keyword) + Preconditions.AtMost(args.AllowList.Value.Length, AutoModRuleProperties.MaxAllowListCountKeyword, nameof(args.AllowList), $"Allow list entry count must be less than or equal to {AutoModRuleProperties.MaxAllowListCountKeyword}."); + + if (args.TriggerType.Value is AutoModTriggerType.KeywordPreset) + Preconditions.AtMost(args.AllowList.Value.Length, AutoModRuleProperties.MaxAllowListCountKeywordPreset, nameof(args.AllowList), $"Allow list entry count must be less than or equal to {AutoModRuleProperties.MaxAllowListCountKeywordPreset}."); + + if (args.AllowList.Value.Any(x => x.Length > AutoModRuleProperties.MaxAllowListEntryLength)) + throw new ArgumentException(message: $"Allow list entry length must be less than or equal to {AutoModRuleProperties.MaxAllowListEntryLength}.", paramName: nameof(args.AllowList)); + + } + + if (args.TriggerType.Value is not AutoModTriggerType.KeywordPreset && args.Presets.IsSpecified) + throw new ArgumentException(message: $"Keyword presets scan only be used with 'KeywordPreset' trigger type.", paramName: nameof(args.Presets)); + + #endregion + + if (args.MentionLimit.IsSpecified) + { + if (args.TriggerType.Value is AutoModTriggerType.MentionSpam) + { + Preconditions.AtMost(args.MentionLimit.Value, AutoModRuleProperties.MaxMentionLimit, nameof(args.MentionLimit), $"Mention limit must be less or equal to {AutoModRuleProperties.MaxMentionLimit}"); + Preconditions.AtLeast(args.MentionLimit.Value, 1, nameof(args.MentionLimit), $"Mention limit must be greater or equal to 1"); + } + else + { + throw new ArgumentException(message: $"MentionLimit can only be used with 'MentionSpam' trigger type.", paramName: nameof(args.MentionLimit)); + } + } + + if (args.ExemptRoles.IsSpecified) + Preconditions.AtMost(args.ExemptRoles.Value.Length, AutoModRuleProperties.MaxExemptRoles, nameof(args.ExemptRoles), $"Exempt roles count must be less than or equal to {AutoModRuleProperties.MaxExemptRoles}."); + + if (args.ExemptChannels.IsSpecified) + Preconditions.AtMost(args.ExemptChannels.Value.Length, AutoModRuleProperties.MaxExemptChannels, nameof(args.ExemptChannels), $"Exempt channels count must be less than or equal to {AutoModRuleProperties.MaxExemptChannels}."); + + if (!args.Actions.IsSpecified && args.Actions.Value.Length == 0) + { + throw new ArgumentException(message: $"At least 1 action must be set for an auto moderation rule.", paramName: nameof(args.Actions)); + } + + if (args.Actions.Value.Any(x => x.TimeoutDuration.GetValueOrDefault().TotalSeconds > AutoModRuleProperties.MaxTimeoutSeconds)) + throw new ArgumentException(message: $"Field count must be less than or equal to {AutoModRuleProperties.MaxTimeoutSeconds}.", paramName: nameof(AutoModRuleActionProperties.TimeoutDuration)); + + var props = new CreateAutoModRuleParams + { + EventType = args.EventType.GetValueOrDefault(AutoModEventType.MessageSend), + Enabled = args.Enabled.GetValueOrDefault(true), + ExemptRoles = args.ExemptRoles.GetValueOrDefault(), + ExemptChannels = args.ExemptChannels.GetValueOrDefault(), + Name = args.Name.Value, + TriggerType = args.TriggerType.Value, + Actions = args.Actions.Value.Select(x => new AutoModAction + { + Metadata = new ActionMetadata + { + ChannelId = x.ChannelId ?? Optional.Unspecified, + DurationSeconds = (int?)x.TimeoutDuration?.TotalSeconds ?? Optional.Unspecified + }, + Type = x.Type + }).ToArray(), + TriggerMetadata = new TriggerMetadata + { + AllowList = args.AllowList, + KeywordFilter = args.KeywordFilter, + MentionLimit = args.MentionLimit, + Presets = args.Presets, + RegexPatterns = args.RegexPatterns, + }, + }; + + return await client.ApiClient.CreateGuildAutoModRuleAsync(guild.Id, props, options); + } + + public static async Task GetAutoModRuleAsync(ulong ruleId, IGuild guild, BaseDiscordClient client, RequestOptions options) + => await client.ApiClient.GetGuildAutoModRuleAsync(guild.Id, ruleId, options); + + public static async Task GetAutoModRulesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + => await client.ApiClient.GetGuildAutoModRulesAsync(guild.Id, options); + + public static Task ModifyRuleAsync(BaseDiscordClient client, IAutoModRule rule, Action func, RequestOptions options) + { + var args = new AutoModRuleProperties(); + func(args); + + var apiArgs = new API.Rest.ModifyAutoModRuleParams + { + Actions = args.Actions.IsSpecified ? args.Actions.Value.Select(x => new API.AutoModAction() + { + Type = x.Type, + Metadata = x.ChannelId.HasValue || x.TimeoutDuration.HasValue ? new API.ActionMetadata + { + ChannelId = x.ChannelId ?? Optional.Unspecified, + DurationSeconds = x.TimeoutDuration.HasValue ? (int)Math.Floor(x.TimeoutDuration.Value.TotalSeconds) : Optional.Unspecified + } : Optional.Unspecified + }).ToArray() : Optional.Unspecified, + Enabled = args.Enabled, + EventType = args.EventType, + ExemptChannels = args.ExemptChannels, + ExemptRoles = args.ExemptRoles, + Name = args.Name, + TriggerType = args.TriggerType, + TriggerMetadata = args.KeywordFilter.IsSpecified || args.Presets.IsSpecified ? new API.TriggerMetadata + { + KeywordFilter = args.KeywordFilter.GetValueOrDefault(Array.Empty()), + RegexPatterns = args.RegexPatterns.GetValueOrDefault(Array.Empty()), + AllowList = args.AllowList.GetValueOrDefault(Array.Empty()), + MentionLimit = args.MentionLimit, + Presets = args.Presets.GetValueOrDefault(Array.Empty()) + } : Optional.Unspecified + }; + + return client.ApiClient.ModifyGuildAutoModRuleAsync(rule.GuildId, rule.Id, apiArgs, options); + } + + public static Task DeleteRuleAsync(BaseDiscordClient client, IAutoModRule rule, RequestOptions options) + => client.ApiClient.DeleteGuildAutoModRuleAsync(rule.GuildId, rule.Id, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs b/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs new file mode 100644 index 000000000..f2dc4c9b7 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Model = Discord.API.AutoModerationRule; + +namespace Discord.Rest; + +public class RestAutoModRule : RestEntity, IAutoModRule +{ + /// + public DateTimeOffset CreatedAt { get; private set; } + + /// + public ulong GuildId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public ulong CreatorId { get; private set; } + + /// + public AutoModEventType EventType { get; private set; } + + /// + public AutoModTriggerType TriggerType { get; private set; } + + /// + public IReadOnlyCollection KeywordFilter { get; private set; } + + /// + public IReadOnlyCollection RegexPatterns { get; private set; } + + /// + public IReadOnlyCollection AllowList { get; private set; } + + /// + public IReadOnlyCollection Presets { get; private set; } + + /// + public int? MentionTotalLimit { get; private set; } + + /// + public IReadOnlyCollection Actions { get; private set; } + + /// + public bool Enabled { get; private set; } + + /// + public IReadOnlyCollection ExemptRoles { get; private set; } + + /// + public IReadOnlyCollection ExemptChannels { get; private set; } + + internal RestAutoModRule(BaseDiscordClient discord, ulong id) : base(discord, id) + { + + } + + internal static RestAutoModRule Create(BaseDiscordClient discord, Model model) + { + var entity = new RestAutoModRule(discord, model.Id); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + CreatorId = model.CreatorId; + GuildId = model.GuildId; + + EventType = model.EventType; + TriggerType = model.TriggerType; + KeywordFilter = model.TriggerMetadata.KeywordFilter.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + Presets = model.TriggerMetadata.Presets.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + RegexPatterns = model.TriggerMetadata.RegexPatterns.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + AllowList = model.TriggerMetadata.AllowList.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + MentionTotalLimit = model.TriggerMetadata.MentionLimit.IsSpecified + ? model.TriggerMetadata.MentionLimit.Value + : null; + Actions = model.Actions.Select(x => new AutoModRuleAction(x.Type, x.Metadata.GetValueOrDefault()?.ChannelId.ToNullable(), x.Metadata.GetValueOrDefault()?.DurationSeconds.ToNullable())).ToImmutableArray(); + Enabled = model.Enabled; + ExemptRoles = model.ExemptRoles.ToImmutableArray(); + ExemptChannels = model.ExemptChannels.ToImmutableArray(); + } + + /// + 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) + => 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 10e0acc58..9ca01ead2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1197,6 +1197,34 @@ namespace Discord.Rest RequestOptions options = null) => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); + #endregion + + #region AutoMod + + + /// + public async Task GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null) + { + var rule = await GuildHelper.GetAutoModRuleAsync(ruleId, this, Discord, options); + return RestAutoModRule.Create(Discord, rule); + } + + /// + public async Task GetAutoModRulesAsync(RequestOptions options = null) + { + var rules = await GuildHelper.GetAutoModRulesAsync(this, Discord, options); + return rules.Select(x => RestAutoModRule.Create(Discord, x)).ToArray(); + } + + /// + public async Task CreateAutoModRuleAsync(Action props, RequestOptions options = null) + { + var rule = await GuildHelper.CreateAutoModRuleAsync(this, props, Discord, options); + + return RestAutoModRule.Create(Discord, rule); + } + + #endregion #region IGuild @@ -1543,6 +1571,19 @@ namespace Discord.Rest public Task ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null) => GuildHelper.ModifyWelcomeScreenAsync(enabled, description, channels, this, Discord, options); + + /// + async Task IGuild.GetAutoModRuleAsync(ulong ruleId, RequestOptions options) + => await GetAutoModRuleAsync(ruleId, options).ConfigureAwait(false); + + /// + async Task IGuild.GetAutoModRulesAsync(RequestOptions options) + => await GetAutoModRulesAsync(options).ConfigureAwait(false); + + /// + async Task IGuild.CreateAutoModRuleAsync(Action props, RequestOptions options) + => await CreateAutoModRuleAsync(props, options).ConfigureAwait(false); + #endregion } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/AutoModActionExecutedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/AutoModActionExecutedEvent.cs new file mode 100644 index 000000000..44d2834d3 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/AutoModActionExecutedEvent.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +namespace Discord.API.Gateway; + +internal class AutoModActionExecutedEvent +{ + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("action")] + public Discord.API.AutoModAction Action { get; set; } + + [JsonProperty("rule_id")] + public ulong RuleId { get; set; } + + [JsonProperty("rule_trigger_type")] + public AutoModTriggerType TriggerType { get; set; } + + [JsonProperty("user_id")] + public ulong UserId { get; set; } + + [JsonProperty("channel_id")] + public Optional ChannelId { get; set; } + + [JsonProperty("message_id")] + public Optional MessageId { get; set; } + + [JsonProperty("alert_system_message_id")] + public Optional AlertSystemMessageId { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + + [JsonProperty("matched_keyword")] + public Optional MatchedKeyword { get; set; } + + [JsonProperty("matched_content")] + public Optional MatchedContent { get; set; } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index fb2110399..bc97139e9 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -892,5 +892,49 @@ namespace Discord.WebSocket internal readonly AsyncEvent> _webhooksUpdated = new AsyncEvent>(); #endregion + + #region AutoModeration + + /// + /// Fired when an auto moderation rule is created. + /// + public event Func AutoModRuleCreated + { + add => _autoModRuleCreated.Add(value); + remove => _autoModRuleCreated.Remove(value); + } + internal readonly AsyncEvent> _autoModRuleCreated = new (); + + /// + /// Fired when an auto moderation rule is modified. + /// + public event Func, SocketAutoModRule, Task> AutoModRuleUpdated + { + add => _autoModRuleUpdated.Add(value); + remove => _autoModRuleUpdated.Remove(value); + } + internal readonly AsyncEvent, SocketAutoModRule, Task>> _autoModRuleUpdated = new (); + + /// + /// Fired when an auto moderation rule is deleted. + /// + public event Func AutoModRuleDeleted + { + add => _autoModRuleDeleted.Add(value); + remove => _autoModRuleDeleted.Remove(value); + } + internal readonly AsyncEvent> _autoModRuleDeleted = new (); + + /// + /// Fired when an auto moderation rule is triggered by a user. + /// + public event Func AutoModActionExecuted + { + add => _autoModActionExecuted.Add(value); + remove => _autoModActionExecuted.Remove(value); + } + internal readonly AsyncEvent> _autoModActionExecuted = new (); + + #endregion } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 9a0f67b0b..6ad323d9a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -6,8 +6,10 @@ using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; using Discord.Utils; + using Newtonsoft.Json; using Newtonsoft.Json.Linq; + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -16,6 +18,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; + using GameModel = Discord.API.Game; namespace Discord.WebSocket @@ -2882,6 +2885,132 @@ namespace Discord.WebSocket #endregion + #region Auto Moderation + + case "AUTO_MODERATION_RULE_CREATE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var rule = guild.AddOrUpdateAutoModRule(data); + + await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule); + } + break; + + case "AUTO_MODERATION_RULE_UPDATE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var cachedRule = guild.GetAutoModRule(data.Id); + var cacheableBefore = new Cacheable(cachedRule?.Clone(), + data.Id, + cachedRule is not null, + async () => await guild.GetAutoModRuleAsync(data.Id)); + + await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data)); + } + break; + + case "AUTO_MODERATION_RULE_DELETE": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + var rule = guild.RemoveAutoModRule(data); + + await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule); + } + break; + + case "AUTO_MODERATION_ACTION_EXECUTION": + { + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + var action = new AutoModRuleAction(data.Action.Type, + data.Action.Metadata.IsSpecified + ? data.Action.Metadata.Value.ChannelId.IsSpecified + ? data.Action.Metadata.Value.ChannelId.Value + : null + : null, + data.Action.Metadata.IsSpecified + ? data.Action.Metadata.Value.DurationSeconds.IsSpecified + ? data.Action.Metadata.Value.DurationSeconds.Value + : null + : null); + + var member = guild.GetUser(data.UserId); + + var cacheableUser = new Cacheable(member, + data.UserId, + member is not null, + async () => + { + var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId); + return guild.AddOrUpdateUser(model); + } + ); + + ISocketMessageChannel channel = null; + if (data.ChannelId.IsSpecified) + channel = GetChannel(data.ChannelId.Value) as ISocketMessageChannel; + + var cacheableChannel = new Cacheable(channel, + data.ChannelId.GetValueOrDefault(0), + channel != null, + async () => + { + if(data.ChannelId.IsSpecified) + return await GetChannelAsync(data.ChannelId.Value).ConfigureAwait(false) as ISocketMessageChannel; + return null; + }); + + + var cachedMsg = channel?.GetCachedMessage(data.MessageId.GetValueOrDefault(0)) as IUserMessage; + + var cacheableMessage = new Cacheable(cachedMsg, + data.MessageId.GetValueOrDefault(0), + cachedMsg is not null, + async () => + { + if(data.MessageId.IsSpecified) + return (await channel.GetMessageAsync(data.MessageId.Value).ConfigureAwait(false)) as IUserMessage; + return null; + }); + + var cachedRule = guild.GetAutoModRule(data.RuleId); + + var cacheableRule = new Cacheable(cachedRule, + data.RuleId, + cachedRule is not null, + async () => await guild.GetAutoModRuleAsync(data.RuleId)); + + var eventData = new AutoModActionExecutedData( + cacheableRule, + data.TriggerType, + cacheableUser, + cacheableChannel, + cachedMsg is not null ? cacheableMessage : null, + data.AlertSystemMessageId.GetValueOrDefault(0), + data.Content, + data.MatchedContent.IsSpecified + ? data.MatchedContent.Value + : null, + data.MatchedKeyword.IsSpecified + ? data.MatchedKeyword.Value + : null); + + await TimedInvokeAsync(_autoModActionExecuted, nameof(AutoModActionExecuted), guild, action, eventData); + } + break; + + #endregion + #region Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/AutoModActionExecutedData.cs b/src/Discord.Net.WebSocket/Entities/Guilds/AutoModActionExecutedData.cs new file mode 100644 index 000000000..0643c1500 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Guilds/AutoModActionExecutedData.cs @@ -0,0 +1,86 @@ +using Discord.Rest; + +namespace Discord.WebSocket; + +public class AutoModActionExecutedData +{ + /// + /// Gets the id of the rule which action belongs to. + /// + public Cacheable Rule { get; } + + /// + /// Gets the trigger type of rule which was triggered. + /// + public AutoModTriggerType TriggerType { get; } + + /// + /// Gets the user which generated the content which triggered the rule. + /// + public Cacheable User { get; } + + /// + /// Gets the channel in which user content was posted. + /// + public Cacheable Channel { get; } + + /// + /// Gets the message that triggered the action. + /// + /// + /// This property will be if the message was blocked by the automod. + /// + public Cacheable? Message { get; } + + /// + /// Gets the id of the system auto moderation messages posted as a result of this action. + /// + /// + /// This property will be if this event does not correspond to an action + /// with type . + /// + public ulong AlertMessageId { get; } + + /// + /// Gets the user-generated text content. + /// + /// + /// This property will be empty if is disabled. + /// + public string Content { get; } + + /// + /// Gets the substring in content that triggered the rule. + /// + /// + /// This property will be empty if is disabled. + /// + public string MatchedContent { get; } + + /// + /// Gets the word or phrase configured in the rule that triggered the rule. + /// + public string MatchedKeyword { get; } + + internal AutoModActionExecutedData(Cacheable rule, + AutoModTriggerType triggerType, + Cacheable user, + Cacheable channel, + Cacheable? message, + ulong alertMessageId, + string content, + string matchedContent, + string matchedKeyword + ) + { + Rule = rule; + TriggerType = triggerType; + User = user; + Channel = channel; + Message = message; + AlertMessageId = alertMessageId; + Content = content; + MatchedContent = matchedContent; + MatchedKeyword = matchedKeyword; + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs new file mode 100644 index 000000000..d562c5a2d --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs @@ -0,0 +1,122 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.AutoModerationRule; + +namespace Discord.WebSocket +{ + public class SocketAutoModRule : SocketEntity, IAutoModRule + { + /// + /// Gets the guild that this rule is in. + /// + public SocketGuild Guild { get; } + + /// + public string Name { get; private set; } + + /// + /// Gets the creator of this rule. + /// + public SocketGuildUser Creator { get; private set; } + + /// + public AutoModEventType EventType { get; private set; } + + /// + public AutoModTriggerType TriggerType { get; private set; } + + /// + public IReadOnlyCollection KeywordFilter { get; private set; } + + /// + public IReadOnlyCollection RegexPatterns { get; private set; } + + /// + public IReadOnlyCollection AllowList { get; private set; } + + /// + public IReadOnlyCollection Presets { get; private set; } + + /// + public IReadOnlyCollection Actions { get; private set; } + + /// + public int? MentionTotalLimit { get; private set; } + + /// + public bool Enabled { get; private set; } + + /// + /// Gets the roles that are exempt from this rule. + /// + public IReadOnlyCollection ExemptRoles { get; private set; } + + /// + /// Gets the channels that are exempt from this rule. + /// + public IReadOnlyCollection ExemptChannels { get; private set; } + + /// + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(Id); + + private ulong _creatorId; + + internal SocketAutoModRule(DiscordSocketClient discord, ulong id, SocketGuild guild) + : base(discord, id) + { + Guild = guild; + } + + internal static SocketAutoModRule Create(DiscordSocketClient discord, SocketGuild guild, Model model) + { + var entity = new SocketAutoModRule(discord, model.Id, guild); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + Name = model.Name; + _creatorId = model.CreatorId; + Creator ??= Guild.GetUser(_creatorId); + EventType = model.EventType; + TriggerType = model.TriggerType; + KeywordFilter = model.TriggerMetadata.KeywordFilter.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + Presets = model.TriggerMetadata.Presets.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + RegexPatterns = model.TriggerMetadata.RegexPatterns.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + AllowList = model.TriggerMetadata.AllowList.GetValueOrDefault(Array.Empty()).ToImmutableArray(); + MentionTotalLimit = model.TriggerMetadata.MentionLimit.IsSpecified + ? model.TriggerMetadata.MentionLimit.Value + : null; + Actions = model.Actions.Select(x => new AutoModRuleAction(x.Type, x.Metadata.GetValueOrDefault()?.ChannelId.ToNullable(), x.Metadata.GetValueOrDefault()?.DurationSeconds.ToNullable())).ToImmutableArray(); + Enabled = model.Enabled; + ExemptRoles = model.ExemptRoles.Select(x => Guild.GetRole(x)).ToImmutableArray(); + ExemptChannels = model.ExemptChannels.Select(x => Guild.GetChannel(x)).ToImmutableArray(); + } + + /// + 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) + => GuildHelper.DeleteRuleAsync(Discord, this, options); + + internal SocketAutoModRule Clone() => MemberwiseClone() as SocketAutoModRule; + + #region IAutoModRule + IReadOnlyCollection IAutoModRule.ExemptRoles => ExemptRoles.Select(x => x.Id).ToImmutableArray(); + IReadOnlyCollection IAutoModRule.ExemptChannels => ExemptChannels.Select(x => x.Id).ToImmutableArray(); + ulong IAutoModRule.GuildId => Guild.Id; + ulong IAutoModRule.CreatorId => _creatorId; + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 94be8c902..abe8cff1f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using AutoModRuleModel = Discord.API.AutoModerationRule; using ChannelModel = Discord.API.Channel; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using EventModel = Discord.API.GuildScheduledEvent; @@ -43,6 +44,7 @@ namespace Discord.WebSocket private ConcurrentDictionary _voiceStates; private ConcurrentDictionary _stickers; private ConcurrentDictionary _events; + private ConcurrentDictionary _automodRules; private ImmutableArray _emotes; private AudioClient _audioClient; @@ -391,6 +393,7 @@ namespace Discord.WebSocket { _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); + _automodRules = new ConcurrentDictionary(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { @@ -1809,6 +1812,78 @@ namespace Discord.WebSocket internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; #endregion + #region AutoMod + + internal SocketAutoModRule AddOrUpdateAutoModRule(AutoModRuleModel model) + { + if (_automodRules.TryGetValue(model.Id, out var rule)) + { + rule.Update(model); + return rule; + } + + var socketRule = SocketAutoModRule.Create(Discord, this, model); + _automodRules.TryAdd(model.Id, socketRule); + return socketRule; + } + + /// + /// 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; + } + + internal SocketAutoModRule RemoveAutoModRule(ulong id) + { + return _automodRules.TryRemove(id, out var rule) ? rule : null; + } + + internal SocketAutoModRule RemoveAutoModRule(AutoModRuleModel model) + { + if (_automodRules.TryRemove(model.Id, out var rule)) + { + rule.Update(model); + } + + return rule ?? SocketAutoModRule.Create(Discord, this, model); + } + + /// + public async Task GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null) + { + var rule = await GuildHelper.GetAutoModRuleAsync(ruleId, this, Discord, options); + + return AddOrUpdateAutoModRule(rule); + } + + /// + public async Task GetAutoModRulesAsync(RequestOptions options = null) + { + var rules = await GuildHelper.GetAutoModRulesAsync(this, Discord, options); + + return rules.Select(AddOrUpdateAutoModRule).ToArray(); + } + + /// + public async Task CreateAutoModRuleAsync(Action props, RequestOptions options = null) + { + var rule = await GuildHelper.CreateAutoModRuleAsync(this, props, Discord, options); + + return AddOrUpdateAutoModRule(rule); + } + + /// + /// Gets the auto moderation rules defined in this guild. + /// + /// + /// This property may not always return all auto moderation rules if they haven't been cached. + /// + public IReadOnlyCollection AutoModRules => _automodRules.ToReadOnlyCollection(); + + #endregion + #region IGuild /// ulong? IGuild.AFKChannelId => AFKChannelId; @@ -2053,6 +2128,19 @@ namespace Discord.WebSocket _audioLock?.Dispose(); _audioClient?.Dispose(); } + + /// + async Task IGuild.GetAutoModRuleAsync(ulong ruleId, RequestOptions options) + => await GetAutoModRuleAsync(ruleId, options).ConfigureAwait(false); + + /// + async Task IGuild.GetAutoModRulesAsync(RequestOptions options) + => await GetAutoModRulesAsync(options).ConfigureAwait(false); + + /// + async Task IGuild.CreateAutoModRuleAsync(Action props, RequestOptions options) + => await CreateAutoModRuleAsync(props, options).ConfigureAwait(false); + #endregion } }