EDA.API.Message (EDA v0.5.0-beta.2)

Copy Markdown View Source

REST API endpoints for Discord messages.

All functions return {:ok, result} or {:error, reason}.

Summary

Functions

Bulk deletes messages (2-100, not older than 14 days).

Creates a message in a channel.

Publishes a message in an announcement channel to every channel following it.

Forwards a message to another channel.

Gets a message by ID.

Retrieves message history from a channel with automatic pagination.

Gets messages from a channel.

Pins a message in a channel.

Gets pinned messages in a channel, paginating automatically.

Gets one page of a channel's pins.

Purges messages from a channel with automatic chunking and 14-day filtering.

Replies to a message, automatically setting message_reference.

Searches a guild's message history.

Returns a lazy Stream that yields messages from a channel, page by page.

Unpins a message from a channel.

Functions

bulk_delete(channel_id, message_ids)

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

Bulk deletes messages (2-100, not older than 14 days).

create(channel_id, content)

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

Creates a message in a channel.

Parameters

  • channel_id - The ID of the channel
  • content - Message content (string), full message payload (map), or keyword options

Examples

EDA.API.Message.create(channel_id, "Hello!")
EDA.API.Message.create(channel_id, content: "Look!", embeds: [embed])
EDA.API.Message.create(channel_id, content: "File!", files: [file])

crosspost(channel_id, message_id)

@spec crosspost(String.t() | integer(), String.t() | integer()) ::
  {:ok, map()} | {:error, term()}

Publishes a message in an announcement channel to every channel following it.

POST /channels/{channel_id}/messages/{message_id}/crosspost. Needs SEND_MESSAGES for the bot's own message, and MANAGE_MESSAGES as well for anyone else's. A message is published once; publishing it again fails with 40033 (EDA.Error.message_already_crossposted/0).

delete(channel_id, message_id, opts \\ [])

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

Deletes a message.

edit(channel_id, message_id, opts)

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

Edits a message.

Accepts a keyword list of options (which may upload files) or a raw payload map.

The attachments array replaces, it does not merge

Discord treats attachments as the complete list the message should end up with, so an attachment left out of it is deleted. Uploading a file without naming the existing ones therefore removes them:

# keeps only the newly uploaded file — the message's other attachments are gone
EDA.API.Message.edit(channel_id, message_id, files: [EDA.File.from_path("new.png")])

Name them with EDA.Attachment.keep/2 to hold on to them, and the upload is appended:

EDA.API.Message.edit(channel_id, message_id,
  attachments: Enum.map(message.attachments, &EDA.Attachment.keep/1),
  files: [EDA.File.from_path("new.png")]
)

:attachments also takes bare ids and raw attachment maps. EDA.Message.edit/2 has the shorter attachments: :keep for a message struct you already hold.

Omitting :attachments entirely leaves the message's attachments untouched — it is only sending the array that is destructive.

Changing an existing attachment

EDA.Attachment.keep/2 carries the two fields Discord lets an edit update, so a spoiler can be applied after the fact without re-uploading the file:

EDA.API.Message.edit(channel_id, message_id,
  attachments: [EDA.Attachment.keep(attachment, is_spoiler: true, description: "Ending")]
)

edit(channel_id, message_id, payload, opts \\ [])

forward(target_channel_id, source_channel_id, message_id)

@spec forward(String.t() | integer(), String.t() | integer(), String.t() | integer()) ::
  {:ok, map()} | {:error, term()}

Forwards a message to another channel.

Creates a message reference with type: 1 (forward) pointing to the original message.

The bot must be able to read the message it forwards

Forwarding is not a way to relay a message out of a channel the bot cannot see. Discord checks read access to the source message's content at forward time and rejects the request with code 160014 otherwise — typically a missing view_channel or read_message_history in the source channel, or a source channel the bot is no longer in:

case EDA.API.Message.forward(target_id, source_id, message_id) do
  {:ok, message} -> message
  {:error, %{code: 160_014}} -> :cannot_read_source
end

EDA.Error.cannot_forward_unreadable_message/0 names that code.

Examples

EDA.API.Message.forward(target_channel_id, source_channel_id, message_id)

get(channel_id, message_id)

@spec get(String.t() | integer(), String.t() | integer()) ::
  {:ok, map()} | {:error, term()}

Gets a message by ID.

history(channel_id, limit, opts \\ [])

@spec history(String.t() | integer(), pos_integer() | :infinity, keyword()) ::
  {:ok, [map()]} | {:error, term()}

Retrieves message history from a channel with automatic pagination.

Handles Discord's 100-message-per-request limit transparently. Supports :infinity to retrieve all messages.

Options

  • :before - snowflake ID, get messages before this
  • :after - snowflake ID, get messages after this
  • :around - snowflake ID, get messages around this (single page, max 100)

Examples

{:ok, msgs} = EDA.API.Message.history(channel_id, 250)
{:ok, msgs} = EDA.API.Message.history(channel_id, 500, before: msg_id)
{:ok, msgs} = EDA.API.Message.history(channel_id, :infinity)

list(channel_id, opts \\ [])

@spec list(
  String.t() | integer(),
  keyword()
) :: {:ok, [map()]} | {:error, term()}

Gets messages from a channel.

pin(channel_id, message_id, opts \\ [])

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

Pins a message in a channel.

Options

  • :reason - Audit log reason

pinned(channel_id, opts \\ [])

@spec pinned(
  String.t() | integer(),
  keyword()
) :: {:ok, [map()]} | {:error, term()}

Gets pinned messages in a channel, paginating automatically.

Returns message objects. Use pins/2 if you need the pinned_at timestamps or want to drive the pagination yourself.

Options

  • :limit - maximum number of messages to return (default :infinity)

Examples

{:ok, msgs} = EDA.API.Message.pinned(channel_id)
{:ok, msgs} = EDA.API.Message.pinned(channel_id, limit: 10)

pins(channel_id, opts \\ [])

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

Gets one page of a channel's pins.

Returns Discord's raw envelope: %{"items" => [pin], "has_more" => boolean}, where each pin is %{"pinned_at" => iso8601, "message" => message}.

Options

  • :before - ISO8601 timestamp, get pins pinned before this (use the pinned_at of the last item of the previous page)
  • :limit - pins per page (1-50, default 50)

purge(channel_id, opts \\ [])

@spec purge(
  String.t() | integer(),
  keyword()
) :: {:ok, non_neg_integer()} | {:error, term()}

Purges messages from a channel with automatic chunking and 14-day filtering.

Options

  • :limit - max messages to purge (default 100, max :infinity)
  • :before - purge messages before this ID
  • :after - purge messages after this ID
  • :filter - predicate fn(message) -> boolean to select which messages to purge
  • :filter_old - filter out messages older than 14 days (default true)

Examples

{:ok, count} = EDA.API.Message.purge(channel_id, limit: 200)
{:ok, count} = EDA.API.Message.purge(channel_id,
  limit: 500,
  filter: fn msg -> msg["author"]["id"] == user_id end
)

reply(map, content)

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

Replies to a message, automatically setting message_reference.

Accepts a message struct (with :channel_id and :id) or a raw map (with "channel_id" and "id").

Examples

EDA.API.Message.reply(msg, "Got it!")
EDA.API.Message.reply(msg, content: "Reply with embed", embeds: [embed])

search(guild_id, opts \\ [])

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

Searches a guild's message history.

GET /guilds/{guild_id}/messages/search. Requires READ_MESSAGE_HISTORY in the channels searched, and is gated on the MESSAGE_CONTENT privileged intent.

Returns Discord's raw payload. EDA.Message.search/2 returns parsed structs and flattens the nesting described below.

The messages key is a list of lists

Each entry is a context group, not a message: Discord returns the matching message surrounded by its neighbours, and marks the match itself with "hit" => true. A group of one is the usual case, but nothing promises it.

{:ok, %{"messages" => groups, "total_results" => total}} =
  EDA.API.Message.search(guild_id, content: "deploy")

hits = Enum.map(groups, fn group -> Enum.find(group, & &1["hit"]) end)

Options

Multi-valued filters take a list and become repeated query keys.

  • :content - free text, max 1024 characters
  • :channel_id - restrict to these channels, max 500
  • :author_id - max 100 · :author_type - :user, :bot or :webhook
  • :mentions - messages mentioning these users, max 100
  • :mentions_role_id - max 100 · :mention_everyone - boolean
  • :replied_to_user_id, :replied_to_message_id - max 100
  • :has - :image, :sound, :video, :file, :sticker, :embed, :link, :poll or :snapshot
  • :embed_type, :embed_provider, :link_hostname, :attachment_filename, :attachment_extension - max 100 each
  • :pinned - boolean · :include_nsfw - boolean, default false
  • :min_id / :max_id - snowflake bounds
  • :sort_by - :timestamp (default) or :relevance · :sort_order - :desc or :asc
  • :limit - 1–25, default 25 · :offset - max 9975
  • :slop - words allowed between matching tokens, max 100, default 2

Examples

EDA.API.Message.search(guild_id,
  content: "incident",
  channel_id: [ops_channel, alerts_channel],
  has: [:link],
  sort_by: :relevance,
  limit: 10
)

An option this endpoint does not define is refused, not forwarded: Discord ignores a query parameter it does not recognise, so contnet: would quietly return the guild's whole history while looking like a filtered search.

Caveats Discord documents

  • Sort order is ignored when sorting by relevance.
  • total_results is approximate while messages are being created or deleted, and a page may come back slightly shorter than :limit.
  • :offset caps at 9975, so at most ten thousand results are reachable — narrow the query rather than paging to the end.
  • A guild whose history is still being indexed answers HTTP 202 rather than results. That is a success status carrying no messages, so it is reported as {:error, {:index_pending, retry_after_seconds}} instead of an empty search.

stream(channel_id, opts \\ [])

@spec stream(
  String.t() | integer(),
  keyword()
) :: Enumerable.t()

Returns a lazy Stream that yields messages from a channel, page by page.

Options

  • :before - start before this message ID
  • :after - start after this message ID
  • :per_page - messages per request (1-100, default 100)

Examples

EDA.API.Message.stream(channel_id) |> Stream.take(50) |> Enum.to_list()
EDA.API.Message.stream(channel_id) |> Enum.find(&(&1["author"]["id"] == user_id))

unpin(channel_id, message_id, opts \\ [])

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

Unpins a message from a channel.

Options

  • :reason - Audit log reason