earn the White PyBites Ninja earn the Yellow PyBites Ninja earn the Orange PyBites Ninja right arrow earn more PyBites Ninja belts and certificates
The best way to learn to code in Python is to actually use the language.

Our platform offers effective Test Driven Learning which will be key to your progress.


Join thousands of Pythonistas and start coding!


Join us on our PyBites Platform
Click here to code!

Building a Karma Bot with Python and the Slack API

Posted by Bob on Sun 25 June 2017 in Tools • 3 min read

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 with SLACK_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's lookup_username to convert them to usernames (which I cache in USERNAME_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 with pickle. 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:

karma example

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


See an error in this post? Please submit a pull request on Github.