├── .github └── CODE_OF_CONDUCT.md ├── .gitignore ├── README.md └── example.py /.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) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | env 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Maximizing your Slack app's usefulness 2 | --------------------------------------- 3 | 4 | When we think about how bots and humans interact within Slack, we often think of two types of interaction: **Notification** and **Conversation**. 5 | 6 | The simplest form of Slack integration is to have your app post important information from another service into a slack channel. This works well for a variety of situations, such as package arrival notifications or an alert that one of your servers has caught on fire. Notifications are simple, timely, and informative. When your app is a part of a much more complex workflow, notifications are just a jumping point into another application or process. 7 | 8 | 9 | 10 | As your processes and workflows become more complex, involving several applications across multiple windows, things can get ...complicated. 11 | 12 | Message Buttons 13 | ---------------- 14 | 15 | Back in June, we released Message Buttons. Using these new interactive elements, developers were able to bring common workflow actions right into the bot's message. Users could not only see the notification message, but take action from inside Slack. This also eased a lot of the complexity with conversational bots. Rather than requiring a user to type a specific confirmation message, you were able to show the user a set of buttons to complete or cancel an action. 16 | 17 | ![Message Buttons demo GIF](https://cdn-images-1.medium.com/max/800/1*aYzTFMBlg8tGnKP7fv7oJg.gif) 18 | 19 | While Message Buttons are a great way to increase the usability of your Slack application, there are some limitations. One limitation is the amount of real estate buttons take up. When you have a large set of options, you'll end up with a message that takes up the user's entire chat window or gets truncated. 20 | 21 | See our [Message Buttons with Node.js](https://api.slack.com/tutorials/intro-to-message-buttons) tutorial for more information on how to add message buttons to your app. 22 | 23 | Message Menus 24 | ------------- 25 | 26 | Today we've released **[message menus](https://api.slack.com/docs/message-menus)** :tada: 27 | 28 | Message menus are the newest interactive feature for Slack apps: clickable dropdown menus that you can add to [message attachments](https://api.slack.com/docs/message-attachments). They can have static options, or they can load dynamically. 29 | You can build with five types of message menu today, each achieving a different flavor of use case: static menus, user menus, channel menus, conversation menus, and dynamic menus. 30 | 31 | ![Slack message menu example](https://cdn-images-1.medium.com/max/800/1*lLR-3KbUjwPF9l6jEbiEwQ.gif) 32 | 33 | Read more about how apps are using message menus on [our blog](https://medium.com/slack-developer-blog/build-an-interactive-slack-app-with-message-menus-1fb2c6298308). 34 | 35 | Adding message menus to your app 36 | -------------------------------- 37 | 38 | In this tutorial, we'll focus on adding dynamic option menus to your app. Here's a summary of how dynamic interactive message menus work: 39 | 40 | 1. Post a message containing one or more ``attachments`` containing one or more interactive elements 41 | 2. Slack sends a request to your registered Options Load URL containing the context you need to generate relevant menu options. You simply respond to this request with a JSON array of options. 42 | 3. Users click a button or select an option from a menu 43 | 4. A request is sent to your registered Action URL containing all the context you need to understand: who clicked it, which option they clicked, which message ``callback_id`` was associated with the message, and the original message inciting the selection 44 | 5. You respond to your Action URL's invocation with a message to replace the original, and/or a new ephemeral message, and/or you utilize the invocation's ``response_url`` to update the original message out of band for a limited time. 45 | 46 | 47 | **Let's build it!** 48 | 49 | :bulb: This code is also available as a complete example application on [GitHub](https://github.com/slackapi/python-message-menu-example/blob/master/example.py). 50 | 51 | While we're building this example in Python, we haven't forgotten about the Node fans. 52 | We've put together a really neat example for [Node.js over here](https://github.com/slackapi/sample-message-menus-node) :tada: 53 | 54 | For this example, we're going to need [Python](https://www.python.org/), the Python Slack client ([slackclient](https://github.com/slackapi/python-slackclient)) and a webserver ([Flask](http://flask.pocoo.org/)). 55 | 56 | First you'll create the basic elements of the app: A Slack client and webserver 57 | 58 | ``` 59 | from flask import Flask, request, make_response, Response 60 | from slackclient import SlackClient 61 | import json 62 | 63 | # Your app's Slack bot user token 64 | SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"] 65 | 66 | # Slack client for Web API requests 67 | slack_client = SlackClient(SLACK_BOT_TOKEN) 68 | 69 | # Flask webserver for incoming traffic from Slack 70 | app = Flask(__name__) 71 | ``` 72 | 73 | Then we'll add the two endpoints Slack will POST requests to. The first endpoint 74 | is where Slack will send a request for items to populate the menu options, 75 | we'll call this one ``message_options``. 76 | 77 | ``` 78 | @app.route("/slack/message_options", methods=["POST"]) 79 | def message_options(): 80 | # Parse the request payload 81 | form_json = json.loads(request.form["payload"]) 82 | 83 | menu_options = { 84 | "options": [ 85 | { 86 | "text": "Chess", 87 | "value": "chess" 88 | }, 89 | { 90 | "text": "Global Thermonuclear War", 91 | "value": "war" 92 | } 93 | ] 94 | } 95 | 96 | return Response(json.dumps(menu_options), mimetype='application/json') 97 | ``` 98 | 99 | The second endpoint you'll need is where Slack will send data when a user makes 100 | a selection. We'll call this one ``message_actions``. 101 | 102 | ``` 103 | @app.route("/slack/message_actions", methods=["POST"]) 104 | def message_actions(): 105 | 106 | # Parse the request payload 107 | form_json = json.loads(request.form["payload"]) 108 | 109 | # Check to see what the user's selection was and update the message 110 | selection = form_json["actions"][0]["selected_options"][0]["value"] 111 | 112 | if selection == "war": 113 | message_text = "The only winning move is not to play.\nHow about a nice game of chess?" 114 | else: 115 | message_text = ":horse:" 116 | 117 | response = slack_client.api_call( 118 | "chat.update", 119 | channel=form_json["channel"]["id"], 120 | ts=form_json["message_ts"], 121 | text=message_text, 122 | attachments=[] 123 | ) 124 | 125 | return make_response("", 200) 126 | ``` 127 | 128 | Now that our endpoints are configured, we can build the message containing the menu and send it to a channel. 129 | 130 | 131 | In order to show the menu, we'll have to build the message attachment which will contain it. 132 | 133 | ``` 134 | message_attachments = [ 135 | { 136 | "fallback": "Upgrade your Slack client to use messages like these.", 137 | "color": "#3AA3E3", 138 | "attachment_type": "default", 139 | "callback_id": "menu_options_2319", 140 | "actions": [ 141 | { 142 | "name": "games_list", 143 | "text": "Pick a game...", 144 | "type": "select", 145 | "data_source": "external" 146 | } 147 | ] 148 | } 149 | ] 150 | ``` 151 | 152 | Once the attachment JSON is ready, simply post a message to the channel, adding the attachment containing the menu. 153 | 154 | ``` 155 | slack_client.api_call( 156 | "chat.postMessage", 157 | channel="C09EM2073", 158 | text="Shall we play a game?", 159 | attachments=message_attachments 160 | ) 161 | ``` 162 | 163 | Take note of the ``"data_source": "external"`` attribute in the attachment JSON. This is how Slack knows to pull the menu options from the ``message_options`` endpoint we set up above. 164 | 165 | ``` 166 | slack_client.api_call( 167 | "chat.postMessage", 168 | channel="C09EM2073", 169 | text="Shall we play a game?", 170 | attachments=attachments_json 171 | ) 172 | ``` 173 | 174 | 175 | Support 176 | -------- 177 | 178 | Need help? Join [Bot Developer Hangout](http://dev4slack.xoxco.com/) and talk to us in [#slack-api](https://dev4slack.slack.com/messages/slack-api/). 179 | 180 | 181 | You can also create an Issue right here on [GitHub](https://github.com/slackapi/python-message-menu-example/issues). 182 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response, Response 2 | import os 3 | import json 4 | 5 | from slackclient import SlackClient 6 | 7 | # Your app's Slack bot user token 8 | SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"] 9 | SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"] 10 | 11 | # Slack client for Web API requests 12 | slack_client = SlackClient(SLACK_BOT_TOKEN) 13 | 14 | # Flask webserver for incoming traffic from Slack 15 | app = Flask(__name__) 16 | 17 | # Helper for verifying that requests came from Slack 18 | def verify_slack_token(request_token): 19 | if SLACK_VERIFICATION_TOKEN != request_token: 20 | print("Error: invalid verification token!") 21 | print("Received {} but was expecting {}".format(request_token, SLACK_VERIFICATION_TOKEN)) 22 | return make_response("Request contains invalid Slack verification token", 403) 23 | 24 | 25 | # The endpoint Slack will load your menu options from 26 | @app.route("/slack/message_options", methods=["POST"]) 27 | def message_options(): 28 | # Parse the request payload 29 | form_json = json.loads(request.form["payload"]) 30 | 31 | # Verify that the request came from Slack 32 | verify_slack_token(form_json["token"]) 33 | 34 | # Dictionary of menu options which will be sent as JSON 35 | menu_options = { 36 | "options": [ 37 | { 38 | "text": "Cappuccino", 39 | "value": "cappuccino" 40 | }, 41 | { 42 | "text": "Latte", 43 | "value": "latte" 44 | } 45 | ] 46 | } 47 | 48 | # Load options dict as JSON and respond to Slack 49 | return Response(json.dumps(menu_options), mimetype='application/json') 50 | 51 | 52 | # The endpoint Slack will send the user's menu selection to 53 | @app.route("/slack/message_actions", methods=["POST"]) 54 | def message_actions(): 55 | 56 | # Parse the request payload 57 | form_json = json.loads(request.form["payload"]) 58 | 59 | # Verify that the request came from Slack 60 | verify_slack_token(form_json["token"]) 61 | 62 | # Check to see what the user's selection was and update the message accordingly 63 | selection = form_json["actions"][0]["selected_options"][0]["value"] 64 | 65 | if selection == "cappuccino": 66 | message_text = "cappuccino" 67 | else: 68 | message_text = "latte" 69 | 70 | response = slack_client.api_call( 71 | "chat.update", 72 | channel=form_json["channel"]["id"], 73 | ts=form_json["message_ts"], 74 | text="One {}, right coming up! :coffee:".format(message_text), 75 | attachments=[] # empty `attachments` to clear the existing massage attachments 76 | ) 77 | 78 | # Send an HTTP 200 response with empty body so Slack knows we're done here 79 | return make_response("", 200) 80 | 81 | # Send a Slack message on load. This needs to be _before_ the Flask server is started 82 | 83 | # A Dictionary of message attachment options 84 | attachments_json = [ 85 | { 86 | "fallback": "Upgrade your Slack client to use messages like these.", 87 | "color": "#3AA3E3", 88 | "attachment_type": "default", 89 | "callback_id": "menu_options_2319", 90 | "actions": [ 91 | { 92 | "name": "bev_list", 93 | "text": "Pick a beverage...", 94 | "type": "select", 95 | "data_source": "external" 96 | } 97 | ] 98 | } 99 | ] 100 | 101 | # Send a message with the above attachment, asking the user if they want coffee 102 | slack_client.api_call( 103 | "chat.postMessage", 104 | channel="#python", 105 | text="Would you like some coffee? :coffee:", 106 | attachments=attachments_json 107 | ) 108 | 109 | # Start the Flask server 110 | if __name__ == "__main__": 111 | app.run() 112 | --------------------------------------------------------------------------------