├── README.md └── example.py /README.md: -------------------------------------------------------------------------------- 1 | ## [Out and back again](https://medium.com/slack-developer-blog/out-and-back-again-6b2f3c84f484) 2 | 3 | #### Using link buttons and deep linking for a seamless flow between your service and Slack 4 | 5 | ![Slack message with link button](https://cdn-images-1.medium.com/max/1600/0*kA5KlFd0uNWcWMMz.) 6 | 7 | Slack apps allow teams to get quick tasks done, alongside their other work, without having to context switch into another product or browser window. But not every part of your service needs to be brought into Slack. “Deep work” that takes more than a few minutes — like setting up a project management board, building a wireframe, or creating a metrics dashboard — can sometimes be a better fit for your external service or web app. 8 | 9 | Now with [deep linking](https://api.slack.com/docs/deep-linking) and [link buttons](https://api.slack.com/docs/message-attachments#link_buttons) — a new type of button that enables you to link out to an external webpage — you can build a user-friendly flow to guide people from Slack to your external service, then direct them back into a particular message in Slack to continue working. In this tutorial, we’ll show you how to do it. 10 | 11 | ##### About link buttons 12 | 13 | Link buttons are Slack message attachments that look like interactive message buttons, but rather than sending an action event to your app, they link out to an external webpage of your choice. Compared to the previous solution — static hyperlinked text — link buttons UI can promote user engagement. Apps that adopt link buttons in favor of hyperlinked text have seen click-throughs increase by as much as 85%. 14 | 15 | Aside from promoting user engagement, link buttons offer a workaround for developers building apps behind a firewall. Features like message buttons, message menus and dialogs can help increase app engagement, but there’s a limitation: you need to add additional webhooks to your application. Because these features require a request URL, some developers — for instance, those running an app on their company’s internal network or a service behind a firewall — were previously blocked from building interactive Slack apps. For developers who have this limitation, link buttons offer an alternative: you can build an interactive app without the overhead of adding additional endpoints. 16 | 17 | ##### Using link buttons 18 | 19 | When someone clicks a link button, they’re taken to your webpage to complete a task. Once that work has been completed, you can use deep linking to bring them back to where they left off the conversation in Slack. 20 | You can break it down into a few steps. We’ll go through each in detail. 21 | 1. Have your app post a message with a link button, leading to your external application or website 22 | 2. The user loads the website and completes a workflow 23 | 3. The user is directed to a “complete” page and the original Slack message is updated. A message permalink is fetched and shown to the user 24 | 4. The user clicks back into Slack and resumes the conversation 25 | 26 | ![Example workflow using link buttons and message permalinks](https://cdn-images-1.medium.com/max/1600/1*FD388dGN2C7jS8cj07JVog.gif) 27 | 28 | ##### 1. Post a message with a link button leading to your external app 29 | 30 | You’ll need to keep a map of the message IDs so you can correlate the message your app posts in Slack with the task you’re giving the user. The simplest way to do this is with a dictionary that contains a reference for each channel and message timestamp: 31 | 32 | ``` 33 | # A dictionary to store `task_id` to message mappings 34 | TASK_IDS = {} 35 | ``` 36 | 37 | Next, have your app post a message with a link button which will take the user out to the pending task. The button should be associated with a unique task, like a ticket number or another unique ID. For this demo, we’ll use `LB-2375`. 38 | 39 | ``` 40 | # Our task ID. This could be a ticket number or any other unique ID 41 | task_id = 'LB-2375' 42 | ``` 43 | 44 | Adding a link button to a message is as simple as adding an attachment containing the URL you went to link to and the text you want on the button. 45 | 46 | ``` 47 | # Attachment JSON containing our link button 48 | # For this demo app, The task ID should be the last segment of the URL 49 | attach_json = [ 50 | { 51 | "fallback": "Upgrade your Slack client to use messages like these.", 52 | "color": "#CC0000", 53 | "actions": [ 54 | { 55 | "type": "button", 56 | "text": ":red_circle: Complete Task: " + task_id, 57 | "url": "https://roach.ngrok.io/workflow/" + task_id, 58 | } 59 | ] 60 | } 61 | ] 62 | ``` 63 | 64 | When the message is posted, Slack’s Web API will return some data about the message (see `res` below) such as the channel, timestamp and message content. We’re going to store the channel and timestamp to the map under the task ID. 65 | 66 | ``` 67 | # Post the message to Slack, storing the result as `res` 68 | res = slack.api_call( 69 | "chat.postMessage", 70 | channel="#link-buttons", 71 | text="Let's get started!", 72 | attachments=attach_json 73 | ) 74 | # Store the message `ts` and `channel`, so we can request the message 75 | # permalink later when the user clicks the link button 76 | TASK_IDS[task_id] = { 77 | 'channel': res['channel'], 78 | 'ts': res['message']['ts'] 79 | } 80 | ``` 81 | 82 | ##### 2. Guide the user through the task 83 | 84 | Now the message is sent to the user, so they can click the link button to be guided to your external app. When they land on your page, you’ll need to grab the task ID from the URL or params, show them the task to be completed, and guide them to a completed state. 85 | For this very simplified demo, we’re going to show the user a form with one action. When the user submits this form, we will direct them to the `/completed` page. 86 | 87 | ``` 88 | # This is where our link button will link to, showing the user a 89 | # task to complete before redirecting them to the `/complete` page 90 | @app.route("/workflow/", methods=['GET']) 91 | def test(task_id): 92 | 93 | task_form = """
94 | 95 |
""".format(task_id) 96 | 97 | return make_response(task_form, 200) 98 | ``` 99 | 100 | ##### 3. Update the message and bring them back to Slack 101 | 102 | When the user loads the `/completed` page, you’ll need to update the Slack message containing the link button to show that the task has been completed, and present the user with a link back to their Slack conversation. 103 | To update the Slack message containing the button, reference the channel and timestamp we saved in the map earlier, and call `chat.update`. 104 | 105 | ``` 106 | @app.route("/complete/", methods=['POST']) 107 | def complete(task_id): 108 | """ 109 | This is where your app's business logic would live. 110 | Once the task has been complete, the user will be directed to this `/complete` 111 | page, which shows a link back to their Slack conversation 112 | """ 113 | 114 | # When this page loads, we update the original Slack message to show that 115 | # the pending task has been completed 116 | attach_json = [ 117 | { 118 | "fallback": "Upgrade your Slack client to use messages like these.", 119 | "color": "#36a64f", 120 | "text": ":white_check_mark: *Completed Task: {}*".format(task_id), 121 | "mrkdwn_in": ["text"] 122 | } 123 | ] 124 | res = slack.api_call( 125 | "chat.update", 126 | channel=TASK_IDS[task_id]["channel"], 127 | ts=TASK_IDS[task_id]["ts"], 128 | text="Task Complete!", 129 | attachments=attach_json 130 | ) 131 | ``` 132 | 133 | Once the message has been updated, fetch the permalink for it… 134 | 135 | ``` 136 | # Get the message permalink to redirect the user back to Slack 137 | res = slack.api_call( 138 | "chat.getPermalink", 139 | channel=TASK_IDS[task_id]["channel"], 140 | message_ts=TASK_IDS[task_id]["ts"] 141 | ) 142 | ``` 143 | 144 | …and show the “Return to Slack” link. 145 | 146 | ``` 147 | # Link the user back to the original message 148 | slack_link = "Return to Slack".format(res['permalink']) 149 | 150 | # Redirect the user back to their Slack conversation 151 | return make_response("Task Complete!
" + slack_link, 200) 152 | ``` 153 | 154 | That’s it! 🎉 155 | 156 | This simple flow using link buttons and deep linking enables your app’s users to complete “deep work” in your external service, then seamlessly return to where they started in Slack. 157 | 158 | To run this example, clone the repository or copy the code from `example.py`, and run: 159 | 160 | ``` 161 | export SLACK_API_TOKEN=”” 162 | python example.py 163 | ``` 164 | 165 | Need help? Find us in the [Bot Developer Hangout](http://dev4slack.xoxco.com/) [#slack-api channel](https://dev4slack.slack.com/messages/slack-api/), reach out on [Twitter](https://twitter.com/slackapi), or [create an issue](https://github.com/slackapi/python-link-button-example/issues) on GitHub. 166 | 167 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response, redirect 2 | from slackclient import SlackClient 3 | from random import randint 4 | import os 5 | 6 | # Our app's Slack Client 7 | SLACK_BOT_TOKEN = os.environ["SLACK_API_TOKEN"] 8 | slack = SlackClient(SLACK_BOT_TOKEN) 9 | 10 | # A dictionary to store `task_id` to message mappings 11 | TASK_IDS = {} 12 | 13 | # Our app's webserver 14 | app = Flask(__name__) 15 | 16 | 17 | # Our task ID. This could be a ticket number or any other unique ID 18 | task_id = 'LB-2375' 19 | 20 | # Attachment JSON containing our link button 21 | # For this demo app, The task ID should be the last segment of the URL 22 | attach_json = [ 23 | { 24 | "fallback": "Upgrade your Slack client to use messages like these.", 25 | "color": "#CC0000", 26 | "actions": [ 27 | { 28 | "type": "button", 29 | "text": ":red_circle: Complete Task: " + task_id, 30 | "url": "https://roach.ngrok.io/workflow/" + task_id, 31 | } 32 | ] 33 | } 34 | ] 35 | 36 | # Post the message to Slack, storing the result as `res` 37 | res = slack.api_call( 38 | "chat.postMessage", 39 | channel="#link-buttons", 40 | text="Let's get started!", 41 | attachments=attach_json 42 | ) 43 | 44 | # Store the message `ts` and `channel`, so we can request the message 45 | # permalink later when the user clicks the link button 46 | TASK_IDS[task_id] = { 47 | 'channel': res['channel'], 48 | 'ts': res['message']['ts'] 49 | } 50 | 51 | 52 | # This is where our link button will link to, showing the user a 53 | # task to complete before redirecting them to the `/complete` page 54 | @app.route("/workflow/", methods=['GET']) 55 | def test(task_id): 56 | 57 | task_form = """
58 | 59 |
""".format(task_id) 60 | 61 | return make_response(task_form, 200) 62 | 63 | 64 | @app.route("/complete/", methods=['POST']) 65 | def complete(task_id): 66 | """ 67 | This is where your app's business logic would live. 68 | Once the task has been complete, the user will be directed to this `/complete` 69 | page, which shows a link back to their Slack conversation 70 | """ 71 | 72 | # When this page loads, we update the original Slack message to show that 73 | # the pending task has been completed 74 | attach_json = [ 75 | { 76 | "fallback": "Upgrade your Slack client to use messages like these.", 77 | "color": "#36a64f", 78 | "text": ":white_check_mark: *Completed Task: {}*".format(task_id), 79 | "mrkdwn_in": ["text"] 80 | } 81 | ] 82 | res = slack.api_call( 83 | "chat.update", 84 | channel=TASK_IDS[task_id]["channel"], 85 | ts=TASK_IDS[task_id]["ts"], 86 | text="Task Complete!", 87 | attachments=attach_json 88 | ) 89 | 90 | # Get the message permalink to redirect the user back to Slack 91 | res = slack.api_call( 92 | "chat.getPermalink", 93 | channel=TASK_IDS[task_id]["channel"], 94 | message_ts=TASK_IDS[task_id]["ts"] 95 | ) 96 | 97 | # Link the user back to the original message 98 | slack_link = "Return to Slack".format(res['permalink']) 99 | 100 | # Redirect the user back to their Slack conversation 101 | return make_response("Task Complete!
" + slack_link, 200) 102 | 103 | 104 | # Start our webserver 105 | app.run(port=3000) 106 | --------------------------------------------------------------------------------