diff --git a/lib/nosedrum/application_command.ex b/lib/nosedrum/application_command.ex index d454389..b2a48f5 100644 --- a/lib/nosedrum/application_command.ex +++ b/lib/nosedrum/application_command.ex @@ -93,6 +93,7 @@ defmodule Nosedrum.ApplicationCommand do """ @moduledoc since: "0.4.0" + alias Nostrum.Permission alias Nostrum.Struct.{Embed, Interaction} @typedoc """ @@ -225,6 +226,21 @@ defmodule Nosedrum.ApplicationCommand do value: String.t() | number() } + @typedoc """ + An interaction context, which dictates where a command may be used (servers, DMs directly with + the bot user, or all other DMS). + + See callback `c:contexts/0` for more examples. + """ + @type context :: :guild | :bot_dms | :private_channel + + @typedoc """ + An installation context for a command (guild or user install). + + See callback `c:integration_types/0` for more examples. + """ + @type installation_context :: :guild_install | :user_install + @doc """ Returns one of `:slash`, `:message`, or `:user`, indicating what kind of application command this module represents. """ @@ -281,16 +297,54 @@ defmodule Nosedrum.ApplicationCommand do @callback options() :: [option] @doc """ - An optional callback that returns a bitset for the required default permissions to run this command. + An optional callback that returns a list of atoms for the required default permissions to run this command. - Example callback that requires that the user has the permission to ban members to be able to see and execute this command + ## Example - ```elixir - def default_member_permissions, do: - Nostrum.Permission.to_bitset([:ban_members]) - ``` + ```elixir + def default_member_permissions, do: [:ban_members, :kick_members, :manage_roles] + ``` """ - @callback default_member_permissions() :: String.t() + @callback default_member_permissions() :: [Permission.t()] + + @doc """ + An optional callback that returns a boolean, determining whether a command is + [age-restricted](https://discord.com/developers/docs/interactions/application-commands#agerestricted-commands). + + Defaults to `false`. + + ## Example + ```elixir + def nsfw, do: true + ``` + """ + @callback nsfw() :: boolean() + + @doc """ + An optional callback that returns a list of interaction contexts. + + Only applies to globally-scoped commands. + + If not set, this will default to guild and DMs directly with the bot user. + + # Example + ```elixir + def contexts, do: [:guild, :private_channel, :bot_dms] + ``` + """ + @callback contexts() :: [context] + + @doc """ + An optional callback that determines which installation context the command may be used in. + + If not set, this will default to all installation contexts. + + # Example + ```elixir + def integration_types, do: [:guild_install, :user_install] + ``` + """ + @callback integration_types() :: [installation_context] @doc """ Execute the command invoked by the given `t:Nostrum.Struct.Interaction.t/0`. Returns a `t:response/0` @@ -317,8 +371,14 @@ defmodule Nosedrum.ApplicationCommand do `Nostrum.Api.create_global_application_command/2` or `Nostrum.Api.create_guild_application_command/3` """ - @callback update_command_payload(map) :: map - @optional_callbacks [options: 0, default_member_permissions: 0, update_command_payload: 1] + @optional_callbacks [ + options: 0, + default_member_permissions: 0, + nsfw: 0, + contexts: 0, + integration_types: 0, + update_command_payload: 1 + ] end diff --git a/lib/nosedrum/component_interaction.ex b/lib/nosedrum/component_interaction.ex index c1290f1..1902f3f 100644 --- a/lib/nosedrum/component_interaction.ex +++ b/lib/nosedrum/component_interaction.ex @@ -10,7 +10,7 @@ defmodule Nosedrum.ComponentInteraction do @doc """ Handle message component interactions. - Behaves the same way as the `Nosedrum.ApplicationCommand.command/1` callback. + Behaves the same way as the `c:Nosedrum.ApplicationCommand.command/1` callback. """ @callback message_component_interaction( interaction :: Nostrum.Struct.Interaction.t(), diff --git a/lib/nosedrum/storage/dispatcher.ex b/lib/nosedrum/storage/dispatcher.ex index 18755e5..c5fa041 100644 --- a/lib/nosedrum/storage/dispatcher.ex +++ b/lib/nosedrum/storage/dispatcher.ex @@ -1,6 +1,9 @@ defmodule Nosedrum.Storage.Dispatcher do @moduledoc """ An implementation of `Nosedrum.Storage`, dispatching Application Command Interactions to the appropriate modules. + + + """ @moduledoc since: "0.4.0" @behaviour Nosedrum.Storage @@ -11,18 +14,43 @@ defmodule Nosedrum.Storage.Dispatcher do alias Nostrum.Api.ApplicationCommand alias Nostrum.Struct.Interaction - @option_type_map %{ - sub_command: 1, - sub_command_group: 2, - string: 3, - integer: 4, - boolean: 5, - user: 6, - channel: 7, - role: 8, - mentionable: 9, - number: 10, - attachment: 11 + require Logger + + @type_map %{ + options: %{ + sub_command: 1, + sub_command_group: 2, + string: 3, + integer: 4, + boolean: 5, + user: 6, + channel: 7, + role: 8, + mentionable: 9, + number: 10, + attachment: 11 + }, + contexts: %{ + guild: 0, + bot_dms: 1, + private_channel: 2 + }, + integration_types: %{ + guild_install: 0, + user_install: 1 + }, + commands: %{ + slash: 1, + user: 2, + message: 3 + } + } + + @optional_fields %{ + nsfw: 0, + default_member_permissions: 0, + contexts: 0, + integration_types: 0 } ## Api @@ -227,19 +255,13 @@ defmodule Nosedrum.Storage.Dispatcher do [] end - payload = - %{ - type: parse_type(command.type()), - name: name - } - |> put_type_specific_fields(command, options) - |> apply_payload_updates(command) - - if function_exported?(command, :default_member_permissions, 0) do - Map.put(payload, :default_member_permissions, command.default_member_permissions()) - else - payload - end + %{ + type: parse_type(command.type()), + name: name + } + |> put_type_specific_fields(command, options) + |> add_optional_fields(command) + |> apply_payload_updates(command) end # This seems like a hacky way to unwrap the outer list... @@ -276,11 +298,7 @@ defmodule Nosedrum.Storage.Dispatcher do defp parse_type(type) do Map.fetch!( - %{ - slash: 1, - user: 2, - message: 3 - }, + @type_map.commands, type ) end @@ -288,7 +306,7 @@ defmodule Nosedrum.Storage.Dispatcher do defp parse_option_types(options) do Enum.map(options, fn map when is_map_key(map, :type) -> - updated_map = Map.update!(map, :type, &Map.fetch!(@option_type_map, &1)) + updated_map = Map.update!(map, :type, &Map.fetch!(@type_map.options, &1)) if is_map_key(updated_map, :options) do parsed_options = parse_option_types(updated_map[:options]) @@ -336,4 +354,58 @@ defmodule Nosedrum.Storage.Dispatcher do payload end end + + defp add_optional_fields(payload, command) do + fun = fn {name, arity}, acc -> + if command |> function_exported?(name, arity) do + acc |> add_field(command, name) + else + acc + end + end + + Enum.reduce(@optional_fields, payload, fun) + end + + defp normalize_permissions(permissions) when is_list(permissions) do + Nostrum.Permission.to_bitset(permissions) + end + + defp normalize_permissions(permissions) when is_integer(permissions) do + Logger.warning(""" + DEPRECATION: Returning a bitset integer from default_member_permissions is deprecated. + Please return a list of Nostrum.Permission.t() atoms instead. + + For compatibility, integer bitsets will still be accepted and translated internally, + but this may be removed in a future release. + """) + + permissions + end + + defp add_field(payload, command, :default_member_permissions) do + Map.put( + payload, + :default_member_permissions, + command.default_members_permissions() |> normalize_permissions() + ) + end + + defp add_field(payload, command, :contexts) do + Map.put( + payload, + :contexts, + command.contexts() |> Enum.map(&@type_map.contexts[&1]) + ) + end + + defp add_field(payload, command, :integration_types) do + Map.put( + payload, + :integration_types, + command.integration_types() |> Enum.map(&@type_map.integration_types[&1]) + ) + end + + defp add_field(payload, command, name), do: Map.put(payload, name, apply(command, name, [])) end