defmodule Slack.Bot do @moduledoc """ This module is used to spawn bots and is used to manage the connection to Slack while delegating events to the specified bot module. """ require Logger @behaviour :websocket_client @doc """ Connects to Slack and delegates events to `bot_handler`. ## Options * `keepalive` - How long to wait for the connection to respond before the client kills the connection. * `name` - registers a name for the process with the given atom ## Examples {:ok, pid} = Slack.Bot.start_link(MyBot, [1,2,3], "abc-123", %{name: :slack_bot}) :sys.get_state(:slack_bot) """ def start_link(bot_handler, initial_state, token, options \\ %{}) do options = Map.merge( %{ client: :websocket_client, 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 } url = String.to_charlist(state.rtm.url) {:ok, pid} = options.client.start_link(url, __MODULE__, state, keepalive: options.keepalive) if options.name != nil do Process.register(pid, options.name) end {:ok, pid} {:error, %HTTPoison.Error{reason: :connect_timeout}} -> {:error, "Timed out while connecting to the Slack RTM API"} {:error, %HTTPoison.Error{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 -> Logger.error(__STACKTRACE__) handle_exception(e) {:close, reason, state} 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 -> Logger.error(__STACKTRACE__) handle_exception(e) {:ok, state} 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 -> Logger.error(__STACKTRACE__) handle_exception(e) 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() |> Jason.decode!(keys: :atoms) end defp handle_exception(e) do message = Exception.message(e) Logger.error(message) raise message end end