├── requirements.txt ├── .gitignore ├── house_points.png ├── .travis.yml ├── consts.py ├── Readme.md ├── points_util.py ├── cup_image.py ├── test.py └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | slackclient -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *[0-9]*.png 2 | *.pyc 3 | consts2.py 4 | *.pkl 5 | -------------------------------------------------------------------------------- /house_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veryeli/slack-points/HEAD/house_points.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: "pip install -r requirements.txt" 6 | # command to run tests 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /consts.py: -------------------------------------------------------------------------------- 1 | HOUSES = ["Ravenclaw", "Hufflepuff", "Gryffindor", "Slytherin"] 2 | SLACK_TOKEN = 'your_token_here' 3 | # only prefects can add and remove multiple points 4 | PREFECTS = ["your_slack_id_here", "someone_elses_here"] 5 | # Announcers will be able to make the bot print the current standing 6 | ANNOUNCERS = PREFECTS 7 | CHANNEL = u'some_slack_channel_id' 8 | IMAGE_PATH = "house_points.png" 9 | POINTS_FILE = 'points.pkl' -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Slack Points Slackbot 2 | 3 | This amazing bot will let everyone on your slack team award points to different crew. Our code of honor is 4 | 5 | - No awarding points to your own crew 6 | - Cheating will be punished with great wrath and greater whimsy 7 | 8 | To set it up, just insert your slack API token, the channel you'd like to listen on, pip install the requirements, and run `python main.py` on some server somewhere. Hackathon style! 9 | 10 | 11 | ![Bot in action][slack] 12 | 13 | 14 | [slack]: https://files.slack.com/files-pri/T029GG40X-F0Q6DDGN7/pasted_image_at_2016_03_03_01_48_pm.png?pub_secret=83fd31bc54 15 | -------------------------------------------------------------------------------- /points_util.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def clean(message): 4 | """Standardize spacing and capitalization""" 5 | return ' '.join(m.lower() for m in message.split() if m) 6 | 7 | def pluralized_points(num_points): 8 | if num_points == 1 or num_points == -1: 9 | return "%d point" % num_points 10 | return "%d points" % num_points 11 | 12 | def detect_points(message): 13 | amounts = [amount for amount in clean(message).split() 14 | if amount.isdigit()] 15 | if len(amounts) == 0 and 'one' in clean(message): 16 | amounts = [1] 17 | if len(amounts) == 1: 18 | return int(amounts[0]) * detect_point_polarity(message) 19 | else: 20 | return 0 21 | 22 | def detect_point_polarity(message): 23 | """Discern whether this is a point awarding or deduction""" 24 | message = clean(message) 25 | if any(x in message for x in ["points to", "point to", "point for", "points for"]): 26 | return 1 27 | elif any(x in message for x in ["points from", "point from"]): 28 | return -1 29 | else: 30 | return 0 31 | 32 | def proper_name_for(house): 33 | """Forgive house misspelling""" 34 | if "raven" in house: 35 | return "Ravenclaw" 36 | if "huff" in house: 37 | return "Hufflepuff" 38 | if "gryf" in house: 39 | return "Gryffindor" 40 | if "slyt" in house: 41 | return "Slytherin" 42 | 43 | def get_houses_from(message): 44 | return list(set(proper_name_for(w) for w in clean(message).split() if proper_name_for(w))) 45 | -------------------------------------------------------------------------------- /cup_image.py: -------------------------------------------------------------------------------- 1 | from consts import HOUSES, IMAGE_PATH 2 | from PIL import Image, ImageDraw 3 | 4 | 5 | BAR_WIDTH = 103.5 6 | BAR_BOTTOM = 947 7 | BAR_HEIGHT = 650 8 | 9 | BAR_X = { 10 | "Gryffindor": 306, 11 | "Hufflepuff": 551, 12 | "Slytherin": 806, 13 | "Ravenclaw": 1043, 14 | } 15 | 16 | BAR_COLOR = { 17 | "Gryffindor": "#ff0000", 18 | "Ravenclaw": "#0000ff", 19 | "Hufflepuff": "#ffff00", 20 | "Slytherin": "#00ff00", 21 | } 22 | 23 | 24 | def calculate_scales(house_points): 25 | total_points = float(sum(house_points.values())) or 1.0 26 | 27 | return {house: house_points.get(house, 0) / total_points for house in HOUSES} 28 | 29 | def draw_bar_for_house(im, house, scale): 30 | draw = ImageDraw.Draw(im) 31 | draw.rectangle((BAR_X[house], BAR_BOTTOM, 32 | BAR_X[house] + BAR_WIDTH, BAR_BOTTOM - scale * BAR_HEIGHT), 33 | fill=BAR_COLOR[house]) 34 | draw.ellipse((BAR_X[house], BAR_BOTTOM - 50, BAR_X[house] + BAR_WIDTH, BAR_BOTTOM + 50), fill=BAR_COLOR[house]) 35 | del draw 36 | 37 | 38 | def image_for_scores(scores, upload=True): 39 | """Generate a sweet house cup image 40 | Arguments: a dictionary with house names as keys and scores as values 41 | Returns: an imgur link to a house cup image representing the 42 | scores 43 | """ 44 | scaled = calculate_scales(scores) 45 | points_image = Image.open(IMAGE_PATH) 46 | for house in HOUSES: 47 | draw_bar_for_house(points_image, house, scaled[house]) 48 | 49 | outfile = str(abs(hash(str(scores)))) + '.png' 50 | points_image.save(outfile, "PNG") 51 | del points_image 52 | return outfile 53 | 54 | 55 | if __name__=="__main__": 56 | image_for_scores(BAR_X, upload=False) -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test point counter functionality 3 | """ 4 | from main import PointCounter 5 | import unittest 6 | 7 | TEST_PREFECTS = ["prefect"] 8 | TEST_POINTS = "test_points.pkl" 9 | 10 | class TestPointCounter(unittest.TestCase): 11 | """Initialize a point counter and test response messages""" 12 | 13 | def setUp(self): 14 | self.p = PointCounter(TEST_PREFECTS, points_file=TEST_POINTS) 15 | 16 | def test_adding_points(self): 17 | p = PointCounter(TEST_PREFECTS, points_file=TEST_POINTS) 18 | msg = p.award_points("6 points to Gryffindor", TEST_PREFECTS[0]) 19 | for m in msg: 20 | self.assertEqual(m,"Gryffindor gets 6 points") 21 | 22 | def test_adding_points_not_by_prefect(self): 23 | p = PointCounter(TEST_PREFECTS, points_file=TEST_POINTS) 24 | msg = p.award_points("6 points to Gryffindor", "harry potter") 25 | for m in msg: 26 | self.assertEqual(m, "Gryffindor gets 1 point") 27 | 28 | def test_adding_one_point(self): 29 | p = PointCounter(TEST_PREFECTS, points_file=TEST_POINTS) 30 | msg = p.award_points("oNe point to Gryffindor", "harry potter") 31 | for m in msg: 32 | self.assertEqual(m, "Gryffindor gets 1 point") 33 | 34 | def test_adding_one_point_to_slytherin(self): 35 | msg = self.p.award_points( 36 | "1 point to slytherin for @benkraft making slackbot" 37 | " listen for '911' mentions in 1s and 0s", "harry potter") 38 | for m in msg: 39 | self.assertEqual(m, "Slytherin gets 1 point") 40 | 41 | def test_subtracting_one_point(self): 42 | for m in self.p.award_points("oNe point from Gryffindor", "harry potter"): 43 | self.assertEqual(m, "Gryffindor loses 1 point") 44 | 45 | def test_works_with_usernames(self): 46 | message = "1 point to ravenclaw <@U0NJ1PH1R>" 47 | for m in self.p.award_points(message, "nymphadora tonks"): 48 | self.assertEqual(m, "Ravenclaw gets 1 point") 49 | 50 | def test_calculate_standings(self): 51 | p = PointCounter(TEST_PREFECTS, points_file=TEST_POINTS) 52 | p.award_points("6 points to Gryffindor", TEST_PREFECTS[0]) 53 | p.award_points("7 points to Ravenclaw", TEST_PREFECTS[0]) 54 | p.award_points("8 points to Hufflepuff", TEST_PREFECTS[0]) 55 | p.award_points("9 points to Slytherin", TEST_PREFECTS[0]) 56 | for m in p.print_status(): 57 | print m 58 | 59 | if __name__ == "__main__": 60 | unittest.main() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import points_util 2 | import cup_image 3 | from consts import HOUSES, SLACK_TOKEN, PREFECTS, ANNOUNCERS, CHANNEL, POINTS_FILE 4 | 5 | from collections import Counter 6 | import os 7 | import re 8 | import pickle 9 | from slackclient import SlackClient 10 | import time 11 | 12 | nth = { 13 | 1: "first", 14 | 2: "second", 15 | 3: "third", 16 | 4: "fourth" 17 | } 18 | 19 | class PointCounter(object): 20 | def __init__(self, prefects=PREFECTS, 21 | announcers=ANNOUNCERS, points_file=POINTS_FILE): 22 | try: 23 | self.points = pickle.load(open(points_file, 'rb')) 24 | if "Gryffendor" in self.points: 25 | self.points["Gryffindor"] = self.points["Gryffendor"] 26 | del self.points["Gryffendor"] 27 | except: 28 | self.points = Counter() 29 | self.prefects = prefects 30 | self.announcers = announcers 31 | self.points_file = points_file 32 | 33 | def get_points_from(self, message, awarder): 34 | amount = points_util.detect_points(message) 35 | # only prefects can award over one point at a time 36 | if awarder not in self.prefects: 37 | amount = max(min(amount, 1), -1) 38 | return amount 39 | 40 | @staticmethod 41 | def message_for(house, points): 42 | if points > 0: 43 | return "%s gets %s" % ( 44 | house, points_util.pluralized_points(points)) 45 | return "%s loses %s" % ( 46 | house, points_util.pluralized_points(abs(points))) 47 | 48 | def award_points(self, message, awarder): 49 | points = self.get_points_from(message, awarder) 50 | houses = points_util.get_houses_from(message) 51 | messages = [] 52 | if points and houses: 53 | for house in houses: 54 | self.points[house] += points 55 | pickle.dump(self.points, open(self.points_file, 'wb')) 56 | messages.append(self.message_for(house, points)) 57 | return messages 58 | 59 | def print_status(self): 60 | for place, (house, points) in enumerate(sorted(self.points.items(), key=lambda x: x[-1])): 61 | yield "In %s place, %s with %d points" % ( 62 | nth[len(HOUSES) - place], house, points) 63 | 64 | 65 | def is_hogwarts_related(message): 66 | return ( 67 | message.get("type", '') == "message" and 68 | message.get("channel", '') == CHANNEL and 69 | "text" in message and 70 | "user" in message and 71 | "point" in message["text"] and 72 | points_util.get_houses_from(message["text"])) 73 | 74 | def main(): 75 | sc = SlackClient(SLACK_TOKEN) 76 | p = PointCounter() 77 | if sc.rtm_connect(): 78 | while True: 79 | messages = sc.rtm_read() 80 | for message in messages: 81 | if is_hogwarts_related(message): 82 | print 'is_hogwarts_related' 83 | for m in p.award_points(message['text'], message['user']): 84 | sc.api_call( 85 | "chat.postMessage", channel=CHANNEL, text=m) 86 | os.system( 87 | "curl -F file=@%s -F title=%s -F channels=%s -F token=%s https://slack.com/api/files.upload" 88 | % (cup_image.image_for_scores(p.points), '"House Points"', CHANNEL, SLACK_TOKEN)) 89 | 90 | 91 | time.sleep(1) 92 | else: 93 | print "Connection Failed, invalid token?" 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | --------------------------------------------------------------------------------