Slink behaviour (Slink v0.1.0)

Copy Markdown View Source

A lightweight Slack bot toolkit.

Slink gives you one event-handling contract and two interchangeable transports:

  • Slink.SocketMode β€” dials out to Slack over a WebSocket. No public endpoint required. Best for development and internal/behind-firewall apps.
  • Slink.EventsApi.Plug β€” a Plug that receives Slack's HTTP event callbacks. Best for production and distributed apps.

Both transports normalise Slack payloads into a Slink.Event and dispatch it to your module's handle_event/2. Write your bot once; pick the transport per environment (Socket Mode in dev, HTTP in prod β€” which is exactly what Slack recommends).

Defining a bot

defmodule MyBot do
  use Slink
  alias Slink.Event

  @impl true
  def handle_event(%Slink.Event{type: :app_mention} = event, _context) do
    # Return a reply and slink sends it (placement: to: :auto by default).
    {:reply, "hi <@#{Event.user(event)}> πŸ‘‹"}
  end

  def handle_event(_event, _context), do: :ok
end

Or reply imperatively β€” reply/3 returns :ok, so it can be the last expression, and to: picks where it lands (:auto, :thread, :channel):

def handle_event(%Slink.Event{type: :app_mention} = event, context) do
  reply(context, Event.command(event), to: :channel)
end

Running it (Socket Mode)

children = [
  {Slink.SocketMode,
   module: MyBot,
   app_token: System.fetch_env!("SLACK_APP_TOKEN"),
   bot_token: System.fetch_env!("SLACK_BOT_TOKEN")}
]

Supervisor.start_link(children, strategy: :one_for_one)

See the module docs for Slink.EventsApi.Plug to run the HTTP transport.

Summary

Types

Context passed to handle_event/2. See Slink.Context.

What a handler returns

Callbacks

Invoked for every event Slack delivers, from either transport.

Functions

Whether a bot should start, given its config.

Whether event happened inside a thread (imported by use Slink).

Reply to the event in context (imported by use Slink). Returns :ok, so a handler can end with it β€” no trailing :ok needed

Post a message to channel, using the bot token from the handler context.

Show a "working on it" indicator on the triggering message only if the work is slow, then clear it (imported by use Slink). Returns whatever fun returns.

Types

context()

@type context() :: Slink.Context.t()

Context passed to handle_event/2. See Slink.Context.

result()

@type result() :: :ok | {:reply, String.t()} | {:reply, String.t(), keyword()}

What a handler returns:

  • :ok β€” done, no reply.
  • {:reply, text} β€” slink replies with text via reply/3 with the default to: :auto placement (threaded if the event is in a thread, otherwise inline).
  • {:reply, text, opts} β€” same, passing opts to reply/3: to: :thread / to: :channel to force placement, and blocks: [...] / attachments: [...] for rich replies. text is still sent as the notification/fallback Slack shows in previews, so always provide something meaningful.

Any other value is treated as :ok (no reply).

Callbacks

handle_event(t, context)

@callback handle_event(Slink.Event.t(), context()) :: result()

Invoked for every event Slack delivers, from either transport.

Return :ok to do nothing, or {:reply, text} to reply (see result/0). The transport has already acknowledged the event to Slack before this runs, so slow work here never risks Slack's 3-second ACK window.

Functions

enabled?(config)

@spec enabled?(keyword() | map()) :: boolean()

Whether a bot should start, given its config.

Returns true only when :enabled is truthy and both :app_token and :bot_token are present. Use it to conditionally add Slink.SocketMode to a supervision tree, so an app without credentials (or with the bot switched off) simply doesn't connect:

children =
  if Slink.enabled?(config) do
    [{Slink.SocketMode, [module: MyBot] ++ config}]
  else
    []
  end

config is any keyword list or map (e.g. from Application.get_env/2).

in_thread?(event)

@spec in_thread?(Slink.Event.t()) :: boolean()

Whether event happened inside a thread (imported by use Slink).

Delegates to Slink.Event.in_thread?/1.

reply(context, text, opts \\ [])

@spec reply(context(), String.t(), keyword()) :: :ok

Reply to the event in context (imported by use Slink). Returns :ok, so a handler can end with it β€” no trailing :ok needed:

def handle_event(%Slink.Event{type: :app_mention} = event, context) do
  reply(context, "on it πŸ‘")
end

The channel and thread come from context.event (set by the dispatcher), so no event argument is needed. Where the reply lands is controlled by opts[:to]:

  • :auto (default) β€” dynamic: in the thread if the event is in one, otherwise inline in the channel.
  • :thread β€” always in a thread: the event's existing thread, or a new one started on the triggering message.
  • :channel β€” always inline in the channel timeline, even if the event was inside a thread.

Every other key in opts is merged into the Slack request body, for rich replies: blocks: [...] (Block Kit), attachments: [...], an explicit thread_ts:, etc.

reply(context, "deployed βœ…", to: :channel, blocks: blocks)

send_message(context, channel, text, opts \\ %{})

@spec send_message(context(), String.t(), String.t(), map()) :: :ok

Post a message to channel, using the bot token from the handler context.

Goes through Slink.Rate so sends are rate-limited per channel (Slack allows ~1/sec/channel). opts is merged into the request body (e.g. blocks, thread_ts). use Slink imports this, so handlers can call it unqualified:

def handle_event(%Slink.Event{type: :app_mention} = event, context) do
  send_message(context, Slink.Event.channel(event), "hi")
end

working(context, fun, opts \\ [])

@spec working(context(), (-> result), keyword()) :: result when result: var

Show a "working on it" indicator on the triggering message only if the work is slow, then clear it (imported by use Slink). Returns whatever fun returns.

Slack has no bot "typing…" indicator for channels, so this reacts to the event's message with an emoji (default hourglass_flowing_sand ⏳). To avoid a pointless flicker on fast replies, fun runs in a task and the reaction is added only if it's still running after :delay_ms (default 3000ms); it's always removed once fun finishes β€” even if it raises. Fast work shows nothing. So it's safe to wrap any handler:

def handle_event(%Slink.Event{type: :app_mention} = event, context) do
  working(context, fn -> reply(context, answer(event)) end)
end

Options:

  • :delay_ms β€” how long to wait before showing the indicator (default 3000). Use 0 to show it immediately.
  • :emoji β€” reaction name without colons (default "hourglass_flowing_sand").

Best-effort: reaction API errors are ignored so they never break the handler, and if the event has no message to react to, fun just runs inline.