EDA.Interaction (EDA v0.5.0-beta.2)

Copy Markdown View Source

Helpers for working with Discord interactions.

Works directly with the raw interaction maps received from the gateway, providing convenient accessors and response helpers.

Handling Slash Commands

def handle_event({:INTERACTION_CREATE, interaction}) do
  import EDA.Interaction

  case command_name(interaction) do
    "ping" ->
      respond(interaction, "Pong!")

    "greet" ->
      msg = get_option(interaction, "message")
      target = get_option(interaction, "target")
      respond(interaction, content: "<@#{target}> #{msg}", ephemeral: true)

    "role" ->
      case sub_command_name(interaction) do
        "add" ->
          role_id = get_option(interaction, "role")
          respond(interaction, "Added <@&#{role_id}>!")

        "remove" ->
          respond(interaction, "Removed!")
      end
  end
end

Deferred Responses

# Show "thinking..." then edit later
defer(interaction)
# ... do work ...
edit_response(interaction, content: "Done!")

# Ephemeral defer
defer(interaction, ephemeral: true)

Summary

Functions

Returns the permissions the bot holds, as a bitset.

Returns the bot's permissions in a resolved channel, as a bitset. See app_permissions/1.

Responds with autocomplete results.

Returns true if the bot holds a permission.

Returns true if the bot holds a permission in a resolved channel. See can?/2.

Returns the channel ID.

Returns the command name from the interaction data.

Returns the command type as an atom.

Returns the component type for a message component interaction.

Returns the custom_id for component interactions and modal submits.

Defers the interaction response (shows "thinking..." indicator).

Defers the interaction, runs the given function, then edits the response.

Deletes the original interaction response.

Deletes the message that triggered a component interaction.

Edits the original interaction response (typically after deferring).

Sends a followup message to the interaction.

Gets an option value by name from the interaction.

Returns all options as a flat map of %{"name" => value}.

Returns the guild ID, or nil in DMs.

Returns the interaction type as an atom.

Returns the guild member map, or nil in DMs.

Names every permission the bot holds, for the interaction's channel or a resolved one.

Names the bot's permissions in a resolved channel. See permission_list/1.

Returns a resolved object by type and ID.

Returns the resolved channel with this ID, as an EDA.Channel struct.

Returns every resolved channel as an EDA.Channel struct.

Sends an immediate response to the interaction.

Responds to an interaction by opening a modal dialog.

Returns the selected values from a select menu interaction.

Returns the sub_command name, or nil if not a sub_command invocation.

Returns the target ID for user/message context menu commands.

Returns the interaction token.

Returns the user who triggered the interaction (works in both guild and DM).

Returns true if the invoking user holds a permission.

Returns true if the invoking user holds a permission in a resolved channel.

Returns the permissions the invoking user holds, as a bitset.

Returns the invoking user's permissions in a resolved channel. See user_permissions/1.

Types

interaction()

@type interaction() :: map()

Functions

app_permissions(arg1)

@spec app_permissions(interaction()) :: integer() | nil

Returns the permissions the bot holds, as a bitset.

With one argument, this is what Discord computed for the channel the interaction came from. With a channel id, it is the bot's permissions in that resolved channel — the one a CHANNEL option named — which is usually a different channel from the one the command was typed in.

Discord computes both and sends them with the interaction, so this answers "may I post there?" with no REST call and no permission arithmetic. EDA.Permission.in_channel/3 remains the answer for a channel Discord did not resolve.

# the channel the user picked, not the one they typed in
destination = get_option(interaction, "destination")

if EDA.Interaction.can?(interaction, destination, :send_messages) do
  EDA.API.Message.create(destination, content: "Posted!")
else
  respond(interaction, "I cannot post there.", ephemeral: true)
end

Returns nil when the field is absent — a channel that was not resolved, an interaction from a context where Discord did not send it, or an app installed to a user rather than to the guild, since Discord only computes a channel's app_permissions when the bot is a member of that guild.

nil is not 0. Being told nothing is not the same as being denied everything, so the two are kept apart; can?/2 treats both as "no".

app_permissions(interaction, channel_id)

@spec app_permissions(interaction(), String.t()) :: integer() | nil

Returns the bot's permissions in a resolved channel, as a bitset. See app_permissions/1.

autocomplete(interaction, choices)

@spec autocomplete(interaction(), [{String.t(), term()}]) :: :ok | {:error, term()}

Responds with autocomplete results.

Takes a list of {name, value} tuples (max 25).

Example

autocomplete(interaction, [
  {"Option A", "a"},
  {"Option B", "b"}
])

can?(interaction, flag)

@spec can?(interaction(), EDA.Permission.flag()) :: boolean()

Returns true if the bot holds a permission.

can?/2 asks about the channel the interaction came from, can?/3 about a resolved channel. An absent bitset is false: not being told is not permission.

Examples

can?(interaction, :embed_links)
can?(interaction, channel_id, :send_messages)

can?(interaction, channel_id, flag)

@spec can?(interaction(), String.t(), EDA.Permission.flag()) :: boolean()

Returns true if the bot holds a permission in a resolved channel. See can?/2.

channel_id(arg1)

@spec channel_id(interaction()) :: String.t() | nil

Returns the channel ID.

command_name(arg1)

@spec command_name(interaction()) :: String.t() | nil

Returns the command name from the interaction data.

command_type(arg1)

@spec command_type(interaction()) :: :slash | :user | :message | nil

Returns the command type as an atom.

  • :slash (type 1, CHAT_INPUT)
  • :user (type 2, USER context menu)
  • :message (type 3, MESSAGE context menu)

component_type(arg1)

@spec component_type(interaction()) :: non_neg_integer() | nil

Returns the component type for a message component interaction.

Returns nil if not a component interaction.

Common types: 2 = button, 3 = string select, 5 = user select, 6 = role select, 7 = mentionable select, 8 = channel select.

Examples

case EDA.Interaction.component_type(interaction) do
  2 -> handle_button(interaction)
  3 -> handle_select(interaction)
  _ -> :ignore
end

custom_id(arg1)

@spec custom_id(interaction()) :: String.t() | nil

Returns the custom_id for component interactions and modal submits.

defer(interaction, opts \\ [])

@spec defer(
  interaction(),
  keyword()
) :: :ok | {:error, term()}

Defers the interaction response (shows "thinking..." indicator).

Must be followed by edit_response/2 within 15 minutes.

Options

  • :ephemeral - If true, the thinking indicator and subsequent response are only visible to the invoking user.

defer_and_edit(interaction, fun, opts \\ [])

@spec defer_and_edit(interaction(), (-> String.t() | keyword()), keyword()) ::
  {:ok, map()} | {:error, term()}

Defers the interaction, runs the given function, then edits the response.

Wraps the common defer → do work → edit_response pattern in a single call. The function receives no arguments and should return a string or keyword list suitable for edit_response/2.

Options

  • :ephemeral — if true, the thinking indicator and response are ephemeral

Examples

EDA.Interaction.defer_and_edit(interaction, fn ->
  result = do_heavy_work()
  "Result: #{result}"
end)

EDA.Interaction.defer_and_edit(interaction, fn ->
  data = fetch_data()
  [content: "Here's your data", embeds: [build_embed(data)]]
end, ephemeral: true)

delete_response(interaction)

@spec delete_response(interaction()) :: :ok | {:error, term()}

Deletes the original interaction response.

delete_source(interaction)

@spec delete_source(interaction()) :: :ok | {:error, term()}

Deletes the message that triggered a component interaction.

Works for both ephemeral and non-ephemeral messages. Uses Discord's type 6 (DEFERRED_UPDATE_MESSAGE) to claim ownership of the source message, then deletes it via delete_response.

Important: After calling delete_source/1, the interaction is already acknowledged. Use followup/2 instead of respond/2 for any reply:

# Correct pattern:
delete_source(interaction)
followup(interaction, content: "Done!", ephemeral: true)

# WRONG — will fail because interaction is already acknowledged:
delete_source(interaction)
respond(interaction, "Done!")

Examples

# User clicks "Confirm" button → delete the prompt, show next step
EDA.Interaction.delete_source(interaction)
EDA.Interaction.followup(interaction, content: "Next step...", components: [select_menu])

edit_response(interaction, content)

@spec edit_response(interaction(), String.t() | keyword()) ::
  {:ok, map()} | {:error, term()}

Edits the original interaction response (typically after deferring).

Examples

edit_response(interaction, "Done!")
edit_response(interaction, content: "Updated!", embeds: [embed])

followup(interaction, content)

@spec followup(interaction(), String.t() | keyword()) ::
  {:ok, map()} | {:error, term()}

Sends a followup message to the interaction.

Examples

followup(interaction, "Another message!")
followup(interaction, content: "Followup", ephemeral: true)

get_option(interaction, name, default \\ nil)

@spec get_option(interaction(), String.t(), term()) :: term()

Gets an option value by name from the interaction.

Automatically traverses into sub_commands and sub_command_groups to find the option.

Returns nil if not found, or default if provided.

get_options(interaction)

@spec get_options(interaction()) :: %{required(String.t()) => term()}

Returns all options as a flat map of %{"name" => value}.

Traverses sub_commands and sub_command_groups automatically.

guild_id(arg1)

@spec guild_id(interaction()) :: String.t() | nil

Returns the guild ID, or nil in DMs.

interaction_type(arg1)

@spec interaction_type(interaction()) :: atom() | nil

Returns the interaction type as an atom.

  • :ping (1)
  • :command (2, APPLICATION_COMMAND)
  • :component (3, MESSAGE_COMPONENT)
  • :autocomplete (4, APPLICATION_COMMAND_AUTOCOMPLETE)
  • :modal_submit (5, MODAL_SUBMIT)

member(arg1)

@spec member(interaction()) :: EDA.Member.t() | map() | nil

Returns the guild member map, or nil in DMs.

permission_list(interaction)

@spec permission_list(interaction()) :: [EDA.Permission.flag()]

Names every permission the bot holds, for the interaction's channel or a resolved one.

Handy in an error message or a log line, where the bitset says nothing.

Examples

permission_list(interaction)
permission_list(interaction, channel_id)

permission_list(interaction, channel_id)

@spec permission_list(interaction(), String.t()) :: [EDA.Permission.flag()]

Names the bot's permissions in a resolved channel. See permission_list/1.

resolved(arg1, type, id)

@spec resolved(interaction(), String.t(), String.t()) :: map() | nil

Returns a resolved object by type and ID.

Types: "users", "members", "roles", "channels", "messages", "attachments".

resolved_channel(interaction, channel_id)

@spec resolved_channel(interaction(), String.t()) :: EDA.Channel.t() | nil

Returns the resolved channel with this ID, as an EDA.Channel struct.

Resolved channels are partial: Discord sends id, name, type, permissions, app_permissions, parent_id, guild_id, flags, nsfw, position, topic, rate_limit_per_user, last_message_id and last_pin_timestamp, and nothing else. Everything absent is nil on the struct rather than missing.

channel_id = get_option(interaction, "destination")
channel = EDA.Interaction.resolved_channel(interaction, channel_id)

resolved_channels(interaction)

@spec resolved_channels(interaction()) :: [EDA.Channel.t()]

Returns every resolved channel as an EDA.Channel struct.

Empty when the command took no channel option.

respond(interaction, content)

@spec respond(interaction(), String.t() | keyword()) :: :ok | {:error, term()}

Sends an immediate response to the interaction.

Examples

respond(interaction, "Hello!")
respond(interaction, content: "Hello!", ephemeral: true)
respond(interaction, content: "Look!", embeds: [embed])

respond_modal(interaction, modal)

@spec respond_modal(interaction(), map()) :: :ok | {:error, term()}

Responds to an interaction by opening a modal dialog.

Takes a modal map built with EDA.Modal.modal/3+.

Example

import EDA.Modal

modal =
  modal("feedback", "Feedback",
    text_input("subject", "Subject", :short),
    text_input("body", "Details", :paragraph)
  )

respond_modal(interaction, modal)

selected_values(arg1)

@spec selected_values(interaction()) :: [String.t()]

Returns the selected values from a select menu interaction.

Returns an empty list if the interaction is not a select menu.

Examples

values = EDA.Interaction.selected_values(interaction)
# => ["option_1", "option_2"]

sub_command_name(arg1)

@spec sub_command_name(interaction()) :: String.t() | {String.t(), String.t()} | nil

Returns the sub_command name, or nil if not a sub_command invocation.

For sub_command_groups, returns {group_name, sub_command_name}.

target_id(arg1)

@spec target_id(interaction()) :: String.t() | nil

Returns the target ID for user/message context menu commands.

token(arg1)

@spec token(interaction()) :: String.t() | nil

Returns the interaction token.

user(arg1)

@spec user(interaction()) :: EDA.User.t() | map() | nil

Returns the user who triggered the interaction (works in both guild and DM).

user_can?(interaction, flag)

@spec user_can?(interaction(), EDA.Permission.flag()) :: boolean()

Returns true if the invoking user holds a permission.

user_can?/2 asks about the channel the interaction came from, user_can?/3 about a resolved channel. See user_permissions/1.

user_can?(interaction, channel_id, flag)

@spec user_can?(interaction(), String.t(), EDA.Permission.flag()) :: boolean()

Returns true if the invoking user holds a permission in a resolved channel.

user_permissions(arg1)

@spec user_permissions(interaction()) :: integer() | nil

Returns the permissions the invoking user holds, as a bitset.

With one argument, this is what Discord computed for them in the channel the interaction came from — it rides along on the interaction's member. With a channel id, it is their permissions in that resolved channel.

Discord computes both alongside the bot's, which is what makes "you cannot do that here" answerable without fetching the member and their roles.

if EDA.Interaction.user_can?(interaction, destination, :manage_messages) do
  # ...
end

nil outside a guild, since a DM has no member.

user_permissions(interaction, channel_id)

@spec user_permissions(interaction(), String.t()) :: integer() | nil

Returns the invoking user's permissions in a resolved channel. See user_permissions/1.