├── .github
└── CODE_OF_CONDUCT.md
├── Gemfile
├── Gemfile.lock
├── README.md
├── Section-1.md
├── Section-2.md
├── Section-3.md
├── Section-4.md
├── auth.rb
├── bot.rb
├── config.ru
└── welcome.json
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Introduction
4 |
5 | Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand.
6 |
7 | Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic.
8 |
9 | This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members.
10 |
11 | For more information on our code of conduct, please visit [https://slackhq.github.io/code-of-conduct](https://slackhq.github.io/code-of-conduct)
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 | ruby '2.3.1'
3 |
4 | gem 'sinatra', '~> 1.4.7'
5 | gem 'slack-ruby-client', '~> 0.7.7'
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: http://rubygems.org/
3 | specs:
4 | activesupport (5.2.4.3)
5 | concurrent-ruby (~> 1.0, >= 1.0.2)
6 | i18n (>= 0.7, < 2)
7 | minitest (~> 5.1)
8 | tzinfo (~> 1.1)
9 | concurrent-ruby (1.1.6)
10 | faraday (0.9.2)
11 | multipart-post (>= 1.2, < 3)
12 | faraday_middleware (0.10.0)
13 | faraday (>= 0.7.4, < 0.10)
14 | gli (2.14.0)
15 | hashie (3.4.4)
16 | i18n (1.8.2)
17 | concurrent-ruby (~> 1.0)
18 | json (2.0.2)
19 | minitest (5.14.1)
20 | multipart-post (2.0.0)
21 | rack (1.6.12)
22 | rack-protection (1.5.5)
23 | rack
24 | sinatra (1.4.7)
25 | rack (~> 1.5)
26 | rack-protection (~> 1.4)
27 | tilt (>= 1.3, < 3)
28 | slack-ruby-client (0.7.7)
29 | activesupport
30 | faraday
31 | faraday_middleware
32 | gli
33 | hashie
34 | json
35 | websocket-driver
36 | thread_safe (0.3.6)
37 | tilt (2.0.5)
38 | tzinfo (1.2.7)
39 | thread_safe (~> 0.1)
40 | websocket-driver (0.6.4)
41 | websocket-extensions (>= 0.1.0)
42 | websocket-extensions (0.1.2)
43 |
44 | PLATFORMS
45 | ruby
46 |
47 | DEPENDENCIES
48 | sinatra (~> 1.4.7)
49 | slack-ruby-client (~> 0.7.7)
50 |
51 | RUBY VERSION
52 | ruby 2.3.1p112
53 |
54 | BUNDLED WITH
55 | 1.13.1
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ARCHIVED
2 |
3 | This piece of sample code has been archived and is no longer maintained. The approaches used may no longer be relevant, or even work correctly. For more up to date samples, check out some of the other repositories on https://github.com/slackapi
4 |
5 | ### Building an onboarding bot in Ruby using Slack's [Events API](https://api.slack.com/events-api)
6 | This example demonstrates what it takes to build a Slack bot in Ruby. We'll cover the steps
7 | required to create and configure your app, set up event subscriptions, and add event handlers.
8 |
9 | We're going to build an onboarding bot to welcome new users to your team and guide them in interacting with Slack messages.
10 |
11 | Something like this:
12 | >
13 |
14 | Don't worry, you can do this. I believe in you. :star2:
15 |
16 | ---
17 |
18 | ## Technical Requirements
19 | Since this example was written in Ruby, you'll need to have Ruby installed. We're using 2.3, specifically. We're also going to use a few Ruby
20 | gems to help us out, so you'll need those too.
21 |
22 | ### Code / Libraries Required
23 | * [Ruby](https://www.ruby-lang.org/) - the programming language we're going to write the app in
24 | * [Bundler](http://bundler.io/) - the Ruby package manager we'll be using to make sure we have everything we need
25 | * [Sinatra](http://www.sinatrarb.com/) - a fairly lightweight web server for Ruby
26 | * [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client/) - an awesome Ruby Slack client maintained by [dblock](https://github.com/dblock)
27 |
28 | First, make sure you have the correct version of Ruby installed and upgrade or install Ruby if necessary. You can check your Ruby version by running `ruby -v`. If you have a version less than 2.3, you'll want to upgrade. If you need to keep your older version and run multiple versions, you can use [RVM](https://github.com/rvm/rvm) or [rbenv](https://github.com/rbenv/rbenv).
29 |
30 | Once you know you have the correct Ruby version, install Bundler by running `gem install bundler`.
31 |
32 | After installing Bundler, you can use Bundler to install the required gems from our [Gemfile](./Gemfile) by running `bundle install`.
33 |
34 | You should see some output similar to this:
35 | ```bash
36 | Using sinatra 1.4.7
37 | Using slack-ruby-client 0.7.7
38 | Bundle complete! 2 Gemfile dependencies, 20 gems now installed.
39 | Use `bundle show [gemname]` to see where a bundled gem is installed.
40 | ```
41 | There will probably be more gems listed, as each of the gems we're using have their own dependencies. As long as it ends with `Bundle complete!` and not an error, you should be good.
42 |
43 | ### Server Requirements
44 | Since Slack will be delivering events to you, you'll need a server which is capable of receiving incoming HTTPS traffic from Slack.
45 |
46 | When running this project locally, you'll need to set up tunnels so that Slack can post requests to your endpoints. I recommend a
47 | tool called [ngrok](https://ngrok.com/). It's easy to use and supports HTTPS, which is required by Slack.
48 |
49 | To test these events coming to your server without going through the actions in a Slack app, you can use something like
50 | [Postman](https://www.getpostman.com/) to recreate the requests sent from Slack to your server. This is especially helpful
51 | for events like user join, where the workflow to recreate the event is rather complex.
52 |
53 | ## Suggested Reading Material
54 |
55 | ### Explore the source code files
56 | Much of the documentation for this project will be found in the source code itself, so you'll want to read through them. I recommend going through them in this order:
57 |
58 | 1. **[config.ru](./config.ru)** is the rack config file. This tells the server what objects (Auth and Bot) to create and run.
59 | 2. **[auth.rb](./auth.rb)** is where we'll handle the OAuth authentication flow. In order to access a team's events and data, we need to ask the
60 | user for permission to install the app on their Team and to grant our bot user access.
61 | 3. **[bot.rb](./bot.rb)** is where all of the event logic lives, where you'll find the handlers to actually process and respond to
62 | events.
63 | 4. **[welcome.json](./welcome.json)** This is a JSON file of message attachments used to create the onboarding welcome message our bot will send to new users.
64 |
65 | ### Documentation
66 | * [Getting started with Slack apps](https://api.slack.com/slack-apps)
67 | * [Slack Events API documentation](https://api.slack.com/events)
68 | * [Slack Web API documentation](https://api.slack.com/web)
69 |
70 | ## Let's get started :tada:
71 | **[Section 1: Getting Started](Section-1.md)** :point_left:
72 | [Section 2: App Configuration](Section-2.md)
73 | [Section 3: Event Handlers](Section-3.md)
74 | [Section 4: Running and Installing Your App](Section-4.md)
75 |
76 | ## Where to Find Help
77 | Wondering what to do if you can't get this dang tutorial to work for you? The Slack Developer community is an awesome place to get help when
78 | you're confused or stuck. We have an excellent 'search first' culture and Slack is committed to improving our tutorials and documentation
79 | based on your feedback. If you've checked the [Slack API documentation](https://api.slack.com/), reached the end of your google patience and
80 | found [StackOverflow](http://stackoverflow.com/questions/tagged/slack-api) to be unhelpful, try asking for help in the
81 | [Dev4Slack](http://dev4slack.xoxco.com/) Slack team.
82 |
83 | ---
84 |
85 | ## Feedback
86 | I'd love to hear your feedback on this example. You can file an issue, submit a PR or message me on GitHub ([roach](https://github.com/roach)) or Twitter ([@roach](https://twitter.com/roach)).
87 |
--------------------------------------------------------------------------------
/Section-1.md:
--------------------------------------------------------------------------------
1 |
2 | ## Section 1: Getting Started
3 | **Previous [README](README.md)**
4 |
5 | ### Create a New App on [api.slack.com](https://api.slack.com/apps)
6 | Visit https://api.slack.com/apps and click the `Create New App` button, fill out the name, team, etc and create the app. Don't worry
7 | about the credentials for now, we'll be revisiting this page later.
8 | > 
9 |
10 | ### Create A Tunnel Using ngrok
11 | This may seem like a weird thing to begin with, but you're going to need to know your ngrok URL as we go through the setup steps.
12 |
13 | Start ngrok so you'll have the ngrok.io URL for use in your app configuration. Ngrok will allow your server to be publicly accessible.
14 |
15 | In your terminal, run `ngrok http 9292` (_Note: 9292 is Sinatra's default HTTP port, be sure to update this command if you change it_)
16 |
17 | Once running, ngrok will output your server's URLs:
18 |
19 | ```bash
20 | ngrok by @inconshreveable (Ctrl+C to quit)
21 |
22 | Session status online
23 | Version 2.1.18
24 | Region United States (us)
25 | Web Interface http://127.0.0.1:4040
26 |
27 | Forwarding http://h7465j.ngrok.io -> localhost:9292
28 | Forwarding https://h7465j.ngrok.io -> localhost:9292
29 | ```
30 | You want the HTTPS URL listed by ngrok. (ex. https://h7465j.ngrok.io)
31 |
32 | The ngrok Web Interface provides stats about your server and allows you to inspect incoming request payloads: http://127.0.0.1:4040.
33 |
34 | ---
35 | **Next [Section 2: App Configuration](Section-2.md)**
36 |
--------------------------------------------------------------------------------
/Section-2.md:
--------------------------------------------------------------------------------
1 | ## Section 2: App Configuration
2 | **Previous [Section 1: Getting Started](Section-1.md)**
3 |
4 | ### OAuth & Permissions
5 | Slack uses OAuth for user authentication. This auth process is performed by exchanging a set of keys and tokens between Slack's servers and yours. This process allows the authorizing user to confirm that they want to grant our bot access to their team.
6 |
7 | To keep this tutorial simple, I've already built out the OAuth flow for you. The code can be found in **[auth.rb](./auth.rb)**. You can read up on Slack's OAuth flow here: https://api.slack.com/docs/oauth.
8 |
9 | >
10 |
11 | The final step of this process is redirecting the user back to your completion page. This URL is called the **Redirect URL**. Add your OAuth redirect URL to your app's **OAuth & Permissions** page. This URL will be your ngrok URL with the `/finish_auth` endpoint (e.g. https://h7465j.ngrok.io/finish_auth).
12 |
13 | >
14 |
15 | ### Add a Bot User
16 | You'll need to add a **bot user** so that your app can interact with users. Go to your app's settings and locate the **Bot Users** section to add your bot user.
17 |
18 | >
19 |
20 | ### Event Subscriptions
21 | First, Enable events:
22 | >
23 |
24 | Then, subscribe to these **Bot Events**:
25 | * [message.im](https://api.slack.com/events/message.im)
26 | * [pin_added](https://api.slack.com/events/pin_added)
27 | * [reaction_added](https://api.slack.com/events/reaction_added)
28 | * [team_join](https://api.slack.com/events/team_join)
29 |
30 | Your bot's Event Subscriptions should look like this:
31 | >
32 |
33 | Remember to save your changes.
34 |
35 | ---
36 | **Next [Section 3: Event Handlers](Section-3.md)**
37 |
--------------------------------------------------------------------------------
/Section-3.md:
--------------------------------------------------------------------------------
1 | ## Section 3: Event Handlers
2 | **Previous [Section 2: App Configuration](Section-2.md)**
3 |
4 | ### Add a URL Verification event handler
5 | On the **Event Subscription** page of your app's settings, you'll see **Request URL**. This is the URL Slack is going to send event data to. In
6 | this example, we're using `/events`. When you enter the URL, Slack is going to make a request to your server to verify it's existence. To do this, Slack will be
7 | sending a `challenge` token.
8 |
9 | >
10 |
11 | >
12 |
13 | The payload of that request will look similar to this:
14 | ```json
15 | {
16 | "token": "abcdefghijklmnopqrstuvwxyz",
17 | "challenge": "abcdefghijklmnopqrstuvwxyz1234567890",
18 | "type": "url_verification"
19 | }
20 | ```
21 |
22 | Our app will need to respond to this request by echoing back the challenge string provided by Slack. We'll need a server object, in this case we're
23 | using Sinatra. Inside of the server class (API), we need a listener for POST requests to our events endpoint, `/events`. We'll need to extract
24 | the `url_verification` challenge token from the request payload. All of these steps together form a complete listener for the `url_verification`
25 | event:
26 |
27 | ```ruby
28 | class API < Sinatra::Base
29 | # This is the endpoint Slack will send event data to
30 | post '/events' do
31 | # Grab the body of the request and parse it as JSON
32 | request_data = JSON.parse(request.body.read)
33 | # The request contains a `type` attribute
34 | # which can be one of many things, in this case,
35 | # we only care about `url_verification` events.
36 | case request_data['type']
37 | when 'url_verification'
38 | # When we receive a `url_verification` event, we need to
39 | # return the same `challenge` value sent to us from Slack
40 | # to confirm our server's authenticity.
41 | request_data['challenge']
42 | end
43 | end
44 | end
45 | ```
46 | More info: https://api.slack.com/events/url_verification
47 |
48 | ### Add the other event handlers
49 | This bot will be listening for 4 events: messages, pins, reactions and joins. Each of these events contains a
50 | different payload object, so there will be some specific logic for each event.
51 |
52 | Our event handlers are within the `Events` class in [bot.rb](./bot.rb#L92) (line 92). This class handles all of the event processing logic so
53 | that it doesn't clutter up the API class. We'll need to reference the event-specific methods inside the event class when an event is received.
54 | We'll add these to the switch statement we added to the API class earlier.
55 |
56 | This bot will take action on `team_join`, `reaction_added` and `pin_added` and `message` events.
57 |
58 | When a user joins a team, sends a message, adds or removed a reaction emoji, Slack will post a request to our `/events` endpoint. The JSON payload for the `user_join` event looks like this:
59 |
60 | ```json
61 | {
62 | "token": "v3rific4ti0nt0k3n",
63 | "team_id": "T2UT3AM",
64 | "api_app_id": "12345678.12345678",
65 | "event": {
66 | "type": "team_join",
67 | "user": {
68 | "id": "U2XU53R",
69 | "team_id": "T2UT3AM",
70 | "name": "@sally",
71 | "deleted": false,
72 | "status": null,
73 | "real_name": "Sally Slackuser",
74 | "profile": {
75 | "first_name": "Sally",
76 | "last_name": "Slackuser",
77 | "real_name": "Sally Slackuser",
78 | "real_name_normalized": "Sally Slackuser"
79 | },
80 | "is_bot": false
81 | }
82 | },
83 | "type": "event_callback",
84 | "authed_users": ["U2XU53R"]
85 | }
86 | ```
87 |
88 | This event data structure varies a little between events, but it's basically split into `request` data and `event` data. Request data contains things like the
89 | app ID, verification token, etc. Event data is a subset of the request data, under `event` and contains attributes specific to each event.
90 |
91 | From that JSON payload, We'll need to grab a few parameters in order to welcome the user and send them the tutorial message.
92 |
93 | Here's a simplified example from [bot.rb](./bot.rb#L68) showing just the `team_join` event:
94 | ```ruby
95 | # This class contains all of the web server logic for processing incoming requests from Slack.
96 | class API < Sinatra::Base
97 | # This is the endpoint Slack will post event data to.
98 | post '/events' do
99 | # Extract the event payload from the request and parse the JSON
100 | request_data = JSON.parse(request.body.read)
101 |
102 | case request_data['type']
103 | when 'event_callback'
104 | # Get the Team ID and event data from the request object
105 | team_id = request_data['team_id']
106 | event_data = request_data['event']
107 |
108 | # Events have a "type" attribute included in their payload, allowing you to handle different
109 | # event payloads as needed.
110 | case event_data['type']
111 | when 'url_verification'
112 | # When we receive a `url_verification` event, we need to
113 | # return the same `challenge` value sent to us from Slack
114 | # to confirm our server's authenticity.
115 | request_data['challenge']
116 | end
117 | when 'team_join'
118 | # Event handler for when a user joins a team
119 | Events.user_join(team_id, event_data)
120 | else
121 | # In the event we receive an event we didn't expect, we'll log it and move on.
122 | puts "Unexpected event:\n"
123 | puts JSON.pretty_generate(request_data)
124 | end
125 | end
126 | # Return HTTP status code 200 so Slack knows we've received the event
127 | status 200
128 | end
129 | end
130 | end
131 |
132 | class Events
133 | # A new user joins the team
134 | def self.user_join(team_id, event_data)
135 | user_id = event_data['user']['id']
136 | # Store a copy of the tutorial_content object specific to this user, so we can edit it
137 | $teams[team_id][user_id] = {
138 | tutorial_content: SlackTutorial.new
139 | }
140 | # Send the user our welcome message, with the tutorial JSON attached
141 | self.send_response(team_id, user_id)
142 | end
143 |
144 | def self.message(team_id, event_data)
145 | # Message share events are posted as a message with an attachment object
146 | # attached. The first item (and only, in this context) is the original
147 | # welcome message we sent the user.
148 | if event_data['attachments']
149 | if event_data['attachments'].first['is_share']
150 | user_id = event_data['user']
151 | ts = event_data['attachments'].first['ts']
152 | channel = event_data['channel']
153 | SlackTutorial.update_item( team_id, user_id, SlackTutorial.items[:share])
154 | self.send_response(team_id, user_id, channel, ts)
155 | end
156 | end
157 | end
158 |
159 | end
160 |
161 | ```
162 |
163 | In [bot.rb](./bot.rb#L67), you'll see example handlers for all of the events listed above.
164 |
165 | ---
166 | **Next [Section 4: Running and Installing Your App](Section-4.md)**
167 |
--------------------------------------------------------------------------------
/Section-4.md:
--------------------------------------------------------------------------------
1 | ## Section 4: Running and Installing your app
2 | **Previous [Section 3: Event Handlers](Section-3.md)**
3 |
4 | ### Set Your Environment Variables and Start the Server
5 | On your app's **Basic Information** page, you'll see two fields labeled **Client ID**, **Client Secret** and **Verification Token**. Your
6 | Client ID will be used to identify your app any time you make a request to Slack's APIs. The Client Secret is used during the OAuth
7 | negotiation process to validate your app's authenticity and the Verification token will be used by your server to verify that requests are
8 | coming from Slack.
9 |
10 | >
11 |
12 | * Assign your app's tokens and verification code to environmental variables. These values are available on your app's **Basic Information**
13 | page.
14 | * `export SLACK_CLIENT_ID="XXXX.XXXX"`
15 | * `export SLACK_API_SECRET="XXXX"`
16 | * `export SLACK_VERIFICATION_TOKEN="XXXX"`
17 |
18 | * Set the OAuth redirect URL using the URL listed in your app's **OAuth & Permissions** settings.
19 | * `export SLACK_REDIRECT_URI="XXXX"`
20 |
21 | * Install the required gems using bundler `bundle install`
22 | * Start the app by calling `rackup`
23 |
24 | ### Go back and add your Request URL
25 | Using the publicly accessible URL provided by ngrok, something like `https://h7465j.ngrok.io/`, our event endpoint will be
26 | `https://h7465j.ngrok.io/events`.
27 |
28 | Enter this URL in the **Request URL** field on your app's **Event Subscriptions** page.
29 |
30 | >
31 |
32 | >
33 |
34 | ### Installing The App
35 | Once all of our app's functionality is in place, we can go ahead and install the app on our team. :tada:
36 |
37 | Visit your app's [/auth/start](http://0.0.0.0:9292) page and click the "Add to Slack" button to begin the OAuth flow. When you
38 | click the button, you'll be directed to Slack's auth request page, where a user specifies a team and agrees to grant access for
39 | the items specified in the app's desired scope. In this demo, we're only using the `bot` scope.
40 |
41 | You can read more about Slack's OAuth Scopes [here](https://api.slack.com/docs/oauth-scopes).
42 |
43 | Once your app has been authorized, Slack will begin sending Events relevant to your bot to your `/events` endpoint. :clap:
44 |
45 | #### Your bot should now be able to see when new users join your team and welcome them with our snazzy onboarding tutorial :tada:
46 | >
47 |
48 | ### Bonus: Adding additional event handlers
49 | Let's say we wanted to add an additional event handler for responding when a user says "Hello" to our bot in DM. You'd start by looking at the incoming message content to see whether it contains a greeting.
50 |
51 | The `message` event contains a `text` attribute. We can scan the contents of that text to see if it contains a greeting like so:
52 | ```ruby
53 | event_data['text'].scan(/hi|hello|greetings/i).any?
54 | ```
55 |
56 | Once we determine whether the message text contains a greeting, we simply call the `chat_postMessage` method and send a greeting back to the user. :smile:
57 |
58 | ```ruby
59 | # INCOMING GREETING
60 | # We only care about message events with text and only if that text contains a greeting.
61 | if event_data['text'] && event_data['text'].scan(/hi|hello|greetings/i).any?
62 | # If the message does contain a greeting, say "Hello" back to the user.
63 | $teams[team_id]['client'].chat_postMessage(
64 | as_user: 'true',
65 | channel: user_id,
66 | text: "Hello <@#{user_id}>!"
67 | )
68 | end
69 | ```
70 |
71 | Here's what it looks like with the rest of our message event handlers in **[bot.rb](./bot.rb#L129-L174)**:
72 |
73 | ```ruby
74 | def self.message(team_id, event_data)
75 | user_id = event_data['user']
76 | # Don't process messages sent from our bot user
77 | unless user_id == $teams[team_id][:bot_user_id]
78 |
79 | # This is where our `message` event handlers go:
80 |
81 | # INCOMING GREETING
82 | # We only care about message events with text and only if that text contains a greeting.
83 | if event_data['text'] && event_data['text'].scan(/hi|hello|greetings/i).any?
84 | # If the message does contain a greeting, say "Hello" back to the user.
85 | $teams[team_id]['client'].chat_postMessage(
86 | as_user: 'true',
87 | channel: user_id,
88 | text: "Hello <@#{user_id}>!"
89 | )
90 | end
91 |
92 | # SHARED MESSAGE EVENT
93 | # To check for shared messages, we must check for the `attachments` attribute
94 | # and see if it contains an `is_shared` attribute.
95 | if event_data['attachments'] && event_data['attachments'].first['is_share']
96 | # We found a shared message
97 | user_id = event_data['user']
98 | ts = event_data['attachments'].first['ts']
99 | channel = event_data['channel']
100 | # Update the `share` section of the user's tutorial
101 | SlackTutorial.update_item( team_id, user_id, SlackTutorial.items[:share])
102 | # Update the user's tutorial message
103 | self.send_response(team_id, user_id, channel, ts)
104 | end
105 | end
106 | ```
107 |
108 | ---
109 |
110 | ## Feedback
111 | I'd love to hear your feedback on this example. You can file an issue, submit a PR or message me on either of these:
112 |
113 | GitHub: [roach](https://github.com/roach) Twitter: [@roach](https://twitter.com/roach)
114 |
115 | ---
116 | **Go back to [README](README.md)**
117 |
--------------------------------------------------------------------------------
/auth.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'slack-ruby-client'
3 |
4 | # Load Slack app info into a hash called `config` from the environment variables assigned during setup
5 | # See the "Running the app" section of the README for instructions.
6 | SLACK_CONFIG = {
7 | slack_client_id: ENV['SLACK_CLIENT_ID'],
8 | slack_api_secret: ENV['SLACK_API_SECRET'],
9 | slack_redirect_uri: ENV['SLACK_REDIRECT_URI'],
10 | slack_verification_token: ENV['SLACK_VERIFICATION_TOKEN']
11 | }
12 |
13 | # Check to see if the required variables listed above were provided, and raise an exception if any are missing.
14 | missing_params = SLACK_CONFIG.select { |key, value| value.nil? }
15 | if missing_params.any?
16 | error_msg = missing_params.keys.join(", ").upcase
17 | raise "Missing Slack config variables: #{error_msg}"
18 | end
19 |
20 | # Set the OAuth scope of your bot. We're just using `bot` for this demo, as it has access to
21 | # all the things we'll need to access. See: https://api.slack.com/docs/oauth-scopes for more info.
22 | BOT_SCOPE = 'bot'
23 |
24 | # This hash will contain all the info for each authed team, as well as each team's Slack client object.
25 | # In a production environment, you may want to move some of this into a real data store.
26 | $teams = {}
27 |
28 | # Since we're going to create a Slack client object for each team, this helper keeps all of that logic in one place.
29 | def create_slack_client(slack_api_secret)
30 | Slack.configure do |config|
31 | config.token = slack_api_secret
32 | fail 'Missing API token' unless config.token
33 | end
34 | Slack::Web::Client.new
35 | end
36 |
37 | # Slack uses OAuth for user authentication. This auth process is performed by exchanging a set of
38 | # keys and tokens between Slack's servers and yours. This process allows the authorizing user to confirm
39 | # that they want to grant our bot access to their team.
40 | # See https://api.slack.com/docs/oauth for more information.
41 | class Auth < Sinatra::Base
42 | # This is the HTML markup for our "Add to Slack" button.
43 | # Note that we pass the `client_id`, `scope` and "redirect_uri" parameters specific to our application's configs.
44 | add_to_slack_button = %(
45 |
46 |
47 |
48 | )
49 |
50 | # If a user tries to access the index page, redirect them to the auth start page
51 | get '/' do
52 | redirect '/begin_auth'
53 | end
54 |
55 | # OAuth Step 1: Show the "Add to Slack" button, which links to Slack's auth request page.
56 | # This page shows the user what our app would like to access and what bot user we'd like to create for their team.
57 | get '/begin_auth' do
58 | status 200
59 | body add_to_slack_button
60 | end
61 |
62 | # OAuth Step 2: The user has told Slack that they want to authorize our app to use their account, so
63 | # Slack sends us a code which we can use to request a token for the user's account.
64 | get '/finish_auth' do
65 | client = Slack::Web::Client.new
66 | # OAuth Step 3: Success or failure
67 | begin
68 | response = client.oauth_access(
69 | {
70 | client_id: SLACK_CONFIG[:slack_client_id],
71 | client_secret: SLACK_CONFIG[:slack_api_secret],
72 | redirect_uri: SLACK_CONFIG[:slack_redirect_uri],
73 | code: params[:code] # (This is the OAuth code mentioned above)
74 | }
75 | )
76 | # Success:
77 | # Yay! Auth succeeded! Let's store the tokens and create a Slack client to use in our Events handlers.
78 | # The tokens we receive are used for accessing the Web API, but this process also creates the Team's bot user and
79 | # authorizes the app to access the Team's Events.
80 | team_id = response['team_id']
81 | $teams[team_id] = {
82 | user_access_token: response['access_token'],
83 | bot_user_id: response['bot']['bot_user_id'],
84 | bot_access_token: response['bot']['bot_access_token']
85 | }
86 |
87 | $teams[team_id]['client'] = create_slack_client(response['bot']['bot_access_token'])
88 | # Be sure to let the user know that auth succeeded.
89 | status 200
90 | body "Yay! Auth succeeded! You're awesome!"
91 | rescue Slack::Web::Api::Error => e
92 | # Failure:
93 | # D'oh! Let the user know that something went wrong and output the error message returned by the Slack client.
94 | status 403
95 | body "Auth failed! Reason: #{e.message}
#{add_to_slack_button}"
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/bot.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'slack-ruby-client'
3 |
4 | # This class contains all of the logic for loading, cloning and updating the tutorial message attachments.
5 | class SlackTutorial
6 | # Store the welcome text for use when sending and updating the tutorial messages
7 | def self.welcome_text
8 | "Welcome to Slack! We're so glad you're here.\nGet started by completing the steps below."
9 | end
10 |
11 | # Load the tutorial JSON file into a hash
12 | def self.tutorial_json
13 | tutorial_file = File.read('welcome.json')
14 | tutorial_json = JSON.parse(tutorial_file)
15 | attachments = tutorial_json["attachments"]
16 | end
17 |
18 | # Store the index of each tutorial section in TUTORIAL_JSON for easy reference later
19 | def self.items
20 | { reaction: 0, pin: 1, share: 2 }
21 | end
22 |
23 | # Return a new copy of tutorial_json so each user has their own instance
24 | def self.new
25 | self.tutorial_json.deep_dup
26 | end
27 |
28 | # This is a helper function to update the state of tutorial items
29 | # in the hash shown above. When the user completes an action on the
30 | # tutorial, the item's icon will be set to a green checkmark and
31 | # the item's border color will be set to blue
32 | def self.update_item(team_id, user_id, item_index)
33 | # Update the tutorial section by replacing the empty checkbox with the green
34 | # checkbox and updating the section's color to show that it's completed.
35 | tutorial_item = $teams[team_id][user_id][:tutorial_content][item_index]
36 | tutorial_item['text'].sub!(':white_large_square:', ':white_check_mark:')
37 | tutorial_item['color'] = '#439FE0'
38 | end
39 | end
40 |
41 | # This class contains all of the webserver logic for processing incoming requests from Slack.
42 | class API < Sinatra::Base
43 | # This is the endpoint Slack will post Event data to.
44 | post '/events' do
45 | # Extract the Event payload from the request and parse the JSON
46 | request_data = JSON.parse(request.body.read)
47 | # Check the verification token provided with the request to make sure it matches the verification token in
48 | # your app's setting to confirm that the request came from Slack.
49 | unless SLACK_CONFIG[:slack_verification_token] == request_data['token']
50 | halt 403, "Invalid Slack verification token received: #{request_data['token']}"
51 | end
52 |
53 | case request_data['type']
54 | # When you enter your Events webhook URL into your app's Event Subscription settings, Slack verifies the
55 | # URL's authenticity by sending a challenge token to your endpoint, expecting your app to echo it back.
56 | # More info: https://api.slack.com/events/url_verification
57 | when 'url_verification'
58 | request_data['challenge']
59 |
60 | when 'event_callback'
61 | # Get the Team ID and Event data from the request object
62 | team_id = request_data['team_id']
63 | event_data = request_data['event']
64 |
65 | # Events have a "type" attribute included in their payload, allowing you to handle different
66 | # Event payloads as needed.
67 | case event_data['type']
68 | when 'team_join'
69 | # Event handler for when a user joins a team
70 | Events.user_join(team_id, event_data)
71 | when 'reaction_added'
72 | # Event handler for when a user reacts to a message or item
73 | Events.reaction_added(team_id, event_data)
74 | when 'pin_added'
75 | # Event handler for when a user pins a message
76 | Events.pin_added(team_id, event_data)
77 | when 'message'
78 | # Event handler for messages, including Share Message actions
79 | Events.message(team_id, event_data)
80 | else
81 | # In the event we receive an event we didn't expect, we'll log it and move on.
82 | puts "Unexpected event:\n"
83 | puts JSON.pretty_generate(request_data)
84 | end
85 | # Return HTTP status code 200 so Slack knows we've received the Event
86 | status 200
87 | end
88 | end
89 | end
90 |
91 | # This class contains all of the Event handling logic.
92 | class Events
93 | # You may notice that user and channel IDs may be found in
94 | # different places depending on the type of event we're receiving.
95 |
96 | # A new user joins the team
97 | def self.user_join(team_id, event_data)
98 | user_id = event_data['user']['id']
99 | # Store a copy of the tutorial_content object specific to this user, so we can edit it
100 | $teams[team_id][user_id] = {
101 | tutorial_content: SlackTutorial.new
102 | }
103 | # Send the user our welcome message, with the tutorial JSON attached
104 | self.send_response(team_id, user_id)
105 | end
106 |
107 | # A user reacts to a message
108 | def self.reaction_added(team_id, event_data)
109 | user_id = event_data['user']
110 | if $teams[team_id][user_id]
111 | channel = event_data['item']['channel']
112 | ts = event_data['item']['ts']
113 | SlackTutorial.update_item(team_id, user_id, SlackTutorial.items[:reaction])
114 | self.send_response(team_id, user_id, channel, ts)
115 | end
116 | end
117 |
118 | # A user pins a message
119 | def self.pin_added(team_id, event_data)
120 | user_id = event_data['user']
121 | if $teams[team_id][user_id]
122 | channel = event_data['item']['channel']
123 | ts = event_data['item']['message']['ts']
124 | SlackTutorial.update_item(team_id, user_id, SlackTutorial.items[:pin])
125 | self.send_response(team_id, user_id, channel, ts)
126 | end
127 | end
128 |
129 | def self.message(team_id, event_data)
130 | user_id = event_data['user']
131 | # Don't process messages sent from our bot user
132 | unless user_id == $teams[team_id][:bot_user_id]
133 |
134 | # This is where our `message` event handlers go:
135 |
136 | # SHARED MESSAGE EVENT
137 | # To check for shared messages, we must check for the `attachments` attribute
138 | # and see if it contains an `is_shared` attribute.
139 | if event_data['attachments'] && event_data['attachments'].first['is_share']
140 | # We found a shared message
141 | user_id = event_data['user']
142 | ts = event_data['attachments'].first['ts']
143 | channel = event_data['channel']
144 | # Update the `share` section of the user's tutorial
145 | SlackTutorial.update_item( team_id, user_id, SlackTutorial.items[:share])
146 | # Update the user's tutorial message
147 | self.send_response(team_id, user_id, channel, ts)
148 | end
149 | end
150 | end
151 |
152 | # Send a response to an Event via the Web API.
153 | def self.send_response(team_id, user_id, channel = user_id, ts = nil)
154 | # `ts` is optional, depending on whether we're sending the initial
155 | # welcome message or updating the existing welcome message tutorial items.
156 | # We open a new DM with `chat.postMessage` and update an existing DM with
157 | # `chat.update`.
158 | if ts
159 | $teams[team_id]['client'].chat_update(
160 | as_user: 'true',
161 | channel: channel,
162 | ts: ts,
163 | text: SlackTutorial.welcome_text,
164 | attachments: $teams[team_id][user_id][:tutorial_content]
165 | )
166 | else
167 | $teams[team_id]['client'].chat_postMessage(
168 | as_user: 'true',
169 | channel: channel,
170 | text: SlackTutorial.welcome_text,
171 | attachments: $teams[team_id][user_id][:tutorial_content]
172 | )
173 | end
174 | end
175 |
176 | end
177 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require './auth'
2 | require './bot'
3 |
4 | # Initialize the app and create the API (bot) and Auth objects.
5 | run Rack::Cascade.new [API, Auth]
6 |
--------------------------------------------------------------------------------
/welcome.json:
--------------------------------------------------------------------------------
1 | {
2 | "attachments": [{
3 | "mrkdwn_in": ["text"],
4 | "author_name": "Learn How to Use Emoji Reactions",
5 | "author_link": "https://get.slack.help/hc/en-us/articles/206870317-Emoji-reactions",
6 | "text": ":white_large_square: *Add an emoji reaction to this message* :thinking_face:",
7 | "fields": [{
8 | "value": "You can quickly respond to any message on Slack with an emoji reaction. Reactions can be used for any purpose: voting, checking off to-do items, showing excitement."
9 | }]
10 | }, {
11 | "mrkdwn_in": ["text"],
12 | "author_name": "Learn How to Pin a Message",
13 | "author_link": "https://get.slack.help/hc/en-us/articles/205239997-Pinning-messages-and-files",
14 | "text": ":white_large_square: *Pin this message* :round_pushpin:",
15 | "fields": [{
16 | "value": "Important messages and files can be pinned to the details pane in any channel or direct message, including group messages, for easy reference."
17 | }]
18 | },{
19 | "mrkdwn_in": ["text"],
20 | "author_name": "Learn How to Share a Message in Slack",
21 | "author_link": "https://get.slack.help/hc/en-us/articles/203274767-Share-messages-in-Slack",
22 | "text": ":white_large_square: *Share this Message* :mailbox_with_mail:",
23 | "fields": [{
24 | "value": "Sharing messages in Slack can help keep conversations on your team organized. And, it's easy to do!"
25 | }]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------