Adding App Home to Slack app in Python.

November 8, 2019. Filed under python 56 slack 6 api 3

Building on Creating a Slack App in Python on GCP, I wanted to continue extending reflect-slack-app to include an App Home.

The App Home screen allows each user in a workspace to have a personalized view powered by the app, which the app can use to share whatever data or interactions (buttons, etc) they want. Want to let folks configure their settings for your app? You can! Want to let folks see their most recent activity? You can! Want to let users know the current status on all open projects? You can!

The functionality is in Beta and before I started playing around with the api, I hadn't actually realized this functionaltiy existed, although I'd been wanting it to exist. For me, the application home concept is particularly interesting because it's a great mechanism to (a) summarize information for users, and (b) improve discoverability of app functionality.

It also represents a meaningful step from chatbot and Slash Command driven interactions to a richer, more web-like experience. Alright, onward into the integration.


post starts at commit 5359, and ends at commit 4120


Slack app in Python series

  1. Creating a Slack App in Python on GCP
  2. Adding App Home to Slack app in Python
  3. Make Slack app respond to reacji
  4. Using Cloud Firestore to power a Slack app
  5. Distributing your Slack application

Sign up for App Home

The first step is signing up the App Home functionality in your App dashboard.

Sign up for App Home

Because this functionality is still in Beta, there is a disclaimer warning you that it might change. Accept it you must.

Beta disclaimer when signing up for App Home

App Home requires a bot user, but we're not going to be using the bot user for any chatbot functionality and neither will we expect users to send messages to the bot (they'll keep using the /reflect and /recall Slash Commands instead), so go ahead and disable the "Messages Tab" to avoid confusion.

Disabling Messages Tab in Slack App Home

Now that we've setup an App Home, how do we actually render it?

Register for app_home_opened

We need register for the app_home_opened event from the Events API. That means we need to integrate with the Event APIs before going any further.

Enable Events screen in Slack

The first step is enabling Events API in your App dashboard.

Add Request URL for Events API screen in Slack

Once the Events API is enabled, the next step is setting up a Request URL. That in turn requires setting up a handler for the url_verification event. When we add our URL, they'll send over a message encoded in application/json (unlike the Slash Commands, which are application/x-www-form-urlencoded).

{
  "token": "random token",
  "challenge": "challenge to return",
  "type": "url_verification"
}

Within that message is a challenge key-value pair, and we want to return that value within a JSON message.

{
  "challenge": "challenge to return"
}

Kicking off our integration, it's time to pop over to the lethain/reflect-slack-app repository, starting from commit 3aa6, and opening up reflect/main.py to add a new function:

def event_post(request):
    signing_secret = os.environ['SLACK_SIGN_SECRET'].encode('utf-8')
    verify(request, signing_secret)

    parsed = request.json
    challenge = parsed['challenge']
    return jsonify({"challenge": challenge})

Notice that we're using verify to verify the request's authenticity. Verifying request signatures is the "modern" way to verify messages from Slack. There is another older method using the token parameter sent by url_verification. Using that deprecated approach, you'd capture the supplied token (random token in the above example) and verify all subsequent requests include that token. Since we're verifying request signatures, it's safe to ignore the token.

Now we need to create a new Cloud Function serving the event_post function.

> gcloud functions deploy event_post \
–env-vars-file env.yaml \
 –runtime python37 \
–trigger-http

Allow unauthenticated invocations of new function [event_post]? (y/N)?
> y
Deploying function (may take a while - up to 2 minutes)...

This will return a URL along the lines of:

https://your-app-here.cloudfunctions.net/event_post

Which we add to Events Subscriptions tab in the App dashboard.

Register request url for Slack Event Subscription

When you paste in your URL, Slack will automatically verify it and either display "Verified" above or show some super helpful debugging information below, including your endpoint's unexpected response.

Once your URL is verified, open up Subscribe to bot events and subscribe to the app_home_opened event.

Register for app_home_opened bot event

Finally, you must click Save Changes at the bottom. It's a bit easy to miss, but you'll have to redo these steps if you accidentally close the tab without saving.

Reinstall app

Prompt to reinstall for new permissions

Now that we've enabled the App Home and Event Subscriptions, we need to reinstall our application to request these additional permissions.

Reinstall with new permissions

And now something amazing has happened, your application's App Home is available now if you go back to the workspace where it's installed and search for the application's name.

Search result for Slack application home

So click on it and let's get started.

Routing events from Events API

The first time you load your App Home is a bit sad, showing a "work in progress" screen. We're going to fix that, eventually, but it's going to require a few steps.

Search result for Slack application home

First we need to handle the app_home_opened event that is sent each time a user visits their App Home for your application. The Events API has two layers of event types, so the requests that event_post will receive are structured like this:

{
  "type": "event_callback",
  "event": {
    "type": "app_home_opened",
    "other": "stuff"
  },
  "other": "stuff"
}

Knowing the format, we can refactor event_post a bit to split up handling different message types and event types.

def event_post(request):
    signing_secret = os.environ['SLACK_SIGN_SECRET'].encode('utf-8')
    verify(request, signing_secret)
    parsed = request.json
    event_type = parsed['type']
    if event_type == 'url_verification':
        return url_verification_event(request, parsed)
    if event_type == 'event_callback':
        return event_callback_event(request, parsed)
    else:
        raise Exception("unable to handle event type: %s" % (event_type,))

def url_verification_event(request, parsed):
    challenge = parsed['challenge']
    return jsonify({"challenge": challenge})

def event_callback_event(request, parsed):
    event_type = parsed['event']['type']
    if event_type == 'app_home_opened':
        return app_home_opened_event(request, parsed)
    else:
        raise Exception("unable to handle event_callback event type: %s" \
                        % (event_type,))

def app_home_opened_event(request, parsed):
    print(parsed)
    return "to be implemented"

If we deploy this updated version and tab away from and back to our App Home, then our logs include a complete event message for app_home_opened:

{
  'token': 'token-token-token',
  'team_id': 'tttttttttt',
  'api_app_id': 'aaaaaaaa',
  'event': {
    'type': 'app_home_opened',
    'user': 'uuuuuuuuuu',
    'channel': 'ccccccccc',
    'tab': 'home'
  },
  'type': 'event_callback',
  'event_id': 'eeeeeeeeee',
  'event_time': 1500000000
}

Now, you might dream that we just need to update our app_home_opened_event function to return some Block Kit elements to have them render, but it's not quite that simple. Instead our handler will need to integrate with one of the Slack Web APIs, views.publish.

Calling Web APIs

Slack's Web API is a pretty typical HTTP API. In our case, we need to send a POST request to views.publish at

https://slack.com/api/views.publish

We'll be sending application/json request bodies which look like:

{
  "user_id": "uuuuuuuuuuu",
  "view": {
    "type": "home",
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "To be implemented"
        }
      }
    ]
  }
}

Each of those blocks conforms to the format we previously used to render responses to Slash Commands.

We'll also need to include our bot's token, which you can find in the OAuth & Permissions tab of your app dashboard, as an HTTP header:

Authorization: Bearer xoxp-xxxxxxxxx-xxxx

We'll start by extending reflect/env.yaml to include that token:

SLACK_SIGN_SECRET: your-signing-secret-here
SLACK_BOT_TOKEN: your-bot-token

We're going to send requests to Slack's APIs using Python's requests library. The first step towards that enviable goal is adding requests/requirements.txt to our repo so Cloud Functions is aware of our dependency:

requests==2.20.0

We'll also want to install that locally for any local validation we do into the virtual environment we created last time:

source ../venv/bin/activate
pip install requests==2.20.0

Then we can write a utility in reflect/main.py to perform requests:

import requests

def slack_api(endpoint, msg):
    url = "https://slack.com/api/%s" % (endpoint,)
    bot_token = os.environ['SLACK_BOT_TOKEN'].encode('utf-8')
    headers = {
        "Authorization": "Bearer %s" % (bot_token.decode('utf-8'),),
        "Content-Type": "application/json; charset=utf-8",
    }
    resp = requests.post(url, json=msg, headers=headers)
    if resp.status_code != 200:
        raise Exception("Error calling slack api (%s): %s" % \
                        (resp.status_code, resp.content))
    return resp.json()

There are a few details within slack_api worth mentioning. First, we're setting the Authorization header to verify our identy, and requests will fail if it's not properly set. Second, the Slack API considers it a warning if you don't specify character set, so we're ensuring that Content-Type specifies one.

Rendering our App Home

Now that we have this utility, we'll call it along the lines of:

msg = {'some':' stuff'}
slack_api("views.publish", msg)

We can finally do something useful when we receive the app_home_opened event and have it render roughly the same content as we return through the /reflect Slash Command:

def app_home_opened_event(request, parsed):
    user_id = parsed['event']['user']
    team_id = parsed['team_id']
    items = recall(team_id, user_id, "last 14 days")
    items_text = "\n".join(["%s. %s" % (i, x) \
                            for i, x in enumerate(items, 1)])
    blocks_spec = [
        ('mrkdwn', "Your home tab for Reflect"),
        ('divider',),
        ('mrkdwn', items_text),
        ('divider',),
        ('mrkdwn', "Some more stuff here"),
    ]
    blocks = [block(*x) for x in blocks_spec]
    msg = {
        "user_id": user_id,
        "view": {
            "type": "home",
            "blocks": blocks,
        }
    }
    resp = slack_api("views.publish", msg)
    return "OK"

If we deploy that updated code and then reload our App Home tab, then something pretty great happens...

Working App Home for Reflect demo app

...it actually works!

Making App Home useful

What we've done here is quite basic, but you could imagine going much further with the concept. This could be used to configure defaults how many days of results are returned by /recall, it could be used to create an export-to-email widget, it could show the number of tasks you've recorded for the trailing three months, etc.

You could even imagine it injecting a personalized image with a histogram of your projects by tag or some such, although I'm not quite sure what the authorization story would be for loading user-specific generated images, and looking at the image block spec I suspect there isn't one quite yet.

In the context of the toy Reflect application, I suspect the most important functionality would be showing a summary of recently added tasks and examples of how to use the various Slash Commands to aid with discoverability.

Next

The goals carried over from the first post on this project were (1) getting the App Home set up, (2) letting users add accomplishments via reacji, (3) integrating an actual database, and (4) publishing into the application directory.

As we reach commit 4120 in lethain/reflect-slack-app, we have a simple but functional App Home, letting us scratch one more item off that list. We've also integrated the Events API, so I suspect the reacji integration will be pretty straightforward, which is what we'll do next.


series continues in Have Slack app respond to reacji