Browse Source

[Feature] AutoMod support (#2578)

* 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
Misha133 GitHub 2 years ago
parent
commit
673b02dd36
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1478 additions and 1 deletions
  1. +26
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModActionType.cs
  2. +19
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModEventType.cs
  3. +36
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleAction.cs
  4. +0
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleBuilder.cs
  5. +151
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleProperties.cs
  6. +39
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModTriggerType.cs
  7. +111
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/IAutoModRule.cs
  8. +29
    -0
      src/Discord.Net.Core/Entities/Guilds/AutoModeration/KeywordPresetTypes.cs
  9. +24
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  10. +13
    -1
      src/Discord.Net.Core/GatewayIntents.cs
  11. +18
    -0
      src/Discord.Net.Rest/API/Common/ActionMetadata.cs
  12. +18
    -0
      src/Discord.Net.Rest/API/Common/AutoModAction.cs
  13. +45
    -0
      src/Discord.Net.Rest/API/Common/AutoModerationRule.cs
  14. +27
    -0
      src/Discord.Net.Rest/API/Common/TriggerMetadata.cs
  15. +36
    -0
      src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs
  16. +20
    -0
      src/Discord.Net.Rest/API/Rest/ModifyAutoModRuleParams.cs
  17. +52
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  18. +165
    -0
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  19. +101
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs
  20. +41
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  21. +38
    -0
      src/Discord.Net.WebSocket/API/Gateway/AutoModActionExecutedEvent.cs
  22. +44
    -0
      src/Discord.Net.WebSocket/BaseSocketClient.Events.cs
  23. +129
    -0
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  24. +86
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/AutoModActionExecutedData.cs
  25. +122
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs
  26. +88
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs

+ 26
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModActionType.cs View File

@@ -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,
}
}

+ 19
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModEventType.cs View File

@@ -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
}
}

+ 36
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleAction.cs View File

@@ -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
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleBuilder.cs View File


+ 151
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModRuleProperties.cs View File

@@ -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; }
}

}

+ 39
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/AutoModTriggerType.cs View File

@@ -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,
}
}

+ 111
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/IAutoModRule.cs View File

@@ -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);
}
}

+ 29
- 0
src/Discord.Net.Core/Entities/Guilds/AutoModeration/KeywordPresetTypes.cs View File

@@ -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,
}
}

+ 24
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -1267,5 +1267,29 @@ namespace Discord
/// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>. /// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>.
/// </returns> /// </returns>
Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null); 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);
} }
} }

+ 13
- 1
src/Discord.Net.Core/GatewayIntents.cs View File

@@ -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 /// 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> /// </summary>
GuildScheduledEvents = 1 << 16, 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> /// <summary>
/// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/> /// This intent includes all but <see cref="GuildMembers"/> and <see cref="GuildPresences"/>
/// which are privileged and must be enabled in the Developer Portal. /// which are privileged and must be enabled in the Developer Portal.
/// </summary> /// </summary>
AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites |
GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages |
DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents,
DirectMessageReactions | DirectMessageTyping | GuildScheduledEvents | AutoModerationConfiguration |
AutoModerationActionExecution,
/// <summary> /// <summary>
/// This intent includes all of them, including privileged ones. /// This intent includes all of them, including privileged ones.
/// </summary> /// </summary>


+ 18
- 0
src/Discord.Net.Rest/API/Common/ActionMetadata.cs View File

@@ -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; }
}
}

+ 18
- 0
src/Discord.Net.Rest/API/Common/AutoModAction.cs View File

@@ -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; }
}
}

+ 45
- 0
src/Discord.Net.Rest/API/Common/AutoModerationRule.cs View File

@@ -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; }
}
}

+ 27
- 0
src/Discord.Net.Rest/API/Common/TriggerMetadata.cs View File

@@ -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; }
}
}

+ 36
- 0
src/Discord.Net.Rest/API/Rest/CreateAutoModRuleParams.cs View File

@@ -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; }
}
}

+ 20
- 0
src/Discord.Net.Rest/API/Rest/ModifyAutoModRuleParams.cs View File

@@ -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; }
}
}

+ 52
- 0
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -2119,6 +2119,58 @@ namespace Discord.API


#endregion #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 #region Guild Welcome Screen


public async Task<WelcomeScreen> GetGuildWelcomeScreenAsync(ulong guildId, RequestOptions options = null) public async Task<WelcomeScreen> GetGuildWelcomeScreenAsync(ulong guildId, RequestOptions options = null)


+ 165
- 0
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -1062,5 +1062,170 @@ namespace Discord.Rest
} }


#endregion #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
} }
} }

+ 101
- 0
src/Discord.Net.Rest/Entities/Guilds/RestAutoModRule.cs View File

@@ -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);
}

+ 41
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -1197,6 +1197,34 @@ namespace Discord.Rest
RequestOptions options = null) RequestOptions options = null)
=> GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); => 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 #endregion


#region IGuild #region IGuild
@@ -1543,6 +1571,19 @@ namespace Discord.Rest
public Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null) public Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null)
=> GuildHelper.ModifyWelcomeScreenAsync(enabled, description, channels, this, Discord, options); => 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 #endregion
} }
} }

+ 38
- 0
src/Discord.Net.WebSocket/API/Gateway/AutoModActionExecutedEvent.cs View File

@@ -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; }
}

+ 44
- 0
src/Discord.Net.WebSocket/BaseSocketClient.Events.cs View File

@@ -892,5 +892,49 @@ namespace Discord.WebSocket
internal readonly AsyncEvent<Func<SocketGuild, SocketChannel, Task>> _webhooksUpdated = new AsyncEvent<Func<SocketGuild, SocketChannel, Task>>(); internal readonly AsyncEvent<Func<SocketGuild, SocketChannel, Task>> _webhooksUpdated = new AsyncEvent<Func<SocketGuild, SocketChannel, Task>>();


#endregion #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
} }
} }

+ 129
- 0
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -6,8 +6,10 @@ using Discord.Net.Udp;
using Discord.Net.WebSockets; using Discord.Net.WebSockets;
using Discord.Rest; using Discord.Rest;
using Discord.Utils; using Discord.Utils;

using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;

using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -16,6 +18,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

using GameModel = Discord.API.Game; using GameModel = Discord.API.Game;


namespace Discord.WebSocket namespace Discord.WebSocket
@@ -2882,6 +2885,132 @@ namespace Discord.WebSocket


#endregion #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) #region Ignored (User only)
case "CHANNEL_PINS_ACK": case "CHANNEL_PINS_ACK":
await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false);


+ 86
- 0
src/Discord.Net.WebSocket/Entities/Guilds/AutoModActionExecutedData.cs View File

@@ -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;
}
}

+ 122
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketAutoModRule.cs View File

@@ -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
}
}

+ 88
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -11,6 +11,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoModRuleModel = Discord.API.AutoModerationRule;
using ChannelModel = Discord.API.Channel; using ChannelModel = Discord.API.Channel;
using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent;
using EventModel = Discord.API.GuildScheduledEvent; using EventModel = Discord.API.GuildScheduledEvent;
@@ -43,6 +44,7 @@ namespace Discord.WebSocket
private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates; private ConcurrentDictionary<ulong, SocketVoiceState> _voiceStates;
private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers; private ConcurrentDictionary<ulong, SocketCustomSticker> _stickers;
private ConcurrentDictionary<ulong, SocketGuildEvent> _events; private ConcurrentDictionary<ulong, SocketGuildEvent> _events;
private ConcurrentDictionary<ulong, SocketAutoModRule> _automodRules;
private ImmutableArray<GuildEmote> _emotes; private ImmutableArray<GuildEmote> _emotes;


private AudioClient _audioClient; private AudioClient _audioClient;
@@ -391,6 +393,7 @@ namespace Discord.WebSocket
{ {
_audioLock = new SemaphoreSlim(1, 1); _audioLock = new SemaphoreSlim(1, 1);
_emotes = ImmutableArray.Create<GuildEmote>(); _emotes = ImmutableArray.Create<GuildEmote>();
_automodRules = new ConcurrentDictionary<ulong, SocketAutoModRule>();
} }
internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model)
{ {
@@ -1809,6 +1812,78 @@ namespace Discord.WebSocket
internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild;
#endregion #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 #region IGuild
/// <inheritdoc /> /// <inheritdoc />
ulong? IGuild.AFKChannelId => AFKChannelId; ulong? IGuild.AFKChannelId => AFKChannelId;
@@ -2053,6 +2128,19 @@ namespace Discord.WebSocket
_audioLock?.Dispose(); _audioLock?.Dispose();
_audioClient?.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 #endregion
} }
} }

Loading…
Cancel
Save