* 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); | |||
} |
@@ -1199,6 +1199,34 @@ namespace Discord.Rest | |||
#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 | |||
/// <inheritdoc /> | |||
bool IGuild.Available => Available; | |||
@@ -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 | |||
} | |||
} |