Skip to content

Commit

Permalink
Merge branch 'acc-fix-rtm-connect' of github.com:acconrad/Elixir-Slac…
Browse files Browse the repository at this point in the history
…k into acc-fix-rtm-connect
  • Loading branch information
acconrad committed Apr 16, 2019
2 parents 6435955 + afce9c2 commit 1c2d347
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/doc
erl_crash.dump
*.ez

.idea
*.iml
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ Status](https://api.travis-ci.org/BlakeWilliams/Elixir-Slack.svg?branch=master)]
# Elixir-Slack

This is a Slack [Real Time Messaging API] client for Elixir. You'll need a
Slack API token which can be retrieved from the [Web API page] or by creating a
Slack API token which can be retrieved by following the [Token Generation
Instructions] or by creating a
new [bot integration].

[Real time Messaging API]: https://api.slack.com/rtm
[Web API page]: https://api.slack.com/web
[Token Generation Instructions]: https://hexdocs.pm/slack/token_generation_instructions.html
[bot integration]: https://my.slack.com/services/new/bot

## Installing
Expand Down Expand Up @@ -153,7 +154,7 @@ and body passed as arguments.
In the case where you only need to control the options passed to HTTPoison/hackney, the default client accepts
a keyword list as an additional configuration parameter. Note that this is ignored if configuring a custom client.

See [HTTPoison docs](https://hexdocs.pm/httpoison/HTTPoison.html#request/5) for a list of avilable options.
See [HTTPoison docs](https://hexdocs.pm/httpoison/HTTPoison.html#request/5) for a list of available options.

```elixir
config :slack, :web_http_client_opts, [timeout: 10_000, recv_timeout: 10_000]
Expand Down
Binary file added guides/assets/access-token1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added guides/assets/access-token2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions guides/token_generation_instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Token Generation Instructions

1. Visit https://api.slack.com/apps
2. If you have already created your app then click on it, otherwise create a new app.
3. Click on "Add features and functionality" to expand that section
4. Click on "Permissions"
![Step 4](./assets/access-token1.png "Step 4 Screenshot")
5. Copy the "OAuth Access Token"
![Step 5](./assets/access-token2.png "Step 5 Screenshot")

Note: The Elixir Slack library only uses the "OAuth Access Token" or "Bot User OAuth Access Token", it does not use the "Client ID", "Client Secret", "Signing Secret", or "Verification Token"
8 changes: 4 additions & 4 deletions lib/slack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ defmodule Slack do
Slack is a genserver-ish interface for working with the Slack real time
messaging API through a Websocket connection.
To use this module you'll need a valid Slack API token. You can find your
personal token on the [Slack Web API] page, or you can add a new
[bot integration].
To use this module you'll need a need a Slack API token which can be retrieved
by following the [Token Generation Instructions] or by creating a new [bot
integration].
[Slack Web API]: https://api.slack.com/web
[Token Generation Instructions]: https://hexdocs.pm/slack/token_generation_instructions.html
[bot integration]: https://api.slack.com/bot-users
## Example
Expand Down
31 changes: 29 additions & 2 deletions lib/slack/lookups.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
defmodule Slack.Lookups do
@moduledoc "Utility functions for looking up slack state informatin"
require Logger

@username_warning """
Referencing "@USER_NAME" is deprecated, and should not be used.
For more information see https://api.slack.com/changelog/2017-09-the-one-about-usernames
"""

@moduledoc "Utility functions for looking up slack state information"

@doc ~S"""
Turns a string like `"@USER_NAME"` into the ID that Slack understands (`"U…"`).
NOTE: Referencing `"@USER_NAME"` is deprecated, and should not be used.
For more information see https://api.slack.com/changelog/2017-09-the-one-about-usernames
"""
def lookup_user_id("@" <> user_name, slack) do
Logger.warn(@username_warning)
slack.users
|> Map.values()
|> Enum.find(%{}, fn user ->
Expand All @@ -17,6 +28,9 @@ defmodule Slack.Lookups do
Turns a string like `"@USER_NAME"` or a user ID (`"U…"`) into the ID for the
direct message channel of that user (`"D…"`). `nil` is returned if a direct
message channel has not yet been opened.
NOTE: Referencing `"@USER_NAME"` is deprecated, and should not be used.
For more information see https://api.slack.com/changelog/2017-09-the-one-about-usernames
"""
def lookup_direct_message_id(user = "@" <> _user_name, slack) do
user
Expand Down Expand Up @@ -47,16 +61,24 @@ defmodule Slack.Lookups do
@doc ~S"""
Turns a Slack user ID (`"U…"`), direct message ID (`"D…"`), or bot ID (`"B…"`)
into a string in the format "@USER_NAME".
NOTE: Referencing `"@USER_NAME"` is deprecated, and should not be used.
For more information see https://api.slack.com/changelog/2017-09-the-one-about-usernames
"""
def lookup_user_name(direct_message_id = "D" <> _id, slack) do
lookup_user_name(slack.ims[direct_message_id].user, slack)
end

def lookup_user_name(user_id = "U" <> _id, slack) do
"@" <> slack.users[user_id].name
find_username_by_id(user_id, slack)
end

def lookup_user_name(user_id = "W" <> _id, slack) do
find_username_by_id(user_id, slack)
end

def lookup_user_name(bot_id = "B" <> _id, slack) do
Logger.warn(@username_warning)
"@" <> slack.bots[bot_id].name
end

Expand All @@ -77,4 +99,9 @@ defmodule Slack.Lookups do
defp find_channel_by_name(nested_map, name) do
Enum.find_value(nested_map, fn {_id, map} -> if map.name == name, do: map, else: nil end)
end

defp find_username_by_id(user_id, slack) do
Logger.warn(@username_warning)
"@" <> slack.users[user_id].name
end
end
39 changes: 25 additions & 14 deletions lib/slack/sends.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ defmodule Slack.Sends do
Sends `text` to `channel` for the given `slack` connection. `channel` can be
a string in the format of `"#CHANNEL_NAME"`, `"@USER_NAME"`, or any ID that
Slack understands.
NOTE: Referencing `"@USER_NAME"` is deprecated, and should not be used.
For more information see https://api.slack.com/changelog/2017-09-the-one-about-usernames
"""
def send_message(text, channel = "#" <> channel_name, slack) do
channel_id = Lookups.lookup_channel_id(channel, slack)
Expand All @@ -18,23 +21,16 @@ defmodule Slack.Sends do
end

def send_message(text, user_id = "U" <> _user_id, slack) do
user_name = Slack.Lookups.lookup_user_name(user_id, slack)
send_message(text, user_name, slack)
send_message_to_user(text, user_id, slack)
end

def send_message(text, user = "@" <> _user_name, slack) do
direct_message_id = Lookups.lookup_direct_message_id(user, slack)
def send_message(text, user_id = "W" <> _user_id, slack) do
send_message_to_user(text, user_id, slack)
end

if direct_message_id do
send_message(text, direct_message_id, slack)
else
open_im_channel(
slack.token,
Lookups.lookup_user_id(user, slack),
fn id -> send_message(text, id, slack) end,
fn reason -> reason end
)
end
def send_message(text, user = "@" <> _user_name, slack) do
user_id = Lookups.lookup_user_id(user, slack)
send_message(text, user_id, slack)
end

def send_message(text, channel, slack) do
Expand Down Expand Up @@ -90,6 +86,21 @@ defmodule Slack.Sends 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")

Expand Down
22 changes: 22 additions & 0 deletions lib/slack/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ defmodule Slack.State do
update_in(slack, [:groups, channel, :members], &Enum.uniq([user | &1]))
end

def update(
%{type: "message", subtype: "channel_topic", channel: channel, user: user, topic: topic},
slack
) do
put_in(slack, [:channels, channel, :topic], %{
creator: user,
last_set: System.system_time(:seconds),
value: topic
})
end

def update(
%{type: "message", subtype: "group_topic", channel: channel, user: user, topic: topic},
slack
) do
put_in(slack, [:groups, channel, :topic], %{
creator: user,
last_set: System.system_time(:seconds),
value: topic
})
end

def update(%{type: "channel_left", channel: channel_id}, slack) do
put_in(slack, [:channels, channel_id, :is_member], false)
end
Expand Down
21 changes: 21 additions & 0 deletions lib/slack/web/docs/channels.replies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"desc": "Retrieve a thread of messages posted to a channel.",

"args": {
"channel": {
"type" : "channel",
"required" : true,
"desc" : "Channel to fetch thread from"
},

"thread_ts": {
"required" : true,
"desc" : "Unique identifier of a thread's parent message."
}
},

"errors": {
"channel_not_found" : "Value passed for `channel` was invalid.",
"thread_not_found": "Value for thread_ts was missing or invalid."
}
}
48 changes: 48 additions & 0 deletions lib/slack/web/docs/chat.postEphemeral.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"desc": "Sends an ephemeral message to a channel.",

"args": {
"channel": {
"type" : "channel",
"required" : true,
"desc" : "Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See [below](#channels) for more details."
},
"text": {
"required" : true,
"example" : "Hello world",
"desc" : "Text of the message to send. See below for an explanation of [formatting](#formatting)."
},
"user": {
"type" : "user",
"required" : true,
"desc" : "`id` of the user who will receive the ephemeral message. The user should be in the channel specified by the `channel` argument."
},
"parse": {
"example" : "full",
"desc" : "Change how messages are treated. Defaults to `none`. See [below](#formatting)."
},
"link_names": {
"example" : "1",
"desc" : "Find and link channel names and usernames."
},
"attachments": {
"example" : "[{\"pretext\": \"pre-hello\", \"text\": \"text-world\"}]",
"desc" : "Structured message attachments."
},
"as_user": {
"required" : false,
"example" : "true",
"desc" : "Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See [authorship](#authorship) below."
}
},

"errors": {

"channel_not_found" : "Value passed for `channel` was invalid.",
"user_not_in_channel" : "Cannot post user messages to a channel they are not in.",
"is_archived" : "Channel has been archived.",
"msg_too_long" : "Message text is too long",
"no_text" : "No message text provided",
"rate_limited" : "Application has posted too many messages, [read the Rate Limit documentation](/docs/rate-limits) for more information"
}
}
21 changes: 21 additions & 0 deletions lib/slack/web/docs/groups.replies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"desc": "Retrieve a thread of messages posted to a private channel.",

"args": {
"channel": {
"type" : "channel",
"required" : true,
"desc" : "Private channel to fetch thread from"
},

"thread_ts": {
"required" : true,
"desc" : "Unique identifier of a thread's parent message."
}
},

"errors": {
"channel_not_found" : "Value passed for `channel` was invalid.",
"thread_not_found": "Value for thread_ts was missing or invalid."
}
}
21 changes: 21 additions & 0 deletions lib/slack/web/docs/im.replies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"desc": "Retrieve a thread of messages posted to a direct message conversation.",

"args": {
"channel": {
"type" : "channel",
"required" : true,
"desc" : "Direct message channel to fetch thread from"
},

"thread_ts": {
"required" : true,
"desc" : "Unique identifier of a thread's parent message."
}
},

"errors": {
"channel_not_found" : "Value passed for `channel` was invalid.",
"thread_not_found": "Value for thread_ts was missing or invalid."
}
}
16 changes: 16 additions & 0 deletions lib/slack/web/docs/users.lookupByEmail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"desc": "Retrieve a single user by looking them up by their registered email address. Requires `users:read.email`.",

"args": {
"email": {
"type" : "string",
"required" : true,
"desc" : "User's email address"
}
},

"errors": {
"user_not_found" : "Value passed for `user` was invalid.",
"missing_scope" : "The token used is not granted the specific scope permissions required to complete this request. Need: `users:read.email`."
}
}
7 changes: 6 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ defmodule Slack.Mixfile do
end

def docs do
[{:main, Slack}]
[
{:main, Slack},
{:assets, "guides/assets"},
{:extra_section, "GUIDES"},
{:extras, ["guides/token_generation_instructions.md"]}
]
end

defp package do
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
Loading

0 comments on commit 1c2d347

Please sign in to comment.