This is for creating Slack applications or bots in Elixir.
The existing libraries I was looking at use the deprecated RTM API, and no longer work with new apps or bots.
To listen for subscribed events, it uses Socket Mode to connect to Slack. It has some pros and cons, so please read up on it (and pay attention to the info blocks).
It's a relatively thin wrapper, which keeps it flexible and easy to maintain.
- connects to Slack using a websocket connection to listen for your event subscriptions.
- uses the Web API to send messages, etc.
- uses dynamically supervised gen servers to handle each channel's message rate-limiting with a message queue per channel.
Add slack_elixir to your list of dependencies in mix.exs:
def deps do
[
{:slack_elixir, "~> 1.3.0"}
]
endThe easiest way to get started is to create a new Slack app and use the following App Manifest to configure all the permissions that are needed:
display_information:
name: YOURBOTNAME
features:
bot_user:
display_name: YOURBOTNAME
always_online: true
oauth_config:
scopes:
bot:
- channels:history
- channels:read
- groups:read
- im:read
- mpim:read
- chat:write
settings:
event_subscriptions:
bot_events:
- message.channels
- member_joined_channel
- channel_left
interactivity:
# Deliver button actions, modal events, and shortcuts through Socket Mode.
# Shortcuts also need to be declared under `features.shortcuts`, and need
# the `commands` bot scope.
is_enabled: true
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: falseYou will need to:
- Create a Slack app for your workspace
- Add permissions (scopes)
- Connect it to your workspace
- Get an OAuth Bot Token (will have the scopes you defined)
- Enable Socket Mode
- Get an app-level token with
connections:writescope - Add Event Subscriptions
- Enable Interactivity for buttons, modals, and shortcuts (shortcuts also
have to be created there, and need the
commandsscope)
See below for some minimum required scopes and event subscriptions. You will need to add more scopes and subscriptions depending on what you want to do.
channels:historychannels:readgroups:readim:readmpim:read
message.channelsmember_joined_channelchannel_left
Write the Bot module:
defmodule MyApp.Slackbot do
use Slack.Bot
require Logger
@impl true
# A silly example of old-school style bot commands.
def handle_event("message", %{"text" => "!" <> cmd, "channel" => channel, "user" => user}, _bot) do
case cmd do
"roll" ->
send_message(channel, "<@#{user}> rolled a #{Enum.random(1..6)}")
"echo " <> text ->
send_message(channel, text)
_ ->
send_message(channel, "Unknown command: #{cmd}")
end
end
def handle_event("message", %{"channel" => channel, "text" => text, "user" => user}, _bot) do
if String.match?(text, ~r/hello/i) do
send_message(channel, "Hello! <@#{user}>")
end
end
def handle_event(type, payload, _bot) do
Logger.debug("Unhandled #{type} event: #{inspect(payload)}")
:ok
end
@impl true
def handle_interaction("block_actions", %{"actions" => actions}, _bot) do
Enum.each(actions, fn action ->
Logger.info("Received action: #{action["action_id"]}")
end)
end
def handle_interaction("view_submission", %{"view" => %{"callback_id" => "signup"} = view}, _bot) do
if get_in(view, ["state", "values", "name", "name_input", "value"]) in [nil, ""] do
{:reply, %{response_action: "errors", errors: %{name: "Please enter a name"}}}
else
:ok
end
end
def handle_interaction(_type, _payload, _bot), do: :ok
endInteractions, such as button clicks, modal submissions and shortcuts, go to the
optional handle_interaction/3 callback with the payload's "type" (for example
"block_actions", "view_submission", "block_suggestion", "shortcut" or
"message_action") and the full interaction payload. Every interaction type is
passed on. In these payloads "user" is a map, not an ID, and so is "channel"
when there is one (modal submissions and global shortcuts have none). The default
handle_interaction/3 from use Slack.Bot acknowledges interactions and ignores
them, so defining your own replaces it: include a catch-all clause.
Most interactions are acknowledged right away and the return value is ignored.
When Slack accepts a response, as for "view_submission" and "block_suggestion",
the acknowledgment waits for handle_interaction/3 for up to 2.5 seconds after the
interaction arrived. Return {:reply, response} to send a response with it, such as
modal validation errors or the options for an external select menu. If the callback
raises or takes longer, the interaction isn't acknowledged and Slack shows the user
an error. A slow callback is left to finish, but its reply is dropped, so do slow
work in a separate process.
Then you start the Slack Supervisor in your application's supervision tree.
For example:
def start(_type, _args) do
children = [
# ...
{Slack.Supervisor, Application.fetch_env!(:my_app, MyApp.SlackBot)}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end# config/runtime.exs
config :my_app, MyApp.SlackBot,
app_token: "MY_SLACK_APP_TOKEN",
bot_token: "MY_SLACK_BOT_TOKEN",
bot: MyApp.SlackBot,
# Add this if you want to customize the channel types to join.
# By default we join all channel types: public_channel, private_channel, im, mpim.
channels: [
types: ["public_channel", "im", "private_channel"]
]The Socket Mode connection verifies Slack's certificate and hostname. It trusts
your operating system's CA certificates (Erlang/OTP 25+). If those can't be
loaded, for example on Alpine without ca-certificates or on OTP 24, it falls
back to CAStore when that is in your
dependencies.
To change this, set ssl_options in your bot config. They are
:ssl client options,
merged over the defaults. Setting cacerts or cacertfile replaces the default
CA certificates, for example to trust your own CA behind a TLS-inspecting proxy:
config :my_app, MyApp.SlackBot,
# ...
ssl_options: [cacertfile: "/path/to/ca.pem"]PRs welcome!
- Socket Mode for events
- Web API POST requests
- Web API GET requests
- Message Server per channel (rate-limited to 1 message per second per channel).