* initial implementation * update models * somewhat working auto mod action executed event * made some properties optional * comments, rest entity, guild methods * add placeholder methods * started working on rule cache * working events * started working on rule builder * working state * fix null issue * commentsssss * public automod rules collection in a socketgulild * forgot nullability * update limits * add Download func to cacheable user * Apply suggestions from code review * Update src/Discord.Net.Rest/DiscordRestApiClient.cs * missing xml doc * reworkkkk * fix the `;` lol --------- Co-authored-by: Quin Lynch <lynchquin@gmail.com> Co-authored-by: Casmir <68127614+csmir@users.noreply.github.com>pull/2241/merge
| @@ -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 | |||
| { | |||
| /// <summary> | |||
| /// Blocks the content of a message according to the rule. | |||
| /// </summary> | |||
| BlockMessage = 1, | |||
| /// <summary> | |||
| /// Logs user content to a specified channel. | |||
| /// </summary> | |||
| SendAlertMessage = 2, | |||
| /// <summary> | |||
| /// Timeout user for a specified duration. | |||
| /// </summary> | |||
| Timeout = 3, | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// An enum indecating in what event context a rule should be checked. | |||
| /// </summary> | |||
| public enum AutoModEventType | |||
| { | |||
| /// <summary> | |||
| /// When a member sends or edits a message in the guild. | |||
| /// </summary> | |||
| MessageSend = 1 | |||
| } | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents an action that will be preformed if a user breaks an <see cref="IAutoModRule"/>. | |||
| /// </summary> | |||
| public class AutoModRuleAction | |||
| { | |||
| /// <summary> | |||
| /// Gets the type for this action. | |||
| /// </summary> | |||
| public AutoModActionType Type { get; } | |||
| /// <summary> | |||
| /// Get the channel id on which to post alerts. <see langword="null"/> if no channel has been provided. | |||
| /// </summary> | |||
| public ulong? ChannelId { get; } | |||
| /// <summary> | |||
| /// Gets the duration of which a user will be timed out for breaking this rule. <see langword="null"/> if no timeout duration has been provided. | |||
| /// </summary> | |||
| public TimeSpan? TimeoutDuration { get; } | |||
| internal AutoModRuleAction(AutoModActionType type, ulong? channelId, int? duration) | |||
| { | |||
| Type = type; | |||
| ChannelId = channelId; | |||
| TimeoutDuration = duration.HasValue ? TimeSpan.FromSeconds(duration.Value) : null; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,151 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Provides properties used to modify a <see cref="IAutoModRule"/>. | |||
| /// </summary> | |||
| public class AutoModRuleProperties | |||
| { | |||
| /// <summary> | |||
| /// Returns the max keyword count for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxKeywordCount = 1000; | |||
| /// <summary> | |||
| /// Returns the max keyword length for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxKeywordLength = 60; | |||
| /// <summary> | |||
| /// Returns the max regex pattern count for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxRegexPatternCount = 10; | |||
| /// <summary> | |||
| /// Returns the max regex pattern length for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxRegexPatternLength = 260; | |||
| /// <summary> | |||
| /// Returns the max allowlist keyword count for a <see cref="AutoModTriggerType.Keyword"/> AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxAllowListCountKeyword = 100; | |||
| /// <summary> | |||
| /// Returns the max allowlist keyword count for a <see cref="AutoModTriggerType.KeywordPreset"/> AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxAllowListCountKeywordPreset = 1000; | |||
| /// <summary> | |||
| /// Returns the max allowlist keyword length for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxAllowListEntryLength = 60; | |||
| /// <summary> | |||
| /// Returns the max mention limit for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxMentionLimit = 50; | |||
| /// <summary> | |||
| /// Returns the max exempt role count for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxExemptRoles = 20; | |||
| /// <summary> | |||
| /// Returns the max exempt channel count for an AutoMod rule allowed by Discord. | |||
| /// </summary> | |||
| public const int MaxExemptChannels = 50; | |||
| /// <summary> | |||
| /// Returns the max timeout duration in seconds for an auto moderation rule action. | |||
| /// </summary> | |||
| public const int MaxTimeoutSeconds = 2419200; | |||
| /// <summary> | |||
| /// Gets or sets the name for the rule. | |||
| /// </summary> | |||
| public Optional<string> Name { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the event type for the rule. | |||
| /// </summary> | |||
| public Optional<AutoModEventType> EventType { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the trigger type for the rule. | |||
| /// </summary> | |||
| public Optional<AutoModTriggerType> TriggerType { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the keyword filter for the rule. | |||
| /// </summary> | |||
| public Optional<string[]> KeywordFilter { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets regex patterns for the rule. | |||
| /// </summary> | |||
| public Optional<string[]> RegexPatterns { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the allow list for the rule. | |||
| /// </summary> | |||
| public Optional<string[]> AllowList { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets total mention limit for the rule. | |||
| /// </summary> | |||
| public Optional<int> MentionLimit { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the presets for the rule. Empty if the rule has no presets. | |||
| /// </summary> | |||
| public Optional<KeywordPresetTypes[]> Presets { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the actions for the rule. | |||
| /// </summary> | |||
| public Optional<AutoModRuleActionProperties[]> Actions { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets whether or not the rule is enabled. | |||
| /// </summary> | |||
| public Optional<bool> Enabled { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the exempt roles for the rule. Empty if the rule has no exempt roles. | |||
| /// </summary> | |||
| public Optional<ulong[]> ExemptRoles { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the exempt channels for the rule. Empty if the rule has no exempt channels. | |||
| /// </summary> | |||
| public Optional<ulong[]> ExemptChannels { get; set; } | |||
| } | |||
| /// <summary> | |||
| /// Provides properties used to modify a <see cref="AutoModRuleAction"/>. | |||
| /// </summary> | |||
| public class AutoModRuleActionProperties | |||
| { | |||
| /// <summary> | |||
| /// Gets or sets the type for this action. | |||
| /// </summary> | |||
| public AutoModActionType Type { get; set; } | |||
| /// <summary> | |||
| /// Get or sets the channel id on which to post alerts. | |||
| /// </summary> | |||
| public ulong? ChannelId { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the duration of which a user will be timed out for breaking this rule. | |||
| /// </summary> | |||
| public TimeSpan? TimeoutDuration { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// An enum representing the type of content which can trigger the rule. | |||
| /// </summary> | |||
| public enum AutoModTriggerType | |||
| { | |||
| /// <summary> | |||
| /// Check if content contains words from a user defined list of keywords. | |||
| /// </summary> | |||
| Keyword = 1, | |||
| /// <summary> | |||
| /// Check if content contains any harmful links. | |||
| /// </summary> | |||
| HarmfulLink = 2, | |||
| /// <summary> | |||
| /// Check if content represents generic spam. | |||
| /// </summary> | |||
| Spam = 3, | |||
| /// <summary> | |||
| /// Check if content contains words from internal pre-defined wordsets. | |||
| /// </summary> | |||
| KeywordPreset = 4, | |||
| /// <summary> | |||
| /// Check if content contains more unique mentions than allowed. | |||
| /// </summary> | |||
| MentionSpam = 5, | |||
| } | |||
| } | |||
| @@ -0,0 +1,111 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// Represents a auto mod rule within a guild. | |||
| /// </summary> | |||
| public interface IAutoModRule : ISnowflakeEntity, IDeletable | |||
| { | |||
| /// <summary> | |||
| /// Gets the guild id on which this rule exists. | |||
| /// </summary> | |||
| ulong GuildId { get; } | |||
| /// <summary> | |||
| /// Get the name of this rule. | |||
| /// </summary> | |||
| string Name { get; } | |||
| /// <summary> | |||
| /// Gets the id of the user who created this use. | |||
| /// </summary> | |||
| ulong CreatorId { get; } | |||
| /// <summary> | |||
| /// Gets the event type on which this rule is triggered. | |||
| /// </summary> | |||
| AutoModEventType EventType { get; } | |||
| /// <summary> | |||
| /// Gets the trigger type on which this rule executes. | |||
| /// </summary> | |||
| AutoModTriggerType TriggerType { get; } | |||
| /// <summary> | |||
| /// Gets the keyword filter for this rule. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This collection will be empty if <see cref="TriggerType"/> is not | |||
| /// <see cref="AutoModTriggerType.Keyword"/>. | |||
| /// </remarks> | |||
| public IReadOnlyCollection<string> KeywordFilter { get; } | |||
| /// <summary> | |||
| /// Gets regex patterns for this rule. Empty if the rule has no regexes. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This collection will be empty if <see cref="TriggerType"/> is not | |||
| /// <see cref="AutoModTriggerType.Keyword"/>. | |||
| /// </remarks> | |||
| public IReadOnlyCollection<string> RegexPatterns { get; } | |||
| /// <summary> | |||
| /// Gets the allow list patterns for this rule. Empty if the rule has no allowed terms. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This collection will be empty if <see cref="TriggerType"/> is not | |||
| /// <see cref="AutoModTriggerType.Keyword"/>. | |||
| /// </remarks> | |||
| public IReadOnlyCollection<string> AllowList { get; } | |||
| /// <summary> | |||
| /// Gets the preset keyword types for this rule. Empty if the rule has no presets. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This collection will be empty if <see cref="TriggerType"/> is not | |||
| /// <see cref="AutoModTriggerType.KeywordPreset"/>. | |||
| /// </remarks> | |||
| public IReadOnlyCollection<KeywordPresetTypes> Presets { get; } | |||
| /// <summary> | |||
| /// Gets the total mention limit for this rule. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property will be <see langword="null"/> if <see cref="TriggerType"/> is not | |||
| /// <see cref="AutoModTriggerType.MentionSpam"/>. | |||
| /// </remarks> | |||
| public int? MentionTotalLimit { get; } | |||
| /// <summary> | |||
| /// Gets a collection of actions that will be preformed if a user breaks this rule. | |||
| /// </summary> | |||
| IReadOnlyCollection<AutoModRuleAction> Actions { get; } | |||
| /// <summary> | |||
| /// Gets whether or not this rule is enabled. | |||
| /// </summary> | |||
| bool Enabled { get; } | |||
| /// <summary> | |||
| /// Gets a collection of role ids that are exempt from this rule. Empty if the rule has no exempt roles. | |||
| /// </summary> | |||
| IReadOnlyCollection<ulong> ExemptRoles { get; } | |||
| /// <summary> | |||
| /// Gets a collection of channel ids that are exempt from this rule. Empty if the rule has no exempt channels. | |||
| /// </summary> | |||
| IReadOnlyCollection<ulong> ExemptChannels { get; } | |||
| /// <summary> | |||
| /// Modifies this rule. | |||
| /// </summary> | |||
| /// <param name="func">The delegate containing the properties to modify the rule with.</param> | |||
| /// <param name="options">The options to be used when sending the request.</param> | |||
| Task ModifyAsync(Action<AutoModRuleProperties> func, RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// An enum representing preset filter types. | |||
| /// </summary> | |||
| public enum KeywordPresetTypes | |||
| { | |||
| /// <summary> | |||
| /// Words that may be considered forms of swearing or cursing. | |||
| /// </summary> | |||
| Profanity = 1, | |||
| /// <summary> | |||
| /// Words that refer to sexually explicit behavior or activity. | |||
| /// </summary> | |||
| SexualContent = 2, | |||
| /// <summary> | |||
| /// Personal insults or words that may be considered hate speech. | |||
| /// </summary> | |||
| Slurs = 3, | |||
| } | |||
| } | |||
| @@ -1267,5 +1267,29 @@ namespace Discord | |||
| /// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>. | |||
| /// </returns> | |||
| Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Get a list of all rules currently configured for the guild. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. The task result contains a collection of <see cref="IAutoModRule"/>. | |||
| /// </returns> | |||
| Task<IAutoModRule[]> GetAutoModRulesAsync(RequestOptions options = null); | |||
| /// <summary> | |||
| /// Gets a single rule configured in a guild. Returns <see langword="null"/> if the rule was not found. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. The task result contains a <see cref="IAutoModRule"/>. | |||
| /// </returns> | |||
| Task<IAutoModRule> GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null); | |||
| /// <summary> | |||
| /// Creates a new auto moderation rule. | |||
| /// </summary> | |||
| /// <returns> | |||
| /// A task that represents the asynchronous creation operation. The task result contains the created <see cref="IAutoModRule"/>. | |||
| /// </returns> | |||
| Task<IAutoModRule> CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options = null); | |||
| } | |||
| } | |||
| @@ -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 | |||
| /// </summary> | |||
| GuildScheduledEvents = 1 << 16, | |||
| /// <summary> | |||
| /// This intent includes AUTO_MODERATION_RULE_CREATE, AUTO_MODERATION_RULE_UPDATE, AUTO_MODERATION_RULE_DELETE | |||
| /// </summary> | |||
| AutoModerationConfiguration = 1 << 20, | |||
| /// <summary> | |||
| /// This intent includes AUTO_MODERATION_ACTION_EXECUTION | |||
| /// </summary> | |||
| AutoModerationActionExecution = 1 << 21, | |||
| /// <summary> | |||
| /// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/> | |||
| /// which are privileged and must be enabled in the Developer Portal. | |||
| /// </summary> | |||
| AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | | |||
| GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | | |||
| DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents, | |||
| DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents | AutoModerationConfiguration | | |||
| AutoModerationActionExecution, | |||
| /// <summary> | |||
| /// This intent includes all of them, including privileged ones. | |||
| /// </summary> | |||
| @@ -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<ulong> ChannelId { get; set; } | |||
| [JsonProperty("duration_seconds")] | |||
| public Optional<int> DurationSeconds { get; set; } | |||
| } | |||
| } | |||
| @@ -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<ActionMetadata> Metadata { get; set; } | |||
| } | |||
| } | |||
| @@ -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; } | |||
| } | |||
| } | |||
| @@ -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<string[]> KeywordFilter { get; set; } | |||
| [JsonProperty("regex_patterns")] | |||
| public Optional<string[]> RegexPatterns { get; set; } | |||
| [JsonProperty("presets")] | |||
| public Optional<KeywordPresetTypes[]> Presets { get; set; } | |||
| [JsonProperty("allow_list")] | |||
| public Optional<string[]> AllowList { get; set; } | |||
| [JsonProperty("mention_total_limit")] | |||
| public Optional<int> MentionLimit { get; set; } | |||
| } | |||
| } | |||
| @@ -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> TriggerMetadata { get; set; } | |||
| [JsonProperty("actions")] | |||
| public AutoModAction[] Actions { get; set; } | |||
| [JsonProperty("enabled")] | |||
| public Optional<bool> Enabled { get; set; } | |||
| [JsonProperty("exempt_roles")] | |||
| public Optional<ulong[]> ExemptRoles { get; set; } | |||
| [JsonProperty("exempt_channels")] | |||
| public Optional<ulong[]> ExemptChannels { get; set; } | |||
| } | |||
| } | |||
| @@ -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<string> Name { get; set; } | |||
| public Optional<AutoModEventType> EventType { get; set; } | |||
| public Optional<AutoModTriggerType> TriggerType { get; set; } | |||
| public Optional<TriggerMetadata> TriggerMetadata { get; set; } | |||
| public Optional<AutoModAction[]> Actions { get; set; } | |||
| public Optional<bool> Enabled { get; set; } | |||
| public Optional<ulong[]> ExemptRoles { get; set; } | |||
| public Optional<ulong[]> ExemptChannels { get; set; } | |||
| } | |||
| } | |||
| @@ -2119,6 +2119,58 @@ namespace Discord.API | |||
| #endregion | |||
| #region Guild AutoMod | |||
| public async Task<AutoModerationRule[]> GetGuildAutoModRulesAsync(ulong guildId, RequestOptions options) | |||
| { | |||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||
| options = RequestOptions.CreateOrClone(options); | |||
| return await SendAsync<AutoModerationRule[]>("GET", () => $"guilds/{guildId}/auto-moderation/rules", new BucketIds(guildId: guildId), options: options); | |||
| } | |||
| public async Task<AutoModerationRule> 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<AutoModerationRule>("GET", () => $"guilds/{guildId}/auto-moderation/rules/{ruleId}", new BucketIds(guildId), options: options); | |||
| } | |||
| public async Task<AutoModerationRule> CreateGuildAutoModRuleAsync(ulong guildId, CreateAutoModRuleParams args, RequestOptions options) | |||
| { | |||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||
| options = RequestOptions.CreateOrClone(options); | |||
| return await SendJsonAsync<AutoModerationRule>("POST", () => $"guilds/{guildId}/auto-moderation/rules", args, new BucketIds(guildId: guildId), options: options); | |||
| } | |||
| public async Task<AutoModerationRule> 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<AutoModerationRule>("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<WelcomeScreen> GetGuildWelcomeScreenAsync(ulong guildId, RequestOptions options = null) | |||
| @@ -1062,5 +1062,170 @@ namespace Discord.Rest | |||
| } | |||
| #endregion | |||
| #region Auto Mod | |||
| public static async Task<AutoModerationRule> CreateAutoModRuleAsync(IGuild guild, Action<AutoModRuleProperties> 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<AutoModRuleActionProperties>()).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<string>()).Length + args.RegexPatterns.GetValueOrDefault(Array.Empty<string>()).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<ulong>.Unspecified, | |||
| DurationSeconds = (int?)x.TimeoutDuration?.TotalSeconds ?? Optional<int>.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<AutoModerationRule> GetAutoModRuleAsync(ulong ruleId, IGuild guild, BaseDiscordClient client, RequestOptions options) | |||
| => await client.ApiClient.GetGuildAutoModRuleAsync(guild.Id, ruleId, options); | |||
| public static async Task<AutoModerationRule[]> GetAutoModRulesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) | |||
| => await client.ApiClient.GetGuildAutoModRulesAsync(guild.Id, options); | |||
| public static Task<AutoModerationRule> ModifyRuleAsync(BaseDiscordClient client, IAutoModRule rule, Action<AutoModRuleProperties> 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<ulong>.Unspecified, | |||
| DurationSeconds = x.TimeoutDuration.HasValue ? (int)Math.Floor(x.TimeoutDuration.Value.TotalSeconds) : Optional<int>.Unspecified | |||
| } : Optional<API.ActionMetadata>.Unspecified | |||
| }).ToArray() : Optional<API.AutoModAction[]>.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<string>()), | |||
| RegexPatterns = args.RegexPatterns.GetValueOrDefault(Array.Empty<string>()), | |||
| AllowList = args.AllowList.GetValueOrDefault(Array.Empty<string>()), | |||
| MentionLimit = args.MentionLimit, | |||
| Presets = args.Presets.GetValueOrDefault(Array.Empty<KeywordPresetTypes>()) | |||
| } : Optional<API.TriggerMetadata>.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 | |||
| } | |||
| } | |||
| @@ -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<ulong>, IAutoModRule | |||
| { | |||
| /// <inheritdoc /> | |||
| public DateTimeOffset CreatedAt { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong GuildId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| /// <inheritdoc /> | |||
| public ulong CreatorId { get; private set; } | |||
| /// <inheritdoc /> | |||
| public AutoModEventType EventType { get; private set; } | |||
| /// <inheritdoc /> | |||
| public AutoModTriggerType TriggerType { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> KeywordFilter { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> RegexPatterns { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<string> AllowList { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<KeywordPresetTypes> Presets { get; private set; } | |||
| /// <inheritdoc /> | |||
| public int? MentionTotalLimit { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<AutoModRuleAction> Actions { get; private set; } | |||
| /// <inheritdoc /> | |||
| public bool Enabled { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<ulong> ExemptRoles { get; private set; } | |||
| /// <inheritdoc /> | |||
| public IReadOnlyCollection<ulong> 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<string>()).ToImmutableArray(); | |||
| Presets = model.TriggerMetadata.Presets.GetValueOrDefault(Array.Empty<KeywordPresetTypes>()).ToImmutableArray(); | |||
| RegexPatterns = model.TriggerMetadata.RegexPatterns.GetValueOrDefault(Array.Empty<string>()).ToImmutableArray(); | |||
| AllowList = model.TriggerMetadata.AllowList.GetValueOrDefault(Array.Empty<string>()).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(); | |||
| } | |||
| /// <inheritdoc /> | |||
| public async Task ModifyAsync(Action<AutoModRuleProperties> func, RequestOptions options = null) | |||
| { | |||
| var model = await GuildHelper.ModifyRuleAsync(Discord, this, func, options); | |||
| Update(model); | |||
| } | |||
| /// <inheritdoc /> | |||
| public Task DeleteAsync(RequestOptions options = null) | |||
| => GuildHelper.DeleteRuleAsync(Discord, this, options); | |||
| } | |||
| @@ -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 | |||
| /// <inheritdoc cref="IGuild.GetAutoModRuleAsync"/> | |||
| public async Task<RestAutoModRule> GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null) | |||
| { | |||
| var rule = await GuildHelper.GetAutoModRuleAsync(ruleId, this, Discord, options); | |||
| return RestAutoModRule.Create(Discord, rule); | |||
| } | |||
| /// <inheritdoc cref="IGuild.GetAutoModRulesAsync"/> | |||
| public async Task<RestAutoModRule[]> GetAutoModRulesAsync(RequestOptions options = null) | |||
| { | |||
| var rules = await GuildHelper.GetAutoModRulesAsync(this, Discord, options); | |||
| return rules.Select(x => RestAutoModRule.Create(Discord, x)).ToArray(); | |||
| } | |||
| /// <inheritdoc cref="IGuild.CreateAutoModRuleAsync"/> | |||
| public async Task<RestAutoModRule> CreateAutoModRuleAsync(Action<AutoModRuleProperties> 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<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null) | |||
| => GuildHelper.ModifyWelcomeScreenAsync(enabled, description, channels, this, Discord, options); | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule> IGuild.GetAutoModRuleAsync(ulong ruleId, RequestOptions options) | |||
| => await GetAutoModRuleAsync(ruleId, options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule[]> IGuild.GetAutoModRulesAsync(RequestOptions options) | |||
| => await GetAutoModRulesAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule> IGuild.CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options) | |||
| => await CreateAutoModRuleAsync(props, options).ConfigureAwait(false); | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -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<ulong> ChannelId { get; set; } | |||
| [JsonProperty("message_id")] | |||
| public Optional<ulong> MessageId { get; set; } | |||
| [JsonProperty("alert_system_message_id")] | |||
| public Optional<ulong> AlertSystemMessageId { get; set; } | |||
| [JsonProperty("content")] | |||
| public string Content { get; set; } | |||
| [JsonProperty("matched_keyword")] | |||
| public Optional<string> MatchedKeyword { get; set; } | |||
| [JsonProperty("matched_content")] | |||
| public Optional<string> MatchedContent { get; set; } | |||
| } | |||
| @@ -892,5 +892,49 @@ namespace Discord.WebSocket | |||
| internal readonly AsyncEvent<Func<SocketGuild, SocketChannel, Task>> _webhooksUpdated = new AsyncEvent<Func<SocketGuild, SocketChannel, Task>>(); | |||
| #endregion | |||
| #region AutoModeration | |||
| /// <summary> | |||
| /// Fired when an auto moderation rule is created. | |||
| /// </summary> | |||
| public event Func<SocketAutoModRule, Task> AutoModRuleCreated | |||
| { | |||
| add => _autoModRuleCreated.Add(value); | |||
| remove => _autoModRuleCreated.Remove(value); | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketAutoModRule, Task>> _autoModRuleCreated = new (); | |||
| /// <summary> | |||
| /// Fired when an auto moderation rule is modified. | |||
| /// </summary> | |||
| public event Func<Cacheable<SocketAutoModRule, ulong>, SocketAutoModRule, Task> AutoModRuleUpdated | |||
| { | |||
| add => _autoModRuleUpdated.Add(value); | |||
| remove => _autoModRuleUpdated.Remove(value); | |||
| } | |||
| internal readonly AsyncEvent<Func<Cacheable<SocketAutoModRule, ulong>, SocketAutoModRule, Task>> _autoModRuleUpdated = new (); | |||
| /// <summary> | |||
| /// Fired when an auto moderation rule is deleted. | |||
| /// </summary> | |||
| public event Func<SocketAutoModRule, Task> AutoModRuleDeleted | |||
| { | |||
| add => _autoModRuleDeleted.Add(value); | |||
| remove => _autoModRuleDeleted.Remove(value); | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketAutoModRule, Task>> _autoModRuleDeleted = new (); | |||
| /// <summary> | |||
| /// Fired when an auto moderation rule is triggered by a user. | |||
| /// </summary> | |||
| public event Func<SocketGuild, AutoModRuleAction, AutoModActionExecutedData, Task> AutoModActionExecuted | |||
| { | |||
| add => _autoModActionExecuted.Add(value); | |||
| remove => _autoModActionExecuted.Remove(value); | |||
| } | |||
| internal readonly AsyncEvent<Func<SocketGuild, AutoModRuleAction, AutoModActionExecutedData, Task>> _autoModActionExecuted = new (); | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -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<AutoModerationRule>(_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<AutoModerationRule>(_serializer); | |||
| var guild = State.GetGuild(data.GuildId); | |||
| var cachedRule = guild.GetAutoModRule(data.Id); | |||
| var cacheableBefore = new Cacheable<SocketAutoModRule, ulong>(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<AutoModerationRule>(_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<AutoModActionExecutedEvent>(_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<SocketGuildUser, ulong>(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<ISocketMessageChannel, ulong>(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<IUserMessage, ulong>(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<IAutoModRule, ulong>(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); | |||
| @@ -0,0 +1,86 @@ | |||
| using Discord.Rest; | |||
| namespace Discord.WebSocket; | |||
| public class AutoModActionExecutedData | |||
| { | |||
| /// <summary> | |||
| /// Gets the id of the rule which action belongs to. | |||
| /// </summary> | |||
| public Cacheable<IAutoModRule, ulong> Rule { get; } | |||
| /// <summary> | |||
| /// Gets the trigger type of rule which was triggered. | |||
| /// </summary> | |||
| public AutoModTriggerType TriggerType { get; } | |||
| /// <summary> | |||
| /// Gets the user which generated the content which triggered the rule. | |||
| /// </summary> | |||
| public Cacheable<SocketGuildUser, ulong> User { get; } | |||
| /// <summary> | |||
| /// Gets the channel in which user content was posted. | |||
| /// </summary> | |||
| public Cacheable<ISocketMessageChannel, ulong> Channel { get; } | |||
| /// <summary> | |||
| /// Gets the message that triggered the action. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property will be <see langword="null"/> if the message was blocked by the automod. | |||
| /// </remarks> | |||
| public Cacheable<IUserMessage, ulong>? Message { get; } | |||
| /// <summary> | |||
| /// Gets the id of the system auto moderation messages posted as a result of this action. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property will be <see langword="null"/> if this event does not correspond to an action | |||
| /// with type <see cref="AutoModActionType.SendAlertMessage"/>. | |||
| /// </remarks> | |||
| public ulong AlertMessageId { get; } | |||
| /// <summary> | |||
| /// Gets the user-generated text content. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property will be empty if <see cref="GatewayIntents.MessageContent"/> is disabled. | |||
| /// </remarks> | |||
| public string Content { get; } | |||
| /// <summary> | |||
| /// Gets the substring in content that triggered the rule. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property will be empty if <see cref="GatewayIntents.MessageContent"/> is disabled. | |||
| /// </remarks> | |||
| public string MatchedContent { get; } | |||
| /// <summary> | |||
| /// Gets the word or phrase configured in the rule that triggered the rule. | |||
| /// </summary> | |||
| public string MatchedKeyword { get; } | |||
| internal AutoModActionExecutedData(Cacheable<IAutoModRule, ulong> rule, | |||
| AutoModTriggerType triggerType, | |||
| Cacheable<SocketGuildUser, ulong> user, | |||
| Cacheable<ISocketMessageChannel, ulong> channel, | |||
| Cacheable<IUserMessage, ulong>? 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; | |||
| } | |||
| } | |||
| @@ -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<ulong>, IAutoModRule | |||
| { | |||
| /// <summary> | |||
| /// Gets the guild that this rule is in. | |||
| /// </summary> | |||
| public SocketGuild Guild { get; } | |||
| /// <inheritdoc/> | |||
| public string Name { get; private set; } | |||
| /// <summary> | |||
| /// Gets the creator of this rule. | |||
| /// </summary> | |||
| public SocketGuildUser Creator { get; private set; } | |||
| /// <inheritdoc/> | |||
| public AutoModEventType EventType { get; private set; } | |||
| /// <inheritdoc/> | |||
| public AutoModTriggerType TriggerType { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<string> KeywordFilter { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<string> RegexPatterns { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<string> AllowList { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<KeywordPresetTypes> Presets { get; private set; } | |||
| /// <inheritdoc/> | |||
| public IReadOnlyCollection<AutoModRuleAction> Actions { get; private set; } | |||
| /// <inheritdoc/> | |||
| public int? MentionTotalLimit { get; private set; } | |||
| /// <inheritdoc/> | |||
| public bool Enabled { get; private set; } | |||
| /// <summary> | |||
| /// Gets the roles that are exempt from this rule. | |||
| /// </summary> | |||
| public IReadOnlyCollection<SocketRole> ExemptRoles { get; private set; } | |||
| /// <summary> | |||
| /// Gets the channels that are exempt from this rule. | |||
| /// </summary> | |||
| public IReadOnlyCollection<SocketGuildChannel> ExemptChannels { get; private set; } | |||
| /// <inheritdoc/> | |||
| 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<string>()).ToImmutableArray(); | |||
| Presets = model.TriggerMetadata.Presets.GetValueOrDefault(Array.Empty<KeywordPresetTypes>()).ToImmutableArray(); | |||
| RegexPatterns = model.TriggerMetadata.RegexPatterns.GetValueOrDefault(Array.Empty<string>()).ToImmutableArray(); | |||
| AllowList = model.TriggerMetadata.AllowList.GetValueOrDefault(Array.Empty<string>()).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(); | |||
| } | |||
| /// <inheritdoc/> | |||
| public async Task ModifyAsync(Action<AutoModRuleProperties> func, RequestOptions options = null) | |||
| { | |||
| var model = await GuildHelper.ModifyRuleAsync(Discord, this, func, options); | |||
| Guild.AddOrUpdateAutoModRule(model); | |||
| } | |||
| /// <inheritdoc/> | |||
| public Task DeleteAsync(RequestOptions options = null) | |||
| => GuildHelper.DeleteRuleAsync(Discord, this, options); | |||
| internal SocketAutoModRule Clone() => MemberwiseClone() as SocketAutoModRule; | |||
| #region IAutoModRule | |||
| IReadOnlyCollection<ulong> IAutoModRule.ExemptRoles => ExemptRoles.Select(x => x.Id).ToImmutableArray(); | |||
| IReadOnlyCollection<ulong> IAutoModRule.ExemptChannels => ExemptChannels.Select(x => x.Id).ToImmutableArray(); | |||
| ulong IAutoModRule.GuildId => Guild.Id; | |||
| ulong IAutoModRule.CreatorId => _creatorId; | |||
| #endregion | |||
| } | |||
| } | |||
| @@ -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<ulong, SocketVoiceState> _voiceStates; | |||
| private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; | |||
| private ConcurrentDictionary<ulong, SocketGuildEvent> _events; | |||
| private ConcurrentDictionary<ulong, SocketAutoModRule> _automodRules; | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private AudioClient _audioClient; | |||
| @@ -391,6 +393,7 @@ namespace Discord.WebSocket | |||
| { | |||
| _audioLock = new SemaphoreSlim(1, 1); | |||
| _emotes = ImmutableArray.Create<GuildEmote>(); | |||
| _automodRules = new ConcurrentDictionary<ulong, SocketAutoModRule>(); | |||
| } | |||
| 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; | |||
| } | |||
| /// <summary> | |||
| /// Gets a single rule configured in a guild from cache. Returns <see langword="null"/> if the rule was not found. | |||
| /// </summary> | |||
| 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); | |||
| } | |||
| /// <inheritdoc cref="IGuild.GetAutoModRuleAsync"/> | |||
| public async Task<SocketAutoModRule> GetAutoModRuleAsync(ulong ruleId, RequestOptions options = null) | |||
| { | |||
| var rule = await GuildHelper.GetAutoModRuleAsync(ruleId, this, Discord, options); | |||
| return AddOrUpdateAutoModRule(rule); | |||
| } | |||
| /// <inheritdoc cref="IGuild.GetAutoModRulesAsync"/> | |||
| public async Task<SocketAutoModRule[]> GetAutoModRulesAsync(RequestOptions options = null) | |||
| { | |||
| var rules = await GuildHelper.GetAutoModRulesAsync(this, Discord, options); | |||
| return rules.Select(AddOrUpdateAutoModRule).ToArray(); | |||
| } | |||
| /// <inheritdoc cref="IGuild.CreateAutoModRuleAsync"/> | |||
| public async Task<SocketAutoModRule> CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options = null) | |||
| { | |||
| var rule = await GuildHelper.CreateAutoModRuleAsync(this, props, Discord, options); | |||
| return AddOrUpdateAutoModRule(rule); | |||
| } | |||
| /// <summary> | |||
| /// Gets the auto moderation rules defined in this guild. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// This property may not always return all auto moderation rules if they haven't been cached. | |||
| /// </remarks> | |||
| public IReadOnlyCollection<SocketAutoModRule> AutoModRules => _automodRules.ToReadOnlyCollection(); | |||
| #endregion | |||
| #region IGuild | |||
| /// <inheritdoc /> | |||
| ulong? IGuild.AFKChannelId => AFKChannelId; | |||
| @@ -2053,6 +2128,19 @@ namespace Discord.WebSocket | |||
| _audioLock?.Dispose(); | |||
| _audioClient?.Dispose(); | |||
| } | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule> IGuild.GetAutoModRuleAsync(ulong ruleId, RequestOptions options) | |||
| => await GetAutoModRuleAsync(ruleId, options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule[]> IGuild.GetAutoModRulesAsync(RequestOptions options) | |||
| => await GetAutoModRulesAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc/> | |||
| async Task<IAutoModRule> IGuild.CreateAutoModRuleAsync(Action<AutoModRuleProperties> props, RequestOptions options) | |||
| => await CreateAutoModRuleAsync(props, options).ConfigureAwait(false); | |||
| #endregion | |||
| } | |||
| } | |||