Sprintero — my first slack app: django + drf

Sebastian Opałczyński
7 min readNov 20, 2016
Time to start something new.

Well, first — it really solves problem. Trivial one, but still. Second — I just want to get familiar with Slack Application submit process and Slack API flows. I think that such experience is nice to have as a backend developer. You just want to be able to provide integrations for your sophisticated solutions.

So what Sprintero does?

Basically it allows to get a random name for the scrum sprint — currently it supports names from the Marvel world — I’ve found the data on github from the creators of marvel wikia — I even send them an email — if I can use this data. They agreed.

You will need

  1. Domain
  2. Server hosting
  3. HTTPS
  4. Slack integration / application
  5. Patience
  6. Main page for application with add to slack button
  7. Server code
  8. All the client ids and needed secrets (available when you create a new slack app)
  9. You can test your application in your private slack team with no restrictions
  10. The submit process is a little pain: provide — privacy policy, support information and some command with help (within your application)
  11. Patience

It can sound little bit overwhelming, but I’ve setup it in my free time and spent no more than 20 hours for that — only two weekend nights.

Also the article that I’ve found on medium was very helpful: Creating a Slack Command Bot from Scratch with Node.js & Distribute It. Basically — I just rewrite node to python, and use different logic. That’s it.

The flow

I will try first to provide some high level overview — how the things works. Later I will focus on the details and even provide some code examples.

Slack Command

I decided to use slack command integration. You are probably familiar with it — if you are a Slack user. The Giphy integration is done that way, simply you write in any channel: /giphy phrase and in the response you will get the random picture which match the phrase.

So how does it work under the hood? You can read more about it directly in slack documentation. But simply you add in Slack command configuration panel the URL and choose http method: GET and POST are available. Next — the command is sent to this URL. The response should be well formatted — and it will appears in the use channel. See the picture:

It is nice to start development with slack command (later on you can just copy it to your Slack application). I encourage to use POST method because Slack application do not allow to use GET — opposed to just command integration.

So if you have this setup — you will definitely need some server that can handle this requests. I’ve decided to use here django + drf (django-rest-framework), I am quite familiar with this combination. I’ve also used the requests package — to make the oauth flow. Will share the server code at the bottom of the post.

Another important note — you will need the HTTPS — I’ve used the free Cloud Flare account for that. I think it is very fast and reliable way to achieve https communication with your server.

And the domain — Yes, I buy a new one :)

Your server handler

Slack will send you something like this to your URL:

{
"token": "gIkuvaNzQIHg97ATvDxqgjtO",
"team_id": "T0001",
"team_domain": "example",
"channel_id": "C2147483705",
"channel_name": "test",
"user_id": "U2147483697",
"user_name": "Steve",
"command": "/sprintero",
"text": "wellknown badass",
"response_url": "https://hooks.slack.com/commands/1234/5678"
}

The token - will be special for your application or integration. Also the other fields will be different, but the most important ones are: command, text and response_url. Base on command, text you make some choices of how your server handler should behave and the response_url can be used to send additional information (except the standard response) to the application user.

So how sprintero response look like?

{ 
"attachments": [
{
"fallback": name,
"pretext": "Your sprint name is",
"title": name,
"title_link": url,
"text": "{}. {}.".format( "Alive" if serialized_data["alive"] =="Living Characters" else "Dead", serialized_data["align"], ),
"color": "#7CD197"
}
]
}

And this displays as follows in the Slack channel:

For more sophisticated formatting you should definitely read: Slack message formatting documentation.

And basically this is it :) Only the details left — and we all know that details make real difference.

The Details

First things first — the repository with a server code can be found here: github.com/trurl-it/sprintero_api. I will show now the most important pieces of the code. I’ve tried to write high quality code — but this is the first iteration and for sure it can be improved — any hints apprieciated.

The Slack handler view

# sprintero_api/rest/views.pyclass SlackPOSTView(APIView):

def post(self, request, *args, **kwargs):
serializer = SlackDataSerializer(data=request.data)
if not serializer.is_valid():
Response(status=status.HTTP_400_BAD_REQUEST)
# check token
if serializer.data.get('token') != settings.SLACK_TOKEN:
return Response(status=status.HTTP_401_UNAUTHORIZED)

# check command - only one supported now
if serializer.data.get('command') not in settings.SUPPORTED_SLACK_COMMANDS:
return Response(status=status.HTTP_400_BAD_REQUEST)

command_text = serializer.data.get('text', [])

if 'help' in command_text:
return Response(
{
"text": "To use sprintero write (it generates for you sprint names):\n"
"/sprintero - will give you good character name from over 8k names in database\n"
"/sprintero wellknown - will give you wellknown and good character name\n"
"/sprintero badass - will give you bad character name\n"
"/sprintero wellknown badass - will give you a well known bad character name\n"
},
status=status.HTTP_200_OK
)

wellknown = False
if 'wellknown' in command_text:
wellknown = True

badass = False
if 'badass' in command_text:
# add support for badass characters;
badass = True

random_marvel_character = get_random_character(badass, wellknown)
if random_marvel_character.count() >= 1:
serialized_data = MarvelSerializer(random_marvel_character[0]).data
data = SlackFormatter().format(serialized_data)
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(status=status.HTTP_404_NOT_FOUND)

And that is it. I will describe in few words what is happening above:

  1. The post handler is defined — it’s is mapped in the settings.urls
  2. Handler serialize the data that comes from Slack — SlackDataSerializer
  3. Check if serializer data is valid
  4. Check if token is valid
  5. Check if server can recognize the command
  6. Check if some special words are present in the command text
  7. help is handled separately - the usage example is returned in this case, with simple text formatting.
  8. Check another special words — wellknown, badass in this case
  9. Set up the bool values — which will control the flow
  10. Pick random name
  11. Return response to slack

Lets take a closer look to the SlackDataSerializer:

# sprintero_api/rest/serializers.py

class SlackDataSerializer(serializers.Serializer):
token = serializers.CharField()
team_id = serializers.CharField()
team_domain = serializers.CharField()
channel_id = serializers.CharField()
channel_name = serializers.CharField()
user_id = serializers.CharField()
user_name = serializers.CharField()
command = serializers.CharField()
text = serializers.CharField(required=False)
response_url = serializers.CharField()

Also the SlackFormatter code can be useful here:

# sprintero_api/rest/slack_formatting.py

class SlackFormatter(object):

def format(self, serialized_data):
name = serialized_data['name'].replace('(Earth-616)', '').strip()
url = urljoin(settings.MARVEL_WIKIA, serialized_data['url_slug'].replace('\\', ''))
return {
"attachments": [
{
"fallback": name,
"pretext": "Your sprint name is",
"title": name,
"title_link": url,
"text": "{}. {}.".format(
"Alive" if serialized_data["alive"] == "Living Characters" else "Dead",
serialized_data["align"],
),
"color": "#7CD197"
}
]
}

What I am not proud of — is the command parsing. Basically I am checking if command is present in Slack text field. And based on that make some simple logic control.

The oAuth

This part is required if you want to place your application in the Slack App directory. And I must say that this part is most frustrating one :) Basically — this is just a some extra work you should and this is not the part of the application logic.

To setup the oAuth — I will suggest using the add to slack button - in this scenario some steps in full oAuth can be omitted.

You will need for this part — a page hosted somewhere, on this page you should include a button, this is how it look on my page:

<a href="https://slack.com/oauth/authorize?scope=commands+team%3Aread&client_id=YOUR_CLIENT_ID"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>

When user click this button it will be redirected for oAuth to URL defined in oAuth section of your Slack Application. Here (this is the screen from different app — just to make this clear):

The oAuth server handler

The handler code look as follow:

# sprintero_api/rest/views.py

class SlackOauthView(APIView):

def get(self, request, *args, **kwargs):
data = {
'client_id': settings.SLACK_CLIENT_ID,
'client_secret': settings.SLACK_SECRET,
'code': request.query_params.get('code')
}

response = requests.post('https://slack.com/api/oauth.access', data=data)
json_response = response.json()
if response.status_code == status.HTTP_200_OK and json_response['ok']:
token = response.json().get('access_token')
# ask about team name:
response = requests.post('https://slack.com/api/team.info', data={
'token': token
})
json_response = response.json()
if json_response:
team_domain = json_response.get('team', {}).get('domain')
if team_domain:
return HttpResponseRedirect('https://{}.slack.com'.format(team_domain))

return Response(status=status.HTTP_400_BAD_REQUEST)

The flow:

  1. Create data for POST request to slack oauth.access endpoint; The SLACK_CLIENT_ID and SLACK_SECRET can be found in the Basic information section in your Slack application page.
  2. The code parameter will be send to you from slack (after user login in his slack account - using your client_id - the one specified in the button)
  3. In the response you will get the access_token - and basically you can stop at this step. But it is nice to redirect user to his Slack team page.
  4. Make a request for the team.info endpoint: NOTE: you will need team.read scope for that. The commands scope is self-explanatory.
  5. Get the team name.
  6. Redirect user to his Slack team page.

The Server

I set up django with uwsgi and used nginx as a load balancer. I will share the configuration files, maybe you will find it useful somehow.

nginx

# sprintero.conf

upstream sprintero_app {
server unix://home/ubuntu/socks/app.sock;
}

server {
listen 80;
server_name sprintero.tech;
charset utf-8;

# max upload size

# All web-api requests redirect to uwsgi
location / {
uwsgi_pass sprintero_app;
include uwsgi_params;
}

location /static {
alias /home/ubuntu/sprintero_api/static;
autoindex off;
}

uwsgi

uid=www-data  
chdir=/home/ubuntu/sprintero_api/
module=sprintero_api.wsgi:application
master=True
pidfile=/home/ubuntu/socks/app.pid
socket=/home/ubuntu/socks/app.sock
vacuum=True
max-requests=5000
processes=2
threads=2
touch-reload=/home/ubuntu/app_touch.ini
daemonize=/home/ubuntu/uwsgi_sprintero.log

That’s it. If you have any questions — please leave me a note or send an email to: [email protected]

Also I’ve submitted this app to the Slack application directory — should have an answer within two weeks — hope that answer will be positive and anyone can use it.

Originally published at showpy.tech on November 20, 2016.

--

--

Sebastian Opałczyński

Software engineer, technology enthusiast, common sense lover, serial co-founder, and a father of three.