DEV Community

Cover image for How I’m learning about APIs by building a Slackbot — Part 2
Phoebe
Phoebe

Posted on

How I’m learning about APIs by building a Slackbot — Part 2

Continuing on my HelloWorld version, I completed my HelloGalaxy version. This journal article is to share the development process of heybot’s HelloGalaxy version, and one of my favourite challenges which there was no official documentation and I figured out my own solution (using private_metadata).

My full code is available here.


The Roadmap

>>Project Overview
>> Part 1: Hello World version
>> Part 3: Final version (coming up next)

Recalling from my original roadmap, this is where we stand.
Alt Text

Demo

Alt Text

Objectives

In this version, my goals were:

  1. To connect to BambooHR API by creating a trial account and Token key.
  2. When I say “Hello”, heybot replies with action options (buttons).
  3. When I click on a button to choose the service, heybot askes for employee ID via a Modal. Heybot returns answers to me.

Resources

BambooHR API

The Documentation is clear and well organized; APIs are grouped by activities (Report, Login, Employees).
I wanted to build heybot for one single customer, so all I needed was (1) an account (NOTE: a trial account is valid in 7 days only), (2) subdomain is “mycompany” which I created when I opened my trial account (My company is monsterinc :D), (3) an API key which I made from my trial account “Settings”.
How I create an API key:
How I create an API key
I recommend copying the API key and saving it in a note because we have to use it frequently.
One more important thing that you should note is the AUTHORIZATION_TOKEN ("Basic NDJim******** *) in *headers.

Why do we need this?

We can't use the API key to send an HTTP Request to BambooHR directly. An API key will have to be converted into an authorization token.
Before running the Flask app, we have to "export AUTHORIZATION_TOKEN= "NDJim**********" to be an environment variable (similar to what I did to SLACK_TOKEN and SLACK_SIGNING_SECRET which I explained in Step 3.1 here).

How to use BambooHR API Key to find AUTHORIZATION TOKEN?

Go to the API Reference page and choose an API and "Try It".

Alt Text

Challenges and Solutions

Find the right documentations on Slack API

Slack has been upgrading its API, and because of that, some features or methods were either discontinued or were still in use, but their support resources were deprecated.
In HelloGalaxy version, I needed to build:

  1. rich-text messages containing buttons/ menu/ date picker
  2. a modal. For #1, Slack introduced Blocks (supported by Block Kit Builder to replace attachments according to this post. The purpose of this upgrade is to support more interactive features (vote, buttons, menu) or rich contents (video, link, image).

This is my action message built with Blocks and sent to the user by chat_postMessage() method.
Alt Text

You can see my Block payload here under def understood_greeting(self, user).

For #2, Modal replaced Dialogs according to this post. This change led to the retirement of dialog.open() method and the introduction of views.open() method to open Modal (we can't use chat_postMessage() or other regular methods to open a Modal).
A pop-up modal

Get content from request body with Content-type = application/x-www-form-urlencoded

Normally, working with Content-type = application/JSON is very easy and straightforward. We can use request.data, request.get_data, request.json, request.get_json.

application/x-www-form-urlencoded is a bytestring type file. So, if we use request.data (the common solution), we will get an empty string.
This StackOverflow post is a comprehensive answer. My full answer with details was in here too. Below is my solution for your quick look.

@ app.route('/slack/request_handler', methods=['POST'])
def request_handler():
   # return an ImmutableMultiDict with 1 pair
   payload = request.form 

   # get value of key 'payload'
   a1 = payload['payload']  

   # Note that if you have single quotes as a part
   # of your keys or values, this will fail due to
   # improper character replacement.
   # Convert a string (representation of a Dict))
   # to a Dict
   a2 = json.loads(a1)

   # Get value of key "channel"
   channel = a2["channel"] 
   print(channel) 
   # {'id': 'D01ACC2E8S3', 'name': 'directmessage'}
Enter fullscreen mode Exit fullscreen mode

Using private_metadata to solve view_submission payload's limitations.

NOTE: When a button clicked, it sends back a block_actions payload8 containing channel_id, action_id. When a modal is closed (made submission), a view_submission payload is sent.
I save samples of block_actions payload and view_submission payload here.

Context:

  • A Modal appears to get Employee ID input from a user. Once the user provided the ID and clicked "Submit", a view_submission payload was sent to my server.
  • I used that ID to get the answer from BambooHR.

  • I wanted to reply to the user.

Problem:

To call the correct function to send a GET request to BambooHR and build a right template message, need an action_id which I set when I created the Block message. view_submission payload doesn't have action_id.
In order to reply, a channel_id is a must. view_submission payload doesn't have channel_id.

Solution:

There was no official documentation on this from Slack. However, I utilized private_metadata to store the action_id received from the block_actions payload.
This Slack doc suggested storing channel_id in private_metadata.

Pseudocode

Alt Text

Code

# build view payload
def get_employee_id_modal(self, channel_id, action_id):
    private_metadata = {
       "channel_id": channel_id,
       "action_id": action_id
    }
    # jsonify a dict into a string 
    # (required type for private_metadata)
    json_private_metadata = 
    json.dumps(private_metadata)

    # return a block
    return ({
      "type": "modal",
      "private_metadata": json_private_metadata,
      "callback_id": "employee_id_modal",
      [....]
      })
Enter fullscreen mode Exit fullscreen mode

Thanks for reading!
Phoebe

Top comments (0)