Mr Tea: a tea break session planning bot using Slack and Google Cloud Platform

Updates

12/05/2020: Thanks to S.Tietz for his recommendations to grouping and spotting mistakes in this post.

Since the COVID-19 lockdown came into force, we’ve been working on keeping the Data Science Campus team community going, looking after our wellbeing and making sure we’re all generally ok. And some of that involves novel and fun ways of making working-from-home feel less like working-on-your-own.

As well as the usual slack channels filled with pictures of pets, the outside world, and children’s first steps, we’ve been reinventing tea breaks. Here, we explain how this has developed into an automated system that could be used elsewhere. Our code repository for this can be found here ⧉..

The tea break

Each Tuesday and Thursday at 14:00, members of the Campus dial into a meeting to have a chat with colleagues whilst sipping a cup of tea. The novelty about these tea break sessions is that the meeting comprises four-to-five randomly assigned colleagues. The benefits to this are two-fold. Firstly, this allows the sessions to have a group size sufficiently small enough for all attendees to engage in the conversations. Secondly, it allows for a rotation of colleagues which one chats to. So far, we have seen a real positive uptake of these sessions.

pexels image of a laptop and tea mug on a table

Source: pexels

Approach

The initial approach was to have a dedicated Slack channel. In the morning, users would then say whether they were available for the afternoons tea break. From this, the list of attendees could be randomised into small groups. To achieve this, we created a simple python script. A list of meeting links could be created to host the tea breaks sessions. It’s up to you which blend of meeting hosting applications you choose here. For ease, we created a number of recurring meetings every Tuesday and Thursday at 14:00 so that the meeting links do not change.

Ok, so 20-minutes work every Tuesday and Thursday isn’t exactly hard-graft, but we always wanted to make this more automated so it didn’t rely on one person to be the tea administrator.

Therefore, we created Mr Tea.

a gif of thee mr tea logo

Source: Data Science Campus

You created what?

Mr Tea is a Slack Bot who asks a simple question…

mr tea asking a question in a channel

Source: Data Science Campus

Keen tea enthusiasts then give the post a thumbs up if they want to participate. We then use the list of people who gave a thumbs up to create groups for the tea break.

mr tea posting thee group results to a channel

Source: Data Science Campus

Simple!

Ok, but, really how?

Ok, so I may have skipped a few steps there on how Mr Tea actually works. Essentially it boils down to three steps:

  1. create the Slack App
  2. post and gather responses with the Slack App using python
  3. deploy to Google Cloud Platform

Step 1. Create the Slack Bot

The first of you need to create a Slack App and set the appropriate Scopes (minimum: channels:history, channels:read, incoming-webhook, users:read).

For posting messages to a Slack channel, we use Slack webhooks. For getting the responses back from the channel we use an OAuth Access Token.

To use the App, you need to both install it to the desired channel (done in the App settings page), as well as add to the channel (done in the channel, like you would a real user: @app-name).

Now your App (bot) is ready for action.

Step 2. Post and gather responses with the Slack App using python

Both posting and retrieving messages from Slack use curl to send a HTTP request. This can be done in your terminal, but we are going to use python here. For this we choose to use the requests library.

Step 2.1. Posting your question in the channel

Posting a question is nice and simple. For this you need the following:

  • your webhook_url
  • your message

Then, using requests and json libraries we can make a request to the webhook.

#!/usr/bin/env python
import requests
import json


""" Your Slack webhook """
slack_webhook = 'slack_webhook'

""" Question message """
slack_question_message = "Morning all, give this post a thumbs up if you are available for a tea break this afternoon!"

def ask_question():
	""" Asks the question to the Slack channel """
	headers = {'Content-type': 'application/json'}
	payload = {'text': slack_question_message}
	payload = json.dumps(payload)
	requests.post(slack_webhook, data=payload, headers=headers)

Calling ask_question() will then post the message to the channel you added the App to.

Step 2.2. Creating the groups and posting them in the channel

This is a little harder, but not too difficult. We will break it down into chunks.

First, you need the following python libraries:

#!/usr/bin/env python
import requests
import random as rnd
import math
import json

Then, the first thing to do is to setup all the preamble variables we will need.

""" Post your meeting links below """
meeting_links = \
	[
	'link_1',
	'link_2',
	'etc'
	]

""" Your Slack webhook """
slack_webhook = 'slack_webhook'

""" Your Slack token """
slack_token = 'slack_token'

""" Slack channel ID """
slack_channel_id = 'slack_channel_ID'

""" Slack bot ID """
slack_bot_id = 'slack_bot_ID'

""" Question message """
slack_question_message = "Morning all, give this post a thumbs up if you are available for a tea break this afternoon!"

""" Requested group size (note, the algorithm may use +1 if ir provides a better split of people """
group_size = 4

Next, we want to get the posts from the channel that relate to our Slack App.

def get_posts():
	""" Gets posts from the channel by the bot and returns as JSON object """
	url = f"https://slack.com/api/channels.history?token={slack_token}&channel={slack_channel_id}&bot_id={slack_bot_id}"
	return requests.get(url).json()

Let’s call posts = get_posts() to get these posts. Ok, now we have all the Apps posts in that channel. But what we really want is the most recent post - the post to check the availability of people for the tea break. Therefore, let’s filter the results:

def get_recent_question(posts):
	""" Gets the most recent question post from the channel by the bot """
	latest_poll = {}
	for post in posts['messages']:
		if post.get('text') == f"{slack_question_message}":
			latest_poll = post
			break
	return latest_poll

Calling poll = get_recent_question(posts) will then bring back that specific post. Next, we want to get a list of people who gave the post a thumbs up. So we request this from the post using:

def get_attendees_ids(poll):
	""" Gets a list of attendees slack IDs """
	attendees_ids = []
	reactions = poll.get('reactions')
	for reaction in reactions:
		if reaction.get('name') == "+1":
			attendees_ids.append(reaction.get('users'))
	return attendees_ids[0]

Great, we call attendees_ids = get_attendees_ids(poll) and we get a list of those who gave a thumbs up. Slack allows us to tag people using their ID, so let’s do that. Next, let’s allocate these IDs to a group. There are to catches here that increase the group size if needed. First, if the split of attendees is better if group size is increased by one. Second, if there are too many attendees for the number of meeting links (this will increase the group size to compensate).

def allocate(ids):
	""" Allocates ids to groups """
	rnd.shuffle(ids)

	result = []

	# increase groupsize by one if the groups are more evenly split by doing so
	groupsize = group_size if abs(((len(ids) / group_size) % 1) - 0.5) >= abs(((len(ids) / (group_size + 1)) % 1) - 0.5) else group_size + 1

	# increase groupsize if we don't have enough meeting links
	groupsize = max(groupsize, math.ceil(len(ids) / len(meeting_links)))

	while True:
		# Make sure no group has less than two people
		if len(ids) > groupsize + 2:
			result.append(ids[0:groupsize])
			ids = ids[groupsize:]
		else:
			result.append(ids[0:])
			break

	return result

Calling groups = allocate(attendees_ids) we then are returned with groups of random slack IDs. Next, let’s create a nice message we can get the App to post to our desired Slack channel.

def present(groups, meeting_links):
	""" Create post for slack channel """
	msg = []
	msg.append("Good afternoon everyone, below are this afternoons tea break groups")
	num = 0
	for i, group in enumerate(groups):
		msg.append("--------------------------------------------------------------------------------")
		msg.append(f"Group {i + 1}: ")
		msg.append(meeting_links[num])
		msg.append("--------------------------------------------------------------------------------")
		for member in group:
			msg.append(f"<@{member}>")
		num = num + 1

	return '\n'.join(map(str, msg))

Calling msg = present(groups, meeting_links) returns this string by using the groups we just created, and Skype links we posted at the start. f"<@{member}>" tags the member in the post. The last thing to do therefore is to ask the App to post to the channel.

def post_to_slack(msg):
	""" Post groups to slack channel """
	headers = {'Content-type': 'application/json'}
	payload = {'text': msg}
	payload = json.dumps(payload)
	requests.post(slack_webhook, data=payload, headers=headers)

At this point you could call post_to_slack(msg) to fire off the message to check everything works.

Ok great. So how to we automate this so someone doesn’t have to run the python scripts? Well, for this we are going to use Google Cloud Platform’s Cloud Function and Cloud Scheduler features.

Step 3. Deploy to Google Cloud Platform

Step 3.1. Cloud Function

Cloud Function is what does the bulk of our work here. It processes our two requests: post the original message, and post the groups, to the Slack channel. If you’ve read our other posts you’ll see we have used Cloud Function and Scheduler before, so we won’t go into detail here on how to set a function up. The fundamentals we need here are:

  • a Pub/Sub trigger (so we can use Cloud Scheduler)
  • python 3.7 inline editor

This isn’t a memory expensive process, so you have stick to 256 Mb option. The ask_question function can be used as is, just remember to put data, context in the function’s header (e.g., ask_question(data, context)).

For the group posting functions, copy all the parts above (or see our GitHub repository with all the code in) into the editor. Then, we need just one more process function, as Cloud Functions only runs one, single function.

def process(event, context):
	""" Pipeline process required for Cloud Function """
	users = get_users()
	posts = get_posts()
	poll = get_recent_question(posts)
	attendees_ids = get_attendees_ids(poll)
	attendees_names = get_attendees_names(attendees_ids, users)
	groups = allocate(attendees_names)
	msg = present(groups, meeting_links)
	post_to_slack(msg)

Calling post_groups will run the other functions in the chain. It’ss probably good at this stage to test both of these out and see if they work.

Step 3.2. Cloud Scheduler

Cloud Scheduler runs our functions at a time that suits us using cron. For the ask_question function we want this to run every Tuesday and Thursday at 09:00 so we set the frequency to 0 9 * * 2,4. For the post_groups function we want this to post on the same dates but at 13:00, so we set the frequency to 0 13 * * 2,4. We can test whether these work by clicking RUN NOW.

Summary

If everything has worked out well you will now have setup a nice Slack App (bot) that can ask your Slack channel users who is available for a tea break session. It will then use the responses to create the randomised tea break session groups and post to the Slack channel.

Feel free to add a nice photo to the bot in its settings, or make the channel posts a little nicer. Or, put your feat up and have a nice cup of tea.


Michael Hodge

By

Senior Data Scientist at ONS Data Science Campus.

Updated