Packages

A Slack Web & Real Time Messaging API client.

Current section

Files

Jump to
slack_kit lib slack state.ex
Raw

lib/slack/state.ex

defmodule Slack.State do
@moduledoc """
Workspace snapshot maintained for a running `Slack.Bot`.
Each callback in a module that does `use Slack` receives a `%Slack.State{}`
as its `slack` argument. The struct starts from the payload Slack returns on
`rtm.start` and is rolled forward by `update/2` as each RTM event arrives,
so the maps you read inside a callback always reflect the latest known
state of the workspace.
## Fields
| Field | Description |
| ---------- | ---------------------------------------------------------------------------- |
| `:me` | The bot's own identity (id, name, profile). |
| `:team` | The workspace's identity (id, name, domain). |
| `:bots` | Map of `bot_id => bot` for bots known to the workspace. |
| `:channels`| Map of `channel_id => channel` for public channels. |
| `:groups` | Map of `channel_id => channel` for private channels. |
| `:users` | Map of `user_id => user`. Includes `:presence` as it's reported. |
| `:ims` | Map of `channel_id => im` for open direct-message channels. |
| `:process` | PID of the `Slack.Bot` GenServer driving the WebSocket. |
| `:client` | Module implementing the WebSocket transport — used by `Slack.Sends`. |
| `:token` | The bot's Slack API token. |
All entity maps use Slack's string IDs as keys. The values are atom-keyed
maps mirroring the [Slack API types](https://api.slack.com/types) — for
example `slack.users["U123"].profile.display_name`.
## Access
`Slack.State` implements the `Access` behaviour and is keyed like a map, so
the standard `get_in`/`put_in`/`update_in` helpers work directly against it:
get_in(slack, [:channels, "C123", :name])
put_in(slack, [:users, "U123", :presence], "away")
## Updating from RTM events
`update/2` pattern-matches on `event.type` and folds the event into the
state. Supporting a new RTM event type means adding another `update/2`
clause here. Unrecognised events fall through to a catch-all and leave the
state untouched.
"""
@behaviour Access
def fetch(client, key)
defdelegate fetch(client, key), to: Map
def get(client, key, default)
defdelegate get(client, key, default), to: Map
def get_and_update(client, key, function)
defdelegate get_and_update(client, key, function), to: Map
def pop(client, key)
defdelegate pop(client, key), to: Map
defstruct [
:process,
:client,
:token,
:me,
:team,
bots: %{},
channels: %{},
groups: %{},
users: %{},
ims: %{}
]
@type t :: %__MODULE__{
process: pid() | nil,
client: module() | nil,
token: String.t() | nil,
me: map() | nil,
team: map() | nil,
bots: map(),
channels: map(),
groups: map(),
users: map(),
ims: map()
}
defp safe_map_getter(key) do
Access.key(key, %{})
end
defp safe_list_getter(key) do
Access.key(key, [])
end
@doc """
Folds a single RTM event into `slack`, returning the updated state.
Recognised event types include `channel_created`, `channel_joined`,
`channel_left`, `channel_rename`, `channel_archive`, `channel_unarchive`,
their `group_*` equivalents, `team_rename`, `team_join`, `user_change`,
`presence_change`, `bot_added`, `bot_changed`, `im_created`, and the
`message` subtypes that signal topic/join/leave changes. Any other event is
returned unchanged.
"""
@spec update(map(), t()) :: t()
def update(%{type: "channel_created", channel: channel}, slack) do
put_in(slack, [:channels, channel.id], channel)
end
def update(%{type: "channel_joined", channel: channel}, slack) do
slack
|> put_in([:channels, channel.id], channel)
|> put_in([:channels, channel.id, :is_member], true)
end
def update(%{type: "group_joined", channel: channel}, slack) do
put_in(slack, [:groups, channel.id], channel)
end
def update(%{type: "channel_left", channel: channel_id}, slack) do
put_in(slack, [:channels, channel_id, :is_member], false)
end
def update(%{type: "group_left", channel: channel}, slack) do
update_in(slack, [:groups], &Map.delete(&1, channel))
end
Enum.map(["channel", "group"], fn type ->
plural_atom = String.to_atom(type <> "s")
def update(%{type: unquote(type <> "_rename"), channel: channel}, slack) do
put_in(slack, [unquote(plural_atom), safe_map_getter(channel.id), :name], channel.name)
end
def update(%{type: unquote(type <> "_archive"), channel: channel}, slack) do
put_in(slack, [unquote(plural_atom), safe_map_getter(channel), :is_archived], true)
end
def update(%{type: unquote(type <> "_unarchive"), channel: channel}, slack) do
put_in(slack, [unquote(plural_atom), safe_map_getter(channel), :is_archived], false)
end
def update(
%{
type: "message",
subtype: unquote(type <> "_topic"),
channel: channel,
user: user,
topic: topic
},
slack
) do
put_in(slack, [unquote(plural_atom), safe_map_getter(channel), :topic], %{
creator: user,
last_set: System.system_time(:second),
value: topic
})
end
def update(
%{type: "message", subtype: unquote(type <> "_join"), channel: channel, user: user},
slack
) do
update_in(
slack,
[unquote(plural_atom), safe_map_getter(channel), safe_list_getter(:members)],
&Enum.uniq([user | &1])
)
end
def update(
%{type: "message", subtype: unquote(type <> "_leave"), channel: channel, user: user},
slack
) do
update_in(
slack,
[unquote(plural_atom), safe_map_getter(channel), safe_list_getter(:members)],
&(&1 -- [user])
)
end
end)
def update(%{type: "team_rename", name: name}, slack) do
put_in(slack, [:team, :name], name)
end
def update(%{type: "presence_change", user: user, presence: presence}, slack) do
put_in(slack, [:users, user, :presence], presence)
end
def update(%{type: "presence_change", users: users, presence: presence}, slack) do
Enum.reduce(users, slack, fn user, acc ->
put_in(acc, [:users, user, :presence], presence)
end)
end
def update(%{type: "team_join", user: user}, slack) do
put_in(slack, [:users, user.id], user)
end
def update(%{type: "user_change", user: user}, slack) do
update_in(slack, [:users, Access.key(user.id, %{})], &Map.merge(&1, user))
end
Enum.map(["bot_added", "bot_changed"], fn type ->
def update(%{type: unquote(type), bot: bot}, slack) do
put_in(slack, [:bots, bot.id], bot)
end
end)
def update(%{type: "im_created", channel: channel}, slack) do
put_in(slack, [:ims, channel.id], channel)
end
def update(%{type: _type}, slack) do
slack
end
end