├── .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 | >![onboarding](https://cloud.githubusercontent.com/assets/32463/20369171/690028d2-ac0c-11e6-95a1-c3078762fddd.gif) 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 | > ![create a new app](https://cloud.githubusercontent.com/assets/32463/20549718/afdd98d0-b0e3-11e6-8d83-8ad7053deb80.png) 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 | >![oauth](https://cloud.githubusercontent.com/assets/32463/20575277/789c0c5a-b16d-11e6-86fd-e30c3a0d2e61.gif) 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 | >![oauth redirect url](https://cloud.githubusercontent.com/assets/32463/20543629/63e41a26-b0bb-11e6-8eee-90c6f4f1dbb1.png) 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 | >![bot user](https://cloud.githubusercontent.com/assets/32463/20371297/9044e2a0-ac18-11e6-8f25-3ffbd8a3bf58.png) 19 | 20 | ### Event Subscriptions 21 | First, Enable events: 22 | >![enable events](https://cloud.githubusercontent.com/assets/32463/20549612/e7ee2ed4-b0e2-11e6-8b9c-01ed08057c7c.png) 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 | >![bot events](https://cloud.githubusercontent.com/assets/32463/20366596/b40ffbc4-ac00-11e6-9626-6356be5612f8.png) 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 | >![request url](https://cloud.githubusercontent.com/assets/32463/20366597/b411042e-ac00-11e6-92ce-fc49940b5786.png) 10 | 11 | >![url verified](https://cloud.githubusercontent.com/assets/32463/20366593/b40d14a4-ac00-11e6-8413-b473c16ef997.png) 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 | >![app credentials](https://cloud.githubusercontent.com/assets/32463/20445302/61ddfc54-ad89-11e6-8523-245a60c875b0.png) 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 | >![request url](https://cloud.githubusercontent.com/assets/32463/20366597/b411042e-ac00-11e6-92ce-fc49940b5786.png) 31 | 32 | >![url verified](https://cloud.githubusercontent.com/assets/32463/20366593/b40d14a4-ac00-11e6-8413-b473c16ef997.png) 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 | >![onboarding](https://cloud.githubusercontent.com/assets/32463/20369171/690028d2-ac0c-11e6-95a1-c3078762fddd.gif) 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 | \"Add 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 | --------------------------------------------------------------------------------