Slink.EventsApi.Plug (Slink v0.6.0)

Copy Markdown View Source

Events API transport: a Plug that receives Slack's HTTP event callbacks.

It verifies Slack's request signature, answers the url_verification challenge, responds 200 immediately, and dispatches the event off-process (so your handler can't blow Slack's ~3s response budget).

Mount it in a router:

forward "/slack/events", to: Slink.EventsApi.Plug,
  init_opts: [
    module: MyBot,
    signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
    bot_token: System.fetch_env!("SLACK_BOT_TOKEN")
  ]

Or run it standalone with Bandit:

Bandit.start_link(
  plug: {Slink.EventsApi.Plug,
         module: MyBot,
         signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
         bot_token: System.fetch_env!("SLACK_BOT_TOKEN")},
  port: 4000
)

Options:

  • :module (required) — a module implementing the Slink behaviour.
  • :signing_secret (required) — the app's Signing Secret, for request verification. A string, or a 0-arity function returning one (resolved per request — see Mounting in Phoenix).
  • :bot_token — bot token (xoxb-…) passed to handlers for Web API calls. A string, a 0-arity function, or — for a multi-workspace app — a 1-arity function called with the event's team id (see Multiple workspaces).

Multiple workspaces

One app can serve many workspaces from a single endpoint. The signing secret is per-app, so it stays one value; only the bot token differs per install. Pass a 1-arity :bot_token — it's called with the event's team id (Slink.Event.team_id/1), so you look the right token up from your own store:

forward "/slack/events", to: Slink.EventsApi.Plug,
  init_opts: [
    module: MyBot,
    signing_secret: fn -> System.fetch_env!("SLACK_SIGNING_SECRET") end,
    bot_token: fn team_id -> MyApp.Installs.bot_token(team_id) end
  ]

Acquiring those per-team tokens is handled by Slink.OAuth (the "Add to Slack" consent URL, code exchange, and callback plug); storing them stays yours — Slink routes to whatever your :bot_token resolver returns.

Mounting in Phoenix

Two pitfalls when mounting inside a Phoenix app:

  • forward options are evaluated at compile time in production, so System.fetch_env!/1 in the router would read the env var on the build machine (or fail there). Pass functions instead — they're resolved per request:

    forward "/slack/events", to: Slink.EventsApi.Plug,
      init_opts: [
        module: MyBot,
        signing_secret: fn -> System.fetch_env!("SLACK_SIGNING_SECRET") end,
        bot_token: fn -> System.fetch_env!("SLACK_BOT_TOKEN") end
      ]
  • The raw body must still be readable. Signature verification hashes the raw request body, but Plug.Parsers (in every generated endpoint) consumes it before the router runs — mounted after it, every request 401s. Mount this plug in endpoint.ex before plug Plug.Parsers, or run it as a standalone listener (see above).

Slash commands & interactivity

Slack delivers those as application/x-www-form-urlencoded, not JSON; this plug decodes both. Point the app's Request URL for Interactivity and Slash Commands at this same endpoint. Slash commands and most interactions are handled off-process like events, and your handler replies via the response_url (see Slink.reply/3).

The one exception is view_submission (a modal submit): Slack expects a response_action in the immediate reply. For that type the handler runs synchronously and its {:ack, map} return is sent back as the response — so keep it fast (Slack's window is ~3s). Returning anything else closes the modal.