Packages

A Slack Web & Real Time Messaging API client.

Current section

Files

Jump to
slack_kit lib slack bot.ex
Raw

lib/slack/bot.ex

defmodule Slack.Bot do
@moduledoc """
Process that owns a bot's RTM WebSocket connection.
`Slack.Bot` is the runtime counterpart to the `Slack` behaviour: it opens the
WebSocket, decodes inbound frames, folds them into a `Slack.State`, and
dispatches each event to the user-supplied `bot_handler` module's callbacks.
## Lifecycle
`start_link/4` performs three steps:
1. Hits Slack's `rtm.start` endpoint to fetch a one-shot WebSocket URL plus
the bot's initial team/identity payload. The module that performs this
can be overridden via `config :slack, :rtm_module, …` for tests.
2. Spawns a `Slack.WebSocketClient` pointed at that URL with `Slack.Bot`
itself as the callback module.
3. Each inbound text frame is JSON-decoded, atomised, run through
`Slack.State.update/2`, and forwarded to the user module's
`handle_event/3`.
You generally won't call this module's callbacks directly — they implement the
`Slack.WebSocketClient` behaviour and exist to glue the socket to your bot.
"""
require Logger
@behaviour Slack.WebSocketClient
@doc """
Connects to Slack and delegates events to `bot_handler`.
## Arguments
* `bot_handler` — module implementing the `Slack` behaviour (typically a
module that does `use Slack`).
* `initial_state` — opaque value handed to the first callback invocation
as the `state` argument. Threaded through callbacks from then on.
* `token` — the Slack API token for this bot.
* `options` — map of optional settings (see below).
## Options
* `:keepalive` — milliseconds between WebSocket keepalive pings. Defaults
to `10_000`.
* `:name` — registers the spawned process under the given atom via
`Process.register/2`. Defaults to `nil` (unregistered).
* `:client` — module that backs the WebSocket transport. Must implement
`start_link/4` and `cast/2`. Defaults to `Slack.WebSocketClient`;
tests swap in a stub.
## Return values
Returns `{:ok, pid}` on a successful RTM handshake, or `{:error, reason}` on
failure. Known reasons include:
* `"Timed out while connecting to the Slack RTM API"`
* `"Could not connect to the Slack RTM API"` — DNS resolution failed
* `"Sent too many connection requests at once to the Slack RTM API."` —
Slack has rate-limited the `rtm.start` endpoint
* any other error returned by the underlying HTTP client
## Examples
{:ok, pid} = Slack.Bot.start_link(MyBot, [], "xoxb-…", %{name: :my_bot})
:sys.get_state(:my_bot)
"""
def start_link(bot_handler, initial_state, token, options \\ %{}) do
options =
Map.merge(
%{
client: Slack.WebSocketClient,
keepalive: 10_000,
name: nil
},
Map.new(options)
)
rtm_module = Application.get_env(:slack, :rtm_module, Slack.Rtm)
case rtm_module.start(token) do
{:ok, rtm} ->
state = %{
bot_handler: bot_handler,
rtm: rtm,
client: options.client,
token: token,
initial_state: initial_state
}
{:ok, pid} =
options.client.start_link(state.rtm.url, __MODULE__, state,
keepalive: options.keepalive
)
if options.name != nil do
Process.register(pid, options.name)
end
{:ok, pid}
{:error, %Req.TransportError{reason: :timeout}} ->
{:error, "Timed out while connecting to the Slack RTM API"}
{:error, %Req.TransportError{reason: :nxdomain}} ->
{:error, "Could not connect to the Slack RTM API"}
{:error, %Slack.JsonDecodeError{string: "You are sending too many requests. Please relax."}} ->
{:error, "Sent too many connection requests at once to the Slack RTM API."}
{:error, error} ->
{:error, error}
end
end
@doc false
@deprecated """
`rtm.start` is replaced with `rtm.connect` and will no longer receive bots, channels, groups, users, or IMs.
In future versions these will no longer be provided on initialization.
"""
def init(%{
bot_handler: bot_handler,
rtm: rtm,
client: client,
token: token,
initial_state: initial_state
}) do
slack = %Slack.State{
process: self(),
client: client,
token: token,
me: rtm.self,
team: rtm.team
}
{:reconnect, %{slack: slack, bot_handler: bot_handler, process_state: initial_state}}
end
@doc false
def onconnect(
_websocket_request,
%{slack: slack, process_state: process_state, bot_handler: bot_handler} = state
) do
{:ok, new_process_state} = bot_handler.handle_connect(slack, process_state)
{:ok, %{state | process_state: new_process_state}}
end
@doc false
def ondisconnect({:error, :keepalive_timeout}, state) do
{:reconnect, state}
end
def ondisconnect(
reason,
%{slack: slack, process_state: process_state, bot_handler: bot_handler} = state
) do
try do
bot_handler.handle_close(reason, slack, process_state)
{:close, reason, state}
rescue
e -> log_and_reraise(e, __STACKTRACE__)
end
end
@doc false
def websocket_info(
message,
_connection,
%{slack: slack, process_state: process_state, bot_handler: bot_handler} = state
) do
try do
{:ok, new_process_state} = bot_handler.handle_info(message, slack, process_state)
{:ok, %{state | process_state: new_process_state}}
rescue
e -> log_and_reraise(e, __STACKTRACE__)
end
end
@doc false
def websocket_terminate(_reason, _conn, _state), do: :ok
@doc false
def websocket_handle(
{:text, message},
_conn,
%{slack: slack, process_state: process_state, bot_handler: bot_handler} = state
) do
message = prepare_message(message)
updated_slack =
if Map.has_key?(message, :type) do
Slack.State.update(message, slack)
else
slack
end
new_process_state =
if Map.has_key?(message, :type) do
try do
{:ok, new_process_state} = bot_handler.handle_event(message, slack, process_state)
new_process_state
rescue
e -> log_and_reraise(e, __STACKTRACE__)
end
else
process_state
end
{:ok, %{state | slack: updated_slack, process_state: new_process_state}}
end
def websocket_handle(_, _conn, state), do: {:ok, state}
defp prepare_message(binstring) do
binstring
|> :binary.split(<<0>>)
|> List.first()
|> JSON.decode!()
|> Slack.JSON.atomize_keys()
end
defp log_and_reraise(e, stacktrace) do
Logger.error(Exception.format(:error, e, stacktrace))
reraise e, stacktrace
end
end