Repository: Kraigie/nostrum
Pull request #426 (2022-06-20 01:08:46)

Resolve deprecation of `$` prefixing properties in identify payload, and add support for auto-mod
Just like it says on the tin. Some further testing would be appreciated as I'm unable to test this feature fully at present, hasn't rolled out to my test guild yet.


Patch:
diff --git a/lib/nostrum/api.ex b/lib/nostrum/api.ex
index 58a2e62c..932115c0 100644
--- a/lib/nostrum/api.ex
+++ b/lib/nostrum/api.ex
@@ -50,6 +50,7 @@ defmodule Nostrum.Api do
 
   alias Nostrum.Struct.{
     ApplicationCommand,
+    AutoModerationRule,
     Channel,
     Embed,
     Emoji,
@@ -3983,6 +3984,80 @@ defmodule Nostrum.Api do
     request(:delete, Constants.thread_member(thread_id, user_id))
   end
 
+  @doc """
+  Get a list of all auto-moderation rules for a guild.
+  """
+  @doc since: "0.6.1"
+  @spec get_guild_auto_moderation_rules(Guild.id()) :: {:ok, [AutoModerationRule.t()]} | error
+  def get_guild_auto_moderation_rules(guild_id) do
+    request(:get, Constants.guild_auto_moderation_rule(guild_id))
+    |> handle_request_with_decode({:list, {:struct, AutoModerationRule}})
+  end
+
+  @doc """
+  Get a single auto-moderation rule for a guild.
+  """
+  @doc since: "0.6.1"
+  @spec get_guild_auto_moderation_rule(Guild.id(), AutoModerationRule.id()) ::
+          {:ok, AutoModerationRule.t()} | error
+  def get_guild_auto_moderation_rule(guild_id, rule_id) do
+    request(:get, Constants.guild_auto_moderation_rule(guild_id, rule_id))
+    |> handle_request_with_decode({:struct, AutoModerationRule})
+  end
+
+  @doc """
+  Create a new auto-moderation rule for a guild.
+
+  ## Options
+    * `:name` (`t:String.t/0`) - The name of the rule.
+    * `:event_type` (`t:AutoModerationRule.event_type/0`) - The type of event that triggers the rule.
+    * `:trigger_type` (`t:AutoModerationRule.trigger_type/0`) - The type of content that triggers the rule.
+    * `:trigger_metadata` (`t:AutoModerationRule.trigger_metadata/0`) - The metadata associated with the rule trigger.
+      - optional, based on the `:trigger_type`.
+    * `:actions` (`t:AutoModerationRule.actions/0`) - The actions to take when the rule is triggered.
+    * `:enabled` (`t:AutoModerationRule.enabled/0`) - Whether the rule is enabled or not.
+      - optional, defaults to `false`.
+    * `:exempt_roles` - (`t:AutoModerationRule.exempt_roles/0`) - A list of role id's that are exempt from the rule.
+      - optional, defaults to `[]`, maximum of 20.
+    * `:exempt_channels` - (`t:AutoModerationRule.exempt_channels/0`) - A list of channel id's that are exempt from the rule.
+      - optional, defaults to `[]`, maximum of 50.
+  """
+  @doc since: "0.6.1"
+  @spec create_guild_auto_moderation_rule(Guild.id(), options()) ::
+          {:ok, AutoModerationRule.t()} | error
+  def create_guild_auto_moderation_rule(guild_id, options) when is_list(options),
+    do: create_guild_auto_moderation_rule(guild_id, Map.new(options))
+
+  def create_guild_auto_moderation_rule(guild_id, options) do
+    request(:post, Constants.guild_auto_moderation_rule(guild_id), options)
+    |> handle_request_with_decode({:struct, AutoModerationRule})
+  end
+
+  @doc """
+  Modify an auto-moderation rule for a guild.
+
+  Takes the same options as `create_guild_auto_moderation_rule/2`, however all fields are optional.
+  """
+  @doc since: "0.6.1"
+  @spec modify_guild_auto_moderation_rule(Guild.id(), AutoModerationRule.id(), options()) ::
+          {:ok, AutoModerationRule.t()} | error
+  def modify_guild_auto_moderation_rule(guild_id, rule_id, options) when is_list(options),
+    do: modify_guild_auto_moderation_rule(guild_id, rule_id, Map.new(options))
+
+  def modify_guild_auto_moderation_rule(guild_id, rule_id, options) do
+    request(:patch, Constants.guild_auto_moderation_rule(guild_id, rule_id), options)
+    |> handle_request_with_decode({:struct, AutoModerationRule})
+  end
+
+  @doc """
+  Delete an auto-moderation rule for a guild.
+  """
+  @doc since: "0.6.1"
+  @spec delete_guild_auto_moderation_rule(Guild.id(), AutoModerationRule.id()) :: {:ok} | error
+  def delete_guild_auto_moderation_rule(guild_id, rule_id) do
+    request(:delete, Constants.guild_auto_moderation_rule(guild_id, rule_id))
+  end
+
   @spec maybe_add_reason(String.t() | nil) :: list()
   defp maybe_add_reason(reason) do
     maybe_add_reason(reason, [{"content-type", "application/json"}])
diff --git a/lib/nostrum/constants.ex b/lib/nostrum/constants.ex
index 1f073384..98c37d18 100644
--- a/lib/nostrum/constants.ex
+++ b/lib/nostrum/constants.ex
@@ -162,6 +162,12 @@ defmodule Nostrum.Constants do
   def private_joined_archived_threads(channel_id),
     do: "/channels/#{channel_id}/users/@me/threads/archived/private"
 
+  def guild_auto_moderation_rule(guild_id),
+    do: "/guilds/#{guild_id}/auto-moderation/rules"
+
+  def guild_auto_moderation_rule(guild_id, rule_id),
+    do: "/guilds/#{guild_id}/auto-moderation/rules/#{rule_id}"
+
   def discord_epoch, do: 1_420_070_400_000
 
   def opcodes do
diff --git a/lib/nostrum/consumer.ex b/lib/nostrum/consumer.ex
index 36990cde..3f92b7d9 100644
--- a/lib/nostrum/consumer.ex
+++ b/lib/nostrum/consumer.ex
@@ -21,10 +21,20 @@ defmodule Nostrum.Consumer do
   use ConsumerSupervisor
 
   alias Nostrum.Shard.Stage.Cache
-  alias Nostrum.Struct.{Channel, Interaction, ThreadMember, VoiceWSState, WSState}
+
+  alias Nostrum.Struct.{
+    AutoModerationRule,
+    Channel,
+    Interaction,
+    ThreadMember,
+    VoiceWSState,
+    WSState
+  }
+
   alias Nostrum.Struct.Guild.Integration
 
   alias Nostrum.Struct.Event.{
+    AutoModerationRuleExecute,
     ChannelPinsUpdate,
     GuildBanAdd,
     GuildBanRemove,
@@ -88,6 +98,16 @@ defmodule Nostrum.Consumer do
           | {:max_seconds, non_neg_integer()}
           | {:subscribe_to, [GenStage.stage() | {GenStage.stage(), keyword()}]}
 
+  @type auto_moderation_rule_create ::
+          {:AUTO_MODERATION_RULE_CREATE, AutoModerationRule.t(), WSState.t()}
+  @type auto_moderation_rule_delete ::
+          {:AUTO_MODERATION_RULE_DELETE, AutoModerationRule.t(), WSState.t()}
+  @type auto_moderation_rule_update ::
+          {:AUTO_MODERATION_RULE_UPDATE, AutoModerationRule.t(), WSState.t()}
+
+  @type auto_moderation_rule_execute ::
+          {:AUTO_MODERATION_RULE_EXECUTE, AutoModerationRuleExecute.t(), WSState.t()}
+
   @typedoc """
   Dispatched when a channel is created.
 
@@ -274,7 +294,11 @@ defmodule Nostrum.Consumer do
   """
   @type thread_members_update :: {:THREAD_MEMBERS_UPDATE, ThreadMembersUpdate.t(), WSState.t()}
   @type event ::
-          channel_create
+          auto_moderation_rule_create
+          | auto_moderation_rule_delete
+          | auto_moderation_rule_update
+          | auto_moderation_rule_execute
+          | channel_create
           | channel_delete
           | channel_update
           | channel_pins_ack
diff --git a/lib/nostrum/shard/dispatch.ex b/lib/nostrum/shard/dispatch.ex
index b73298a7..fde46602 100644
--- a/lib/nostrum/shard/dispatch.ex
+++ b/lib/nostrum/shard/dispatch.ex
@@ -6,6 +6,7 @@ defmodule Nostrum.Shard.Dispatch do
   alias Nostrum.Shard.{Intents, Session}
 
   alias Nostrum.Struct.Event.{
+    AutoModerationRuleExecute,
     ChannelPinsUpdate,
     GuildBanAdd,
     GuildBanRemove,
@@ -31,7 +32,7 @@ defmodule Nostrum.Shard.Dispatch do
     VoiceState
   }
 
-  alias Nostrum.Struct.{Guild, Interaction, Message, ThreadMember, User}
+  alias Nostrum.Struct.{AutoModerationRule, Guild, Interaction, Message, ThreadMember, User}
   alias Nostrum.Struct.Guild.{Integration, ScheduledEvent, UnavailableGuild}
   alias Nostrum.Util
   alias Nostrum.Voice
@@ -65,6 +66,18 @@ defmodule Nostrum.Shard.Dispatch do
     end
   end
 
+  def handle_event(:AUTO_MODERATION_RULE_CREATE = event, p, state),
+    do: {event, AutoModerationRule.to_struct(p), state}
+
+  def handle_event(:AUTO_MODERATION_RULE_DELETE = event, p, state),
+    do: {event, AutoModerationRule.to_struct(p), state}
+
+  def handle_event(:AUTO_MODERATION_RULE_UPDATE = event, p, state),
+    do: {event, AutoModerationRule.to_struct(p), state}
+
+  def handle_event(:AUTO_MODERATION_RULE_EXECUTION = event, p, state),
+    do: {event, AutoModerationRuleExecute.to_struct(p), state}
+
   def handle_event(:CHANNEL_CREATE = event, %{type: 1} = p, state) do
     {event, ChannelCache.create(p), state}
   end
diff --git a/lib/nostrum/shard/intents.ex b/lib/nostrum/shard/intents.ex
index f7221ec3..363b8fbc 100644
--- a/lib/nostrum/shard/intents.ex
+++ b/lib/nostrum/shard/intents.ex
@@ -28,7 +28,9 @@ defmodule Nostrum.Shard.Intents do
       direct_message_reactions: 1 <<< 13,
       direct_message_typing: 1 <<< 14,
       message_content: 1 <<< 15,
-      guild_scheduled_events: 1 <<< 16
+      guild_scheduled_events: 1 <<< 16,
+      auto_moderation_configuration: 1 <<< 20,
+      auto_moderation_execution: 1 <<< 21
     ]
   end
 
diff --git a/lib/nostrum/shard/payload.ex b/lib/nostrum/shard/payload.ex
index ea73e19a..f3339662 100644
--- a/lib/nostrum/shard/payload.ex
+++ b/lib/nostrum/shard/payload.ex
@@ -18,11 +18,9 @@ defmodule Nostrum.Shard.Payload do
     %{
       "token" => Application.get_env(:nostrum, :token),
       "properties" => %{
-        "$os" => Atom.to_string(os) <> " " <> Atom.to_string(name),
-        "$browser" => "Nostrum",
-        "$device" => "Nostrum",
-        "$referrer" => "",
-        "$referring_domain" => ""
+        "os" => Atom.to_string(os) <> " " <> Atom.to_string(name),
+        "browser" => "Nostrum",
+        "device" => "Nostrum"
       },
       "compress" => false,
       "large_threshold" => @large_threshold,
diff --git a/lib/nostrum/struct/auto_moderation_rule.ex b/lib/nostrum/struct/auto_moderation_rule.ex
new file mode 100644
index 00000000..4b728a8e
--- /dev/null
+++ b/lib/nostrum/struct/auto_moderation_rule.ex
@@ -0,0 +1,135 @@
+defmodule Nostrum.Struct.AutoModerationRule do
+  @moduledoc """
+  Struct representing an auto-moderation rule.
+  """
+  @moduledoc since: "0.6.1"
+
+  alias Nostrum.Struct.AutoModerationRule.{Action, TriggerMetadata}
+  alias Nostrum.Struct.Channel
+  alias Nostrum.Struct.Guild
+  alias Nostrum.Struct.Guild.Role
+  alias Nostrum.Struct.User
+  alias Nostrum.{Snowflake, Util}
+
+  defstruct [
+    :id,
+    :guild_id,
+    :name,
+    :creator_id,
+    :event_type,
+    :trigger_type,
+    :trigger_metadata,
+    :actions,
+    :enabled,
+    :exempt_roles,
+    :exempt_channels
+  ]
+
+  @typedoc "The id of the auto-moderation rule"
+  @type id :: Snowflake.t()
+
+  @typedoc "The id of the guild the rule belongs to"
+  @type guild_id :: Guild.id()
+
+  @typedoc "The name of the rule"
+  @type name :: String.t()
+
+  @typedoc "The id of the user who created the rule"
+  @type creator_id :: User.id()
+
+  @typedoc """
+  Indicates in what event context a rule should be checked
+
+  | value | type | description
+  | ---- | ---- | -----------
+  |`1` | `MESSAGE_SEND` | when a member sends or edits a message in a guild
+  """
+  @type event_type :: 1
+
+  @typedoc """
+  Characters the type of content which triggered the rule
+
+  | value | type | max per guild | description
+  | ---- | ---- | ----- | -----------
+  |`1` | `KEYWORD` | 3 | check if content contains words from a user defined list of keywords
+  | `2` | `HARMFUL_LINK` | 1 | check if the content contains any harmful links
+  | `3` | `SPAM` | 1 | check if the content represents generic spam
+  | `4` | `KEYWORD_PRESET `| 1 | check if the content contains a list of discord defined keywords
+
+  note: `HARMFUL_LINK` and `SPAM` are not yet offically released at the time of this writing.
+  """
+  @type trigger_type :: 1..4
+
+  @typedoc """
+  Values which represent the different presets defined by Discord
+
+  | value | type | description
+  | ---- | ---- | -----------
+  |`1` | `PROFANITY` | Words which may be considered profane
+  | `2` | `HARMFUL_LINK` | Words that refer to sexually explicit behavior or activity
+  | `3` | `SLURS` | Personal insults or words that may be considered hate speech
+  """
+  @type preset_values :: 1..3
+
+  @typedoc """
+  Additional data used to determine if the rule should triggered.
+
+  The `t:trigger_type/0` field will determine which of the following fields are present.
+
+  | key | associated `trigger_type`
+  | ---- | -----------
+  | `keywords` | `KEYWORD`
+  | `preset` | `KEYWORD_PRESET`
+  """
+  @type trigger_metadata :: TriggerMetadata.t()
+
+  @typedoc """
+  A list of Actions which will be performed when the rule is triggered.
+  """
+  @type actions :: [Action.t()]
+
+  @typedoc """
+  If the rule is enabled or not.
+  """
+  @type enabled :: boolean()
+
+  @typedoc """
+  A list of roles that are exempt from the rule.
+  """
+  @type exempt_roles :: [Role.id()]
+
+  @typedoc """
+  A list of channels that are exempt from the rule
+  """
+  @type exempt_channels :: [Channel.id()]
+
+  @type t :: %__MODULE__{
+          id: id(),
+          guild_id: guild_id(),
+          name: name(),
+          creator_id: creator_id(),
+          event_type: event_type(),
+          trigger_type: trigger_type(),
+          trigger_metadata: trigger_metadata(),
+          actions: actions(),
+          enabled: enabled(),
+          exempt_roles: exempt_roles(),
+          exempt_channels: exempt_channels()
+        }
+
+  @doc false
+  def to_struct(map) do
+    new =
+      map
+      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
+      |> Map.update(:id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:guild_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:creator_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:trigger_metadata, nil, &Util.cast(&1, {:struct, TriggerMetadata}))
+      |> Map.update(:actions, nil, &Util.cast(&1, {:list, {:struct, Action}}))
+      |> Map.update(:exempt_roles, nil, &Util.cast(&1, {:list, Snowflake}))
+      |> Map.update(:exempt_channels, nil, &Util.cast(&1, {:list, Snowflake}))
+
+    struct(__MODULE__, new)
+  end
+end
diff --git a/lib/nostrum/struct/auto_moderation_rule/action.ex b/lib/nostrum/struct/auto_moderation_rule/action.ex
new file mode 100644
index 00000000..2f7b28a2
--- /dev/null
+++ b/lib/nostrum/struct/auto_moderation_rule/action.ex
@@ -0,0 +1,42 @@
+defmodule Nostrum.Struct.AutoModerationRule.Action do
+  @moduledoc """
+  Defines an action to be taken when a rule is triggered.
+  """
+  @moduledoc since: "0.6.1"
+
+  alias Nostrum.Struct.AutoModerationRule.ActionMetadata
+  alias Nostrum.Util
+
+  defstruct [
+    :type,
+    :metadata
+  ]
+
+  @typedoc """
+  The type of action to be taken.
+
+  | value | action | description
+  | ---- | ---- | -----------
+  |`1` | `BLOCK_MESSAGE` | Blocks the message from being created
+  | `2` | `SEND_ALERT_MESSAGE` | Logs the content of the message in the specified channel
+  | `3` | `TIMEOUT` | timeout a user for a specified duration
+  """
+  @type action_type :: 1..3
+
+  @type metadata :: ActionMetadata.t()
+
+  @type t :: %__MODULE__{
+          type: action_type(),
+          metadata: metadata()
+        }
+
+  @doc false
+  def to_struct(map) do
+    new =
+      map
+      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
+      |> Map.update(:metadata, nil, &Util.cast(&1, {:struct, ActionMetadata}))
+
+    struct(__MODULE__, new)
+  end
+end
diff --git a/lib/nostrum/struct/auto_moderation_rule/action_metadata.ex b/lib/nostrum/struct/auto_moderation_rule/action_metadata.ex
new file mode 100644
index 00000000..7e20f591
--- /dev/null
+++ b/lib/nostrum/struct/auto_moderation_rule/action_metadata.ex
@@ -0,0 +1,48 @@
+defmodule Nostrum.Struct.AutoModerationRule.ActionMetadata do
+  @moduledoc """
+  Struct representing any additional data used when an action is taken.
+  """
+  @moduledoc since: "0.6.1"
+
+  alias Nostrum.{Snowflake, Util}
+
+  defstruct [
+    :channel_id,
+    :duration_seconds
+  ]
+
+  @typedoc """
+  The id of the channel to send an alert message to.
+  """
+  @type send_alert_message_metadata :: %__MODULE__{
+          channel_id: Snowflake.t()
+        }
+
+  @typedoc """
+  The number of seconds to timeout the user for,
+  has a maximum of 2419200 seconds (4 weeks).
+  """
+  @type timeout_metadata :: %__MODULE__{
+          duration_seconds: 1..2_419_200
+        }
+
+  @typedoc """
+  The type of metadata present depends on the action type.
+
+  | value | type
+  | ---- | ----
+  | `channel_id` | `SEND_ALERT_MESSAGE`
+  | `duration_seconds` | `TIMEOUT`
+  """
+  @type t :: send_alert_message_metadata() | timeout_metadata()
+
+  @doc false
+  def to_struct(map) do
+    new =
+      map
+      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
+      |> Map.update(:channel_id, nil, &Util.cast(&1, Snowflake))
+
+    struct(__MODULE__, new)
+  end
+end
diff --git a/lib/nostrum/struct/auto_moderation_rule/trigger_metadata.ex b/lib/nostrum/struct/auto_moderation_rule/trigger_metadata.ex
new file mode 100644
index 00000000..cc4d7cae
--- /dev/null
+++ b/lib/nostrum/struct/auto_moderation_rule/trigger_metadata.ex
@@ -0,0 +1,54 @@
+defmodule Nostrum.Struct.AutoModerationRule.TriggerMetadata do
+  @moduledoc """
+  Struct representing the metadata of a trigger.
+  """
+  @moduledoc since: "0.6.1"
+
+  alias Nostrum.Util
+
+  defstruct [
+    :keyword_filter,
+    :presets
+  ]
+
+  @typedoc """
+  A list of Values which represent the different presets defined by Discord
+
+  | value | type | description
+  | ---- | ---- | -----------
+  | `1` | `PROFANITY` | Words which may be considered profane
+  | `2` | `HARMFUL_LINK` | Words that refer to sexually explicit behavior or activity
+  | `3` | `SLURS` | Personal insults or words that may be considered hate speech
+  """
+  @type preset_value_metadata :: %__MODULE__{
+          presets: [1..3]
+        }
+
+  @typedoc """
+  Contains the list of keywords to that will trigger the rule.
+  """
+  @type keyword_metadata :: %__MODULE__{
+          keyword_filter: [String.t()]
+        }
+
+  @typedoc """
+  Additional data used to determine if the rule should triggered.
+
+  The `t:Nostrum.Struct.AutoModerationRule.trigger_type/0` of the parent struct determine which of the following fields are not `nil`.
+
+  | key | associated `trigger_type`
+  | ---- | -----------
+  | `keywords` | `KEYWORD`
+  | `preset` | `KEYWORD_PRESET`
+  """
+  @type t :: preset_value_metadata() | keyword_metadata()
+
+  @doc false
+  def to_struct(map) do
+    new =
+      map
+      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
+
+    struct(__MODULE__, new)
+  end
+end
diff --git a/lib/nostrum/struct/event/auto_moderation_rule_execute.ex b/lib/nostrum/struct/event/auto_moderation_rule_execute.ex
new file mode 100644
index 00000000..b2b28462
--- /dev/null
+++ b/lib/nostrum/struct/event/auto_moderation_rule_execute.ex
@@ -0,0 +1,103 @@
+defmodule Nostrum.Struct.Event.AutoModerationRuleExecute do
+  @moduledoc """
+  Sent when an auto-moderation rule executes.
+  (e.g. message is blocked).
+  """
+  @moduledoc since: "0.6.1"
+
+  alias Nostrum.Struct.{AutoModerationRule, Channel, Guild, Message}
+  alias Nostrum.{Snowflake, Util}
+
+  defstruct [
+    :guild_id,
+    :action,
+    :rule_id,
+    :rule_trigger_type,
+    :user_id,
+    :channel_id,
+    :message_id,
+    :alert_system_message_id,
+    :content,
+    :matched_keyword,
+    :matched_content
+  ]
+
+  @typedoc "The id of the guild in which the action was executed"
+  @type guild_id :: Guild.id()
+
+  @typedoc "The action that was executed"
+  @type action :: AutoModerationRule.Action.t()
+
+  @typedoc "The id of the rule that was executed"
+  @type rule_id :: AutoModerationRule.id()
+
+  @typedoc "The type of the rule that was executed"
+  @type rule_trigger_type :: AutoModerationRule.trigger_type()
+
+  @typedoc "The id of the user which generated the content which triggered the rule"
+  @type user_id :: User.id()
+
+  @typedoc """
+  The id of the channel in which the content was posted
+
+  note: this field may not exist if the content was blocked from being created
+  """
+  @type channel_id :: Channel.id() | nil
+
+  @typedoc """
+  The id of the message which was posted
+
+  note: this field will not exist if the content was blocked from being created
+  """
+  @type message_id :: Message.id() | nil
+
+  @typedoc """
+  The id of any system message that was generated as a result of the action
+
+  note: will not exist if the event does not correspond to an action that generates a system message
+  """
+  @type alert_system_message_id :: Message.id() | nil
+
+  @typedoc "The content of the message which triggered the rule"
+  @type content :: String.t()
+
+  @typedoc """
+  The keyword that was matched in the content
+  """
+  @type matched_keyword :: String.t() | nil
+
+  @typedoc """
+  The substring which matched the content
+  """
+  @type matched_content :: String.t() | nil
+
+  @type t :: %__MODULE__{
+          guild_id: guild_id(),
+          action: action(),
+          rule_id: rule_id(),
+          rule_trigger_type: rule_trigger_type(),
+          user_id: user_id(),
+          channel_id: channel_id(),
+          message_id: message_id(),
+          alert_system_message_id: alert_system_message_id(),
+          content: content(),
+          matched_keyword: matched_keyword(),
+          matched_content: matched_content()
+        }
+
+  @doc false
+  def to_struct(map) do
+    new =
+      map
+      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
+      |> Map.update(:guild_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:action, nil, &Util.cast(&1, {:struct, AutoModerationRule.Action}))
+      |> Map.update(:rule_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:user_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:channel_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:message_id, nil, &Util.cast(&1, Snowflake))
+      |> Map.update(:alert_system_message_id, nil, &Util.cast(&1, Snowflake))
+
+    struct(__MODULE__, new)
+  end
+end


Test patch:
diff --git a/test/nostrum/shard/intents_test.exs b/test/nostrum/shard/intents_test.exs
index a608c121..3febeb5a 100644
--- a/test/nostrum/shard/intents_test.exs
+++ b/test/nostrum/shard/intents_test.exs
@@ -10,7 +10,7 @@ defmodule Nostrum.Shard.IntentsTest do
       result = Intents.get_enabled_intents()
 
       # Value of all intents
-      expected = 131_071
+      expected = 3_276_799
 
       assert(^result = expected)
     end
@@ -21,7 +21,7 @@ defmodule Nostrum.Shard.IntentsTest do
       result = Intents.get_enabled_intents()
 
       # Value of all non-privileged intents
-      expected = 131_071 - (2 + 256 + 32_768)
+      expected = 3_276_799 - (2 + 256 + 32_768)
 
       assert(^result = expected)
     end