We love Slack! But what if we can make it even cooler? Imagine: you are geeking out with your fellow developers on Slack and you want to give them credit. Or you can write “stupidsubject–” and it automagically shows “stupidsubject’s karma decreased to -2”. Enter Karma Bot. This is nothing new but building one myself was a great learning exercise and a fun tool we use on our Slack now.
I will show you how I implemented our Karma Bot using Slack’s Real Time Messaging API. I hope to extend it into an open source package later on adding tests, docs, setup file, etc. I will document progress in future articles.
Setup
This exercise is similar to our How to Build a Simple Slack Bot article. First you create a bot user and get an API_KEY from Slack.
The bot user needs to be defined as ID so you need to retrieve it for which I made a helper script:
$ python3 -m utils.get_botid
Bot ID for 'karmabot' is xyz
(This calls the get_botid.py script in the utils package. More on packaging next week …)
Then I stored the following two env variables in my bashrc
:
export SLACK_KARMA_BOTUSER=xyz
export SLACK_KARMA_TOKEN=super-secret
As we will see next week __init__.py makes a folder a package. You can use this file to do setup. I read env variables in, define my (regex) constants, instantiate the SlackClient
object to talk to the Slack API, and setup logging and caching. See __init__.py.
Structure
The code for this project is here.
The main.py script is the driver calling methods from the bot package (folder):
-
It connects to the Real Time Messaging API with
SLACK_CLIENT.rtm_connect()
. -
Each second it checks our Slack for new messages with the helper
parse_next_msg
(karma.py) which pings the API withSLACK_CLIENT.rtm_read()
and parses the response. -
One of my favorite regex methods
findall
checks each new message for potential karma actions:karma_changes = KARMA_ACTION.findall(text)
where:
KARMA_ACTION = re.compile(r'(?:^| )(\S{2,}?)\s?([\+\-]{2,})')
This is a complex regex so let me break it down:
- start of message or preceding space
- two or more non-space characters
- one optional space (convenient because Slack’s autocomplete-select of username inserts one)
- the voting component = two or more +’s and/or -‘s (one + or – led to a lot of false positives!)
-
karma.py‘s
parse_karma_change
is then called to parse out giver, receiver and points. Giver and receiver are returned by the Slack API as IDs so I need slack.py‘slookup_username
to convert them to usernames (which I cache inUSERNAME_CACHE
). -
Then karma.py‘s
change_karma
is called to increase/decrease the karma and returns a message for the bot to post. -
Lastly
post_msg
(slack.py) is called to have the bot post the karma result message back to the same channel the original message (request) came from. -
To keep track of scores I use a
Counter
object which is stored to disk withpickle
. This is setup in __init__.py:try: logging.info('Retrieving karma cache file') karmas = pickle.load(open(KARMA_CACHE, "rb")) except FileNotFoundError: logging.info('No cache file starting new Counter object in memory') karmas = Counter()
… and is backed up every minute with:
def _save_cache(): pickle.dump(karmas, open(KARMA_CACHE, "wb"))
I might actually turn this into a real DB.
Deploy
When we built our first Slack bot for How to Build a Simple Slack Bot we needed a way to keep the bot alive even if it crashed or the process was terminated by the OS. For Karma Bot I went with the same workaround as then: a run.sh wrapper that respawns. So if you want to use this code yourself, you would kick it off like this:
$ nohup ./run.sh &
Example
Test session in private Karma Bot channel:
You need to invite the bot to any channel you want to use this in.
More on packaging
My first attempt at this was one big script. I then splitted it out into different modules (responsabilities). Unfortunately I did not commit the initial script to compare. No worries though. Next week I go back to basics on modules and packaging, explaining how they work. I will explain how we import from them which often leads to confusion.
Update 07/08/2017
I refactored this project for Code Challenge 30 – The Art of Refactoring: Improve Your Code, see the review here.
Keep Calm and Code in Python!
— Bob