When we started using Slack in our small team I was sold on the spot, and I particularly enjoy being able to build integrations which, well, integrate seamlessly into Slack: type a command into a Slack channel, and get a response right there, without leaving Slack.

I was thinking about how to go about getting a bit of our OwnTracks into Slack. Wouldn’t it be neat if you could tell with a single command where a team mate is? Sure, we could glance at a map or use one of the other utilities that the OwnTracks project provides, but let’s see if we can do better.

Slack command in action

If you’ve been following a bit of what we do with OwnTracks, you’ll know privacy is a big word for us, and as such we recommend you use your own MQTT broker for OwnTracks. I have a dozen friends and family members using my broker with carefully designed ACLs so that only consenting parties can see each others’ location. I don’t want to open up MQTT publishes from my broker to the rest of the world, but I want my Slack team to be able to determine where I am so they know if I can be disturbed. (I trust these guys.)

OwnTracks maps

What I’m going to show you is how we can use a combination of OwnTracks, mqttwarn (which I introduced here), and a Slack slash command to produce a useful utility you can use in Slack as well. (If you’re more of an IRC user, I’m sure you can use some of these techniques to write a bot which will do similarly, and do tell me about how you solved it!) The difference between a slash command in Slack and other integrations such as Webhooks is that only the person who issued the command sees the response, so it doesn’t clutter the channel.

what we build

My smart phone publishes a location update via MQTT to my broker, from which mqttwarn picks it up, reformats the payload, and performs a HTTP POST to an external system on which a Slack integration can obtain the data.

The payload published by OwnTracks over MQTT is JSON which contains the latitude, longitude, altitude, etc. of the smart phone, as well as a time stamp, and a tracker-ID called TID which I configure on the device itself. The payload looks like this (I’ve removed some of the elements to keep the example brief):

{
  "_type": "location",
  "tid": "jJ",
  "tst": 1430493519,
  "lon": "2.295134",
  "lat": "48.858334"
}

mqttwarn subscribes to MQTT messages published at the topic used by my phone, and determines whether the message qualifies to be handled. The filter function can drop messages I’m not interested in handling even though they’re published to the same topic (e.g. lwt messages when the broker detects the device has temporarily gone offline).

I also provide a custom function in slackfuncs.py called slack_whereis() which will extract latitude and longitude from the OwnTracks payload and perform a reverse-geo lookup.

[defaults]
functions = 'slackfuncs'
launch	 = log, http

[config:http]
timeout = 60
targets = {
   'slack_whereis' : ['post', 'https://example.org/whereis/user/jjolie', None, None],
  }

[owntracks-Slack]
topic = owntracks/jjolie/+
filter = slack_whereis_filter()
alldata = slack_whereis()
targets = http:slack_whereis
format = reported last at {_dthhmm} from {geo}

So, if OwnTracks publishes the JSON shown above to the topic owntracks/jjolie/nex, say, mqttwarn, which is subscribed to that topic, will receive and handle the message and, via the slack_whereis() function, will add a geo element which mqttwarn can use in format when finally sending the message to the configured target.

In this case, I use the http target to POST data to a remote endpoint where the association (Jane is at this place) is stored.

The HTTP endpoint is a small Bottle app which accepts these location update POSTs on the one hand, and which is an endpoint for a Slack command on the other.

whereis.py

Slack slash commands

There’s pretty little that the guys over at SlackHQ forgot to implement, and one of my favorites is

/remind me in 3 days to tell bbucks to push changes to master soon

reminder

Slack allows me to add my own custom slash commands which I configure in Slack integrations. What I’m going to do here is to create a /whereis command so that my mates can see where I am (and maybe I can see where they are).

custom command

Adding a custom command requires two steps:

  1. add the Slack integration for the custom command
  2. create the code which will actually run the command

Adding the command integration is easy: choose the name of the command (/whereis), the HTTP endpoint, and a usage hint for the user. I make a note of the token which I can use in my code to ensure only clients which know the token can query my service.

When I invoke a custom command, Slack fires off a POST (or optional GET) request to the endpoint we configure. The example I show you here implements a simple solution for the task: use the value passed to /whereis to find information about a particular user and return a one-line text result which Slack displays au-lieu of the command I entered. (I run this particular app under nginx and UWSGI.)

#!/usr/bin/python
# Jan-Piet Mens, May 2015.  Slack slash command. (/whereis user)

import bottle   # pip install bottle
from bottle import request
from persist import PersistentDict # http://code.activestate.com/recipes/576642/

botname = 'owntracks'

path = '/home/jpm/slack-whereis/db/userlist.json'
userlist = PersistentDict(path, 'c', format='json')

app = application = bottle.Bottle()

@app.route('/', method='POST')
def slack_post():
    body = bottle.request.body.read()

    token           = request.forms.get('token')          # yJxxxxxxxxxxxxxxxxxxxxxx
    team_id         = request.forms.get('team_id')        # T00000000
    team_domain     = request.forms.get('team_domain')    # example
    service_id      = request.forms.get('service_id')     # 0123456789
    channel_id      = request.forms.get('channel_id')     # C01234567
    channel_name    = request.forms.get('channel_name')   # general
    timestamp       = request.forms.get('timestamp')      # 1428242917.000011
    user_id         = request.forms.get('user_id')        # U10101010
    user_name       = request.forms.get('user_name')      # jpmens
    text            = request.forms.get('text')           # <free form>
    trigger_words   = request.forms.get('trigger_words')

    if token != '1xyfx098xelxRxk913x01234':  # integration token
        return "NOPE"

    # text contains the username (or it is empty)
    who = text.lower().rstrip()
    if who == "" or who is None:
        return "Who?"

    if who in userlist:
        response = "%s %s" % (who, userlist.get(who, 'dunno'))
    else:
        response = "I haven't a clue where %s is." % (who)

    return response

# curl -d 'is lying on the beach...' https://example.org/whereis/user/jjolie

@app.route('/user/<username>', method='POST')
def user(username):
    text = bottle.request.body.read()

    userlist[username] = text
    userlist.sync()

    return ""

if __name__ == '__main__':
    bottle.run(app, host='0.0.0.0', port=80)

If I’m very concerned about privacy, I can add a bit of haversine magic or use the OwnTracks waypoints feature on the location reported by OwnTracks to keep things anonymous even from trusted team mates:

at home

For example, I can hide at which restaurant customer I am, etc.

If this has inspired you to build something with OwnTracks, we’d love to know.

OwnTracks and Slack :: 02 May 2015 :: e-mail