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

Phoebe Phuong Nguyen
4 min readOct 6, 2020

The final version of heybot is finally here. To pull the last piece of this project (request time off function and return a receipt), new tools I had to use were Threading, calendar, datetime modules.

My full code is available here.

The Roadmap

>> Project Overview

>> Part 1: Hello World version

>> Part 2: Hello Galaxy version

The journey completed

Demo

Three services that heybot delivers:

“Check Time Off Balance” clicked
‘Request time off” clicked
‘Get time off policies” clicked

Objectives

In this version, my goals are:

  1. When I send my greetings, heybot replies with action options (buttons): Check Time Off Balance, Request Time Off, Get Time Off Policies.
  2. When Check Time Off Balance, Get Time Off Policies are clicked, a modal appears to collect Employee ID.
  3. When Request Time Off is clicked, a modal appears to collect inputs for a time-off request.
  4. heybot returns answers.

Challenges and Solutions

Asynchronous response (using Threading)

According to Slack API documents, Modal view interaction must respond and return to close the modal within 3 seconds. It was challenging to follow this requirement in the case of Request Time Off. Because it takes longer than 3 seconds to send the request to BambooHR and receive the answer; so, when the answer comes back to the user, it’s time out for my server to respond to close the modal.

Timeout to close the modal.

This error happens because the request handler runs synchronously. This means the time_off_request’s call to BambooHR blocks the rest of the execution.

To solve this problem, I need to run the time_off_request function independently (in a separated thread) from the main thread.

Before using Threading

This block is in heybot_v3.py

@ app.route('/slack/request_handler', methods=['POST', 'GET'])
def request_handler():
# some code

elif payload_type == "view_submission":
# some code
elif callback_id == "inputs_request_timeoff_modal" and
action_id == "time_off_request":
# start requesting
response = bamboohr.time_off_request(employee_id,
start_date, end_date, amount, timeOffTypeId, note)

# collect receipt
if response == 'requested':
amount_in_days = amount
receipt = bamboohr.get_request_receipt(employee_id,
amount_in_days)

# prepare receipt block message
blocks = msg.answer_time_off_request(receipt)

# reply user
slack_web_client.chat_postMessage(channel=channel_id,
blocks=blocks)
# close modal view immediately when user clicked Submit
# compulsory return an empty HTTP 200 response
# --Note: This will close the current view only. To close
all view, must return ({"response_action": "clear"})

return ({})
else:
print("ERROR: Wrong payload type")
return ({"ok": 200})
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True, port=5000)

After using Threading

import threading@ app.route('/slack/request_handler', methods=['POST', 'GET'])def request_handler():
# some code

elif payload_type == "view_submission":
# some code
elif callback_id == "inputs_request_timeoff_modal" and
action_id == "time_off_request":
# create a Thread object
t = threading.Thread(target=thread_time_off_request,
args=[channel_id, employee_id, start_date, end_date,
amount, timeOffTypeId, note])
# run a thread
t.start()
else:
print("ERROR: Wrong payload type")
return ({"ok": 200})
def thread_time_off_request(channel_id, employee_id, start_date, end_date, amount, timeOffTypeId, note):response = response = bamboohr.time_off_request(employee_id,
start_date, end_date, amount, timeOffTypeId, note)
# collect receipt
if response == 'requested':
amount_in_days = amount
receipt = bamboohr.get_request_receipt(employee_id,
amount_in_days)

# prepare receipt block message
blocks = msg.answer_time_off_request(receipt)
# reply user
slack_web_client.chat_postMessage(channel=channel_id,
blocks=blocks)
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True, port=5000)

Finding the last day of the current month using calendar.monthrange()

To prepare a receipt for the user, I needed to send a GET request to BambooHR. One of the information I need in the request’s body is the time range.

calendar and datetime Python built-in libraries don’t have a direct solution, but with a bit of research, you can quickly build your small tool with the help of monthrange().

Luckily, there was a ready-to-use and elegant solution from here.

import calendar
import datetime
class Bamboohr:def get_request_receipt(self, employee_id, amount):
amount_in_days = amount
start = datetime.date.today()
end = self.get_month_day_range(start)
url =
f"https://api.bamboohr.com/api/gateway.php/
{self.companyDomain}/v1/time_off/requests/"
querystring = {
"employeeId": f"{employee_id}",
"start": f"{start}",
"end": f"{end}", "status": "requested"}

headers = {
"accept": "application/json",
"authorization": f"Basic {self.AUTHORIZATION_TOKEN}"}
response = requests.request("GET", url, headers=headers,
params=querystring)
response_text = json.loads(response.text)receipt = {
"id": response_text[-1]["id"],
"type": response_text[-1]["type"]["name"],
"amount_in_days": amount_in_days,
"amount_in_hours": response_text[-1]["amount"]["amount"],
"start": response_text[-1]["start"],
"end": response_text[-1]["end"]}
return receiptdef get_month_day_range(self, date):# For a date 'date' returns the start
and end date for the month of 'date'.

# first_day = date.replace(day=1)
last_day = date.replace(day=calendar.monthrange(date.year,
date.month)[1])
return last_day

p/s: you can find the first day of the month by uncommenting this line

first_day = date.replace(day=1)

Thanks for reading!

Phoebe

--

--

Phoebe Phuong Nguyen

Software Engineer with strong HRIS expertise. love surfing and playing beach volleyball when I don’t code!