Lingo.Endpoint (Lingo v0.4.2)

Copy Markdown View Source

Receiving interactions and webhook events over HTTP, instead of (or next to) the gateway.

Discord can deliver interactions to an Interactions Endpoint URL and app events (an app added or removed, entitlements, lobby messages...) to a Webhook Events URL, both set in the Developer Portal. Every request is signed; Lingo checks the signature with your app's public key and answers Discord's checks. Interactions reach your bot's callbacks as usual and events reach Lingo.Bot.handle_event/2.

Configuration

{MyBot,
 token: System.fetch_env!("BOT_TOKEN"),
 endpoint: [public_key: System.fetch_env!("DISCORD_PUBLIC_KEY"), port: 4000],
 gateway: false}

endpoint options:

  • :public_key (required) - the app's public key, from the Developer Portal
  • :port - starts a small HTTP server (OTP's :httpd) on this port, answering POSTs on :interactions_path (default "/interactions") and :events_path (default "/events"). Put it behind a reverse proxy for HTTPS. Without :port, call interaction/3 and event/3 from your own web server.
  • :ip - the address the server listens on (default {0, 0, 0, 0})

gateway: false runs the bot without a gateway connection: commands and components work, gateway events don't arrive, and :register_commands registers when the bot starts.

Once an Interactions Endpoint URL is set, Discord sends interactions there and no longer over the gateway, so the endpoint has to be served even when the gateway runs too. Entitlement events come both through the gateway and as webhook events: a bot with both gets each one twice.

From your own web server

Hand over the request headers and the raw body, before anything parses it, and send back what you get. With Plug:

def call(%{method: "POST", path_info: ["interactions"]} = conn, _opts) do
  {:ok, body, conn} = Plug.Conn.read_body(conn)
  {status, headers, resp} = Lingo.Endpoint.interaction(MyBot, conn.req_headers, body)

  conn |> Plug.Conn.merge_resp_headers(headers) |> Plug.Conn.send_resp(status, resp)
end

How interactions are answered

An interaction is answered through the interaction callback, the same as with the gateway, and the HTTP request is held open until it has been (up to about 2.5 seconds), then answered with 202 and no body, as Discord documents for interactions answered through the callback. So Lingo.Context and collectors work unchanged, files and large responses included. An interaction nobody answers in time gets the 202 anyway, and Discord shows it as failed, as it does with the gateway.

Summary

Types

The bot's name: its module, or the :name it was started with.

Headers as a list or map of {name, value}, names in any case.

Functions

Handles a request to the Webhook Events URL: answers Discord's PING, rejects bad signatures with 401, and hands the event to the bot's Lingo.Bot.handle_event/2, as :application_authorized, :entitlement_create, :lobby_message_create and so on (see Lingo.Event).

Handles a request to the Interactions Endpoint URL: answers Discord's PING, rejects bad signatures with 401, and hands the interaction to the bot, returning once it's answered.

Whether a request carries Discord's valid signature for public_key.

Types

bot()

@type bot() :: atom()

The bot's name: its module, or the :name it was started with.

headers()

@type headers() :: Enumerable.t()

Headers as a list or map of {name, value}, names in any case.

response()

@type response() ::
  {status :: pos_integer(), [{String.t(), String.t()}], body :: binary()}

Functions

event(bot, headers, body)

@spec event(bot(), headers(), binary()) :: response()

Handles a request to the Webhook Events URL: answers Discord's PING, rejects bad signatures with 401, and hands the event to the bot's Lingo.Bot.handle_event/2, as :application_authorized, :entitlement_create, :lobby_message_create and so on (see Lingo.Event).

Raises ArgumentError when bot wasn't started with an :endpoint.

interaction(bot, headers, body)

@spec interaction(bot(), headers(), binary()) :: response()

Handles a request to the Interactions Endpoint URL: answers Discord's PING, rejects bad signatures with 401, and hands the interaction to the bot, returning once it's answered.

Raises ArgumentError when bot wasn't started with an :endpoint.

valid_signature?(headers, body, public_key)

@spec valid_signature?(headers(), binary(), String.t()) :: boolean()

Whether a request carries Discord's valid signature for public_key.