Current section
Files
Jump to
Current section
Files
lib/slack/sends.ex
defmodule Slack.Sends do
@moduledoc """
Helpers for writing frames back through an open RTM WebSocket.
These functions are imported into any module that does `use Slack`, so
inside a bot callback you can write `send_message("hi", channel, slack)`
directly. They serialise messages to JSON and cast them onto the WebSocket
process referenced by the `Slack.State` you pass in — so they require an
active RTM connection.
For richer messages (attachments, blocks, files, ephemeral posts), use the
generated Web API modules (`Slack.Web.Chat`, `Slack.Web.Files`, etc.)
instead — `send_message/3` covers only plain text.
"""
alias Slack.Lookups
@type slack :: Slack.State.t() | %{process: pid(), client: module()}
@type channel :: String.t()
@doc """
Sends `text` to `channel` over the RTM WebSocket held by `slack`.
`channel` may be:
* a channel name prefixed with `#` (e.g. `"#general"`) — resolved via
`Slack.Lookups.lookup_channel_id/2`
* a user ID starting with `U` or `W` — the message is sent to the open
DM channel for that user, opening one via `im.open` if necessary
* a user reference starting with `@` (deprecated; see
[Slack's changelog](https://api.slack.com/changelog/2017-09-the-one-about-usernames))
* any channel ID that Slack accepts directly (`"C…"`, `"G…"`, `"D…"`)
Raises `ArgumentError` if `#CHANNEL_NAME` cannot be resolved against the
current state.
"""
@spec send_message(String.t(), channel, slack) :: :ok
def send_message(text, "#" <> channel_name = channel, slack) do
channel_id = Lookups.lookup_channel_id(channel, slack)
if channel_id do
send_message(text, channel_id, slack)
else
raise ArgumentError, "channel ##{channel_name} not found"
end
end
def send_message(text, "U" <> _user_id = user_id, slack) do
send_message_to_user(text, user_id, slack)
end
def send_message(text, "W" <> _user_id = user_id, slack) do
send_message_to_user(text, user_id, slack)
end
def send_message(text, "@" <> _user_name = user, slack) do
user_id = Lookups.lookup_user_id(user, slack)
send_message(text, user_id, slack)
end
def send_message(text, channel, slack) do
%{
type: "message",
text: text,
channel: channel
}
|> JSON.encode!()
|> send_raw(slack)
end
@doc """
Same as `send_message/3`, but threads the message under an existing parent.
`thread` is the `ts` of the message you want to reply to — use the
top-level parent's `ts`, not a nested reply's. RTM threaded replies are
always posted, never broadcast back to the channel; for `reply_broadcast`
support, use the Web API (`Slack.Web.Chat`).
"""
@spec send_message(String.t(), channel, slack, String.t()) :: :ok
def send_message(text, "#" <> channel_name = channel, slack, thread) do
channel_id = Lookups.lookup_channel_id(channel, slack)
if channel_id do
send_message(text, channel_id, slack, thread)
else
raise ArgumentError, "channel ##{channel_name} not found"
end
end
def send_message(text, channel, slack, thread) do
%{
type: "message",
text: text,
channel: channel,
thread_ts: thread
}
|> JSON.encode!()
|> send_raw(slack)
end
@doc """
Notifies Slack that the bot is typing in `channel`.
Slack clients render the "@bot is typing…" indicator for a few seconds
after this is called. There is no acknowledgement.
"""
@spec indicate_typing(channel, slack) :: :ok
def indicate_typing(channel, slack) do
%{
type: "typing",
channel: channel
}
|> JSON.encode!()
|> send_raw(slack)
end
@doc """
Sends a `ping` frame, optionally merging extra fields into the payload.
Slack replies with a matching `pong`. The default `Slack.WebSocketClient`
already issues low-level WebSocket keepalive pings; this is the
application-level RTM ping, useful when you want to round-trip a custom
payload such as a request id.
"""
@spec send_ping(map() | keyword(), slack) :: :ok
def send_ping(data \\ %{}, slack) do
%{
type: "ping"
}
|> Map.merge(Map.new(data))
|> JSON.encode!()
|> send_raw(slack)
end
@doc """
Subscribes to `presence_change` events for `ids`.
Slack's RTM no longer broadcasts presence updates by default — callers
must opt-in per user. Pass a list of user IDs to start receiving events;
pass `[]` to clear the subscription.
"""
@spec subscribe_presence([String.t()], slack) :: :ok
def subscribe_presence(ids \\ [], slack) do
%{
type: "presence_sub",
ids: ids
}
|> JSON.encode!()
|> send_raw(slack)
end
@doc """
Casts an already-encoded JSON string onto the RTM WebSocket.
Escape hatch for RTM event types `Slack.Sends` doesn't wrap directly. The
string must be a complete, well-formed JSON message — Slack will close the
connection on malformed frames.
"""
@spec send_raw(String.t(), slack) :: :ok
def send_raw(json, %{process: pid, client: client}) do
client.cast(pid, {:text, json})
end
defp send_message_to_user(text, user_id, slack) do
direct_message_id = Lookups.lookup_direct_message_id(user_id, slack)
if direct_message_id do
send_message(text, direct_message_id, slack)
else
open_im_channel(
slack.token,
user_id,
fn id -> send_message(text, id, slack) end,
fn reason -> reason end
)
end
end
defp open_im_channel(token, user_id, on_success, on_error) do
url = Application.get_env(:slack, :url, "https://slack.com") <> "/api/im.open"
options = Application.get_env(:slack, :web_http_client_opts, [])
options = Keyword.merge(options, form: [token: token, user: user_id], decode_body: false)
case Req.post(url, options) do
{:ok, response} ->
case response.body |> JSON.decode!() |> Slack.JSON.atomize_keys() do
%{ok: true, channel: %{id: id}} -> on_success.(id)
e = %{error: _error_message} -> on_error.(e)
end
{:error, reason} ->
on_error.(reason)
end
end
end