Packages

An Elixir Slack client and bot.

Current section

Files

Jump to
slacks lib slack bot bot.ex
Raw

lib/slack/bot/bot.ex

defmodule Slack.Bot do
@moduledoc ~S"""
## Options
- `token` (required), the bot Slack token.
"""
### Macro Details ###
@callback init(state :: any) :: {:ok, state :: any} | {:error, atom}
@callback handle_info(
message :: term,
conn :: Slack.Bot.Conn.t(),
state :: any
) ::
:ok
| {:ok, new_state :: any}
@callback handle_event(
event :: map,
conn :: Slack.Bot.Conn.t(),
state :: any
) ::
:ok
| {:ok, new_state :: any}
@doc @moduledoc
defmacro __using__(opts \\ []) do
complete_opts =
if otp = opts[:otp_app] do
quote do
Keyword.merge(
unquote(opts),
Application.get_env(unquote(otp), __MODULE__, [])
)
end
else
quote do
unquote(opts)
end
end
quote do
@behaviour unquote(__MODULE__)
@doc unquote(@moduledoc)
@spec child_spec(Keyword.t()) :: tuple
def child_spec(opts \\ []) do
opts = Keyword.merge(unquote(complete_opts), opts)
%{
id: opts[:name] || __MODULE__,
start: {unquote(__MODULE__), :start_link, [[{:client, __MODULE__} | opts]]}
}
end
@doc unquote(@moduledoc)
@spec start_link(Keyword.t()) :: tuple
def start_link(opts \\ []) do
opts = Keyword.merge(unquote(complete_opts), opts)
unquote(__MODULE__).start_link([{:client, __MODULE__} | opts])
end
@impl unquote(__MODULE__)
def init(state), do: {:ok, state}
@impl unquote(__MODULE__)
def handle_info(_msg, _conn, state), do: {:ok, state}
defoverridable init: 1,
handle_info: 3
# Fix defoverridable spec
@spec handle_info(
message :: term,
conn :: Slack.Bot.Conn.t(),
state :: any
) ::
:ok
| {:ok, new_state :: any}
end
end
### Bot Implementation ###
alias __MODULE__.{Conn, Credentials}
require Logger
def start_link(opts \\ []) do
token = opts[:token]
with client when is_atom(client) <- opts[:client] || {:error, :missing_client},
{:ok, cred} <- connect(token),
{:ok, client_state} <- client.init(opts[:state]) do
Logger.debug(fn -> "Slacks: Connecting as #{cred.self.name}." end)
:websocket_client.start_link(String.to_charlist(cred.url), __MODULE__, %Conn{
token: token,
self: cred.self,
client: client,
client_state: client_state
})
end
end
@doc ~S"""
Connect to the API rtm with the given token.
Returns bot credentials.
"""
@spec connect(String.t()) :: {:ok, Credentials.t()} | {:error, atom}
def connect(token) do
url =
"https://slack.com/api/rtm.connect?token=#{token}&batch_presence_aware=true&presence_sub=true"
case HTTPX.get(url, format: :json_atoms) do
{:ok, %{body: token = %{ok: true}}} -> Credentials.parse(token)
{:ok, %{body: %{ok: false}}} -> {:error, :invalid_token}
error -> error
end
end
@behaviour :websocket_client
@impl :websocket_client
def init(state), do: {:reconnect, state}
@impl :websocket_client
def onconnect(_websocket_request, state) do
{:ok, state}
end
@impl :websocket_client
def websocket_handle({:ping, ping}, _conn, state) do
{:reply, {:pong, ping}, state}
end
def websocket_handle({:text, ~S({"type": "hello"})}, _conn, state = %{self: self}) do
Logger.info(fn -> "Slacks: #{self.name} connected." end)
{:ok, state}
end
def websocket_handle({:text, data}, _conn, state) do
case Jason.decode(data, keys: :atoms) do
{:ok, event = %{type: _type}} ->
Logger.debug(fn -> "Slacks: Event: #{inspect(event)}" end)
case state.client.handle_event(event, state, state.client_state) do
:ok -> {:ok, state}
{:ok, new_state} -> {:ok, %{state | client_state: new_state}}
end
{:ok, data} ->
Logger.warn(fn -> "Slacks: Unhandled payload: #{inspect(data)}" end)
{:ok, state}
{:error, error} ->
Logger.warn(fn -> "Slacks: Error parsing message: #{inspect(error)}" end)
{:ok, state}
end
end
def websocket_handle(msg, _conn, state) do
Logger.warn(fn -> "Slacks: Unhandled message: #{inspect(msg)}" end)
{:ok, state}
end
@impl :websocket_client
def ondisconnect(_, state = %{client: client}) do
Logger.warn(fn -> "Slacks: #{client} disconnected, reconnecting..." end)
{:reconnect, state}
end
@impl :websocket_client
def websocket_info(message, _conn, state) do
case state.client.handle_info(message, state, state.client_state) do
:ok -> {:ok, state}
{:ok, new_state} -> {:ok, %{state | client_state: new_state}}
end
end
@impl :websocket_client
def websocket_terminate(reason, _conn, %{client: client}) do
Logger.debug(fn -> "Slacks: Terminating #{client} (#{reason})" end)
:ok
end
end