Sprintero — my first slack app: django + drf
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
- Domain
- Server hosting
- HTTPS
- Slack integration / application
- Patience
- Main page for application with
add to slack
button - Server code
- All the client ids and needed secrets (available when you create a new slack app)
- You can test your application in your private slack team with no restrictions
- The submit process is a little pain: provide — privacy policy, support information and some command with help (within your application)
- 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:
- The post handler is defined — it’s is mapped in the
settings.urls
- Handler serialize the data that comes from Slack —
SlackDataSerializer
- Check if serializer data is valid
- Check if token is valid
- Check if server can recognize the command
- Check if some special words are present in the command
text
help
is handled separately - the usage example is returned in this case, with simple text formatting.- Check another special words —
wellknown, badass
in this case - Set up the bool values — which will control the flow
- Pick random name
- 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:
- Create data for POST request to slack
oauth.access
endpoint; TheSLACK_CLIENT_ID
andSLACK_SECRET
can be found in theBasic information
section in your Slack application page. - The
code
parameter will be send to you from slack (after user login in his slack account - using yourclient_id
- the one specified in the button) - 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. - Make a request for the
team.info
endpoint: NOTE: you will needteam.read
scope for that. Thecommands
scope is self-explanatory. - Get the team name.
- 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.