├── .gitignore ├── LICENSE ├── README.md ├── User.py ├── default.json ├── fetchChannelId.py ├── requirements.txt ├── results.csv └── slackbotExercise.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | *.pyc 3 | 4 | log*.csv 5 | log*.csv_DEBUG 6 | user_cache.save 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slackbot-workout 2 | A fun hack that gets Slackbot to force your teammates to work out! 3 | 4 | 5 | 6 | 7 | # Instructions 8 | 9 | 1. Clone the repo and navigate into the directory in your terminal. 10 | 11 | `$ git clone git@github.com:brandonshin/slackbot-workout.git` 12 | 13 | 2. In the **[Slack API Page](https://api.slack.com/docs/oauth-test-tokens)**, register yourself an auth token for your team. You should see this. Take note of the token, e.g. `xoxp-2751727432-4028172038-5281317294-3c46b1`. This is your **SLACK_USER_TOKEN_STRING** 14 | 15 | 16 | 17 | 3. In the **Slackbot [Remote control Page](https://slack.com/apps/A0F81R8ET-slackbot)**. Register an integration by clicking Add Configuration & then you should see this. __Make sure you grab just the token out of the url__, e.g. `AizJbQ24l38ai4DlQD9yFELb`. This is your **SLACK_URL_TOKEN_STRING** 18 | 19 | 20 | 21 | 4. Save your SLACK_USER_TOKEN_STRING and SLACK_URL_TOKEN_STRING as environmental variables in your terminal. 22 | 23 | `$ export SLACK_USER_TOKEN_STRING=YOURUSERTOKEN` 24 | 25 | `$ export SLACK_URL_TOKEN_STRING=YOURURLTOKEN` 26 | 27 | If you need help with this, try adapting the first 5 steps of the guide to [edit your .bash_profile](http://natelandau.com/my-mac-osx-bash_profile/) 28 | 29 | 5. Set up channel and customize configurations 30 | 31 | Open `default.json` and set `teamDomain` (ex: ctrlla) `channelName` (ex: general) and `channelId` (ex: B22D35YMS). Save the file as `config.json` in the same directory. Set any other configurations as you like. 32 | 33 | If you don't know the channel Id, fetch it using 34 | 35 | `$ python fetchChannelId.py channelname` 36 | 37 | 6. If you haven't set up pip for python, go in your terminal and run. 38 | `$ sudo easy_install pip` 39 | 40 | 7. While in the project directory, run 41 | 42 | `$ sudo pip install -r requirements.txt` 43 | 44 | `$ python slackbotExercise.py` 45 | 46 | Run the script to start the workouts and hit ctrl+c to stop the script. Hope you have fun with it! 47 | -------------------------------------------------------------------------------- /User.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | import datetime 5 | 6 | # Environment variables must be set with your tokens 7 | USER_TOKEN_STRING = os.environ['SLACK_USER_TOKEN_STRING'] 8 | 9 | class User: 10 | 11 | def __init__(self, user_id): 12 | # The Slack ID of the user 13 | self.id = user_id 14 | 15 | # The username (@username) and real name 16 | self.username, self.real_name = self.fetchNames() 17 | 18 | # A list of all exercises done by user 19 | self.exercise_history = [] 20 | 21 | # A record of all exercise totals (quantity) 22 | self.exercises = {} 23 | 24 | # A record of exercise counts (# of times) 25 | self.exercise_counts = {} 26 | 27 | # A record of past runs 28 | self.past_workouts = {} 29 | 30 | print "New user: " + self.real_name + " (" + self.username + ")" 31 | 32 | 33 | def storeSession(self, run_name): 34 | try: 35 | self.past_workouts[run_name] = self.exercises 36 | except: 37 | self.past_workouts = {} 38 | 39 | self.past_workouts[run_name] = self.exercises 40 | self.exercises = {} 41 | self.exercise_counts = {} 42 | 43 | 44 | def fetchNames(self): 45 | params = {"token": USER_TOKEN_STRING, "user": self.id} 46 | response = requests.get("https://slack.com/api/users.info", 47 | params=params) 48 | user_obj = json.loads(response.text, encoding='utf-8')["user"] 49 | 50 | username = user_obj["name"] 51 | real_name = user_obj["profile"]["real_name"] 52 | 53 | return username, real_name 54 | 55 | 56 | def getUserHandle(self): 57 | return ("@" + self.username).encode('utf-8') 58 | 59 | 60 | ''' 61 | Returns true if a user is currently "active", else false 62 | ''' 63 | def isActive(self): 64 | try: 65 | params = {"token": USER_TOKEN_STRING, "user": self.id} 66 | response = requests.get("https://slack.com/api/users.getPresence", 67 | params=params) 68 | status = json.loads(response.text, encoding='utf-8')["presence"] 69 | 70 | return status == "active" 71 | except requests.exceptions.ConnectionError: 72 | print "Error fetching online status for " + self.getUserHandle() 73 | return False 74 | 75 | def addExercise(self, exercise, reps): 76 | # Add to total counts 77 | self.exercises[exercise["id"]] = self.exercises.get(exercise["id"], 0) + reps 78 | self.exercise_counts[exercise["id"]] = self.exercise_counts.get(exercise["id"], 0) + 1 79 | 80 | # Add to exercise history record 81 | self.exercise_history.append([datetime.datetime.now().isoformat(),exercise["id"],exercise["name"],reps,exercise["units"]]) 82 | 83 | def hasDoneExercise(self, exercise): 84 | return exercise["id"] in self.exercise_counts 85 | 86 | -------------------------------------------------------------------------------- /default.json: -------------------------------------------------------------------------------- 1 | { 2 | "teamDomain": "yourDomainHere", 3 | "channelName": "general", 4 | "channelId": "channelIdHere", 5 | 6 | "officeHours": { 7 | "on": false, 8 | "begin": 9, 9 | "end": 17 10 | }, 11 | 12 | "debug": false, 13 | 14 | "callouts": { 15 | "timeBetween": { 16 | "minTime": 17, 17 | "maxTime": 23, 18 | "units": "minutes" 19 | }, 20 | "numPeople": 3, 21 | "slidingWindowSize": 8, 22 | "groupCalloutChance": 0.05 23 | }, 24 | 25 | "exercises": [ 26 | { 27 | "id": 0, 28 | "name": "pushups", 29 | "minReps": 15, 30 | "maxReps": 20, 31 | "units": "rep" 32 | }, 33 | { 34 | "id": 1, 35 | "name": "planks", 36 | "minReps": 40, 37 | "maxReps": 60, 38 | "units": "second" 39 | }, 40 | { 41 | "id": 2, 42 | "name": "wall sit", 43 | "minReps": 40, 44 | "maxReps": 50, 45 | "units": "second" 46 | }, 47 | { 48 | "id": 3, 49 | "name": "chair dips", 50 | "minReps": 15, 51 | "maxReps": 30, 52 | "units": "rep" 53 | }, 54 | { 55 | "id": 4, 56 | "name": "calf raises", 57 | "minReps": 20, 58 | "maxReps": 30, 59 | "units": "rep" 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /fetchChannelId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A quick script to fetch the id of a channel you want to use. 3 | 4 | USAGE: python fetchChannelId.py 5 | ''' 6 | 7 | import requests 8 | import sys 9 | import os 10 | import json 11 | 12 | # Environment variables must be set with your tokens 13 | USER_TOKEN_STRING = os.environ['SLACK_USER_TOKEN_STRING'] 14 | URL_TOKEN_STRING = os.environ['SLACK_URL_TOKEN_STRING'] 15 | 16 | HASH = "%23" 17 | 18 | channelName = sys.argv[1] 19 | 20 | params = {"token": USER_TOKEN_STRING } 21 | 22 | # Capture Response as JSON 23 | response = requests.get("https://slack.com/api/channels.list", params=params) 24 | channels = json.loads(response.text, encoding='utf-8')["channels"] 25 | 26 | for channel in channels: 27 | if channel["name"] == channelName: 28 | print channel["id"] 29 | break 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.7.0 2 | -------------------------------------------------------------------------------- /results.csv: -------------------------------------------------------------------------------- 1 | @bshin,26, SECOND PLANK 2 | @shahan,45, PUSHUPS 3 | @shahan,42, SECOND WALL SIT 4 | @bshin,26, PUSHUPS 5 | @bshin,35, PUSHUPS 6 | @bshin,28, PUSHUPS 7 | @shahan,40, SECOND PLANK 8 | @brandon,48, PUSHUPS 9 | @bshin,36, SECOND WALL SIT 10 | -------------------------------------------------------------------------------- /slackbotExercise.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import requests 4 | import json 5 | import csv 6 | import os 7 | from random import shuffle 8 | import pickle 9 | import os.path 10 | import datetime 11 | 12 | from User import User 13 | 14 | # Environment variables must be set with your tokens 15 | USER_TOKEN_STRING = os.environ['SLACK_USER_TOKEN_STRING'] 16 | URL_TOKEN_STRING = os.environ['SLACK_URL_TOKEN_STRING'] 17 | 18 | HASH = "%23" 19 | 20 | 21 | # Configuration values to be set in setConfiguration 22 | class Bot: 23 | def __init__(self): 24 | self.setConfiguration() 25 | 26 | self.csv_filename = "log" + time.strftime("%Y%m%d-%H%M") + ".csv" 27 | self.first_run = True 28 | 29 | # local cache of usernames 30 | # maps userIds to usernames 31 | self.user_cache = self.loadUserCache() 32 | 33 | # round robin store 34 | self.user_queue = [] 35 | 36 | 37 | def loadUserCache(self): 38 | if os.path.isfile('user_cache.save'): 39 | with open('user_cache.save','rb') as f: 40 | self.user_cache = pickle.load(f) 41 | print "Loading " + str(len(self.user_cache)) + " users from cache." 42 | return self.user_cache 43 | 44 | return {} 45 | 46 | ''' 47 | Sets the configuration file. 48 | 49 | Runs after every callout so that settings can be changed realtime 50 | ''' 51 | def setConfiguration(self): 52 | # Read variables fromt the configuration file 53 | with open('config.json') as f: 54 | settings = json.load(f) 55 | 56 | self.team_domain = settings["teamDomain"] 57 | self.channel_name = settings["channelName"] 58 | self.min_countdown = settings["callouts"]["timeBetween"]["minTime"] 59 | self.max_countdown = settings["callouts"]["timeBetween"]["maxTime"] 60 | self.num_people_per_callout = settings["callouts"]["numPeople"] 61 | self.sliding_window_size = settings["callouts"]["slidingWindowSize"] 62 | self.group_callout_chance = settings["callouts"]["groupCalloutChance"] 63 | self.channel_id = settings["channelId"] 64 | self.exercises = settings["exercises"] 65 | self.office_hours_on = settings["officeHours"]["on"] 66 | self.office_hours_begin = settings["officeHours"]["begin"] 67 | self.office_hours_end = settings["officeHours"]["end"] 68 | 69 | self.debug = settings["debug"] 70 | 71 | self.post_URL = "https://" + self.team_domain + ".slack.com/services/hooks/slackbot?token=" + URL_TOKEN_STRING + "&channel=" + HASH + self.channel_name 72 | 73 | 74 | ################################################################################ 75 | ''' 76 | Selects an active user from a list of users 77 | ''' 78 | def selectUser(bot, exercise): 79 | active_users = fetchActiveUsers(bot) 80 | 81 | # Add all active users not already in the user queue 82 | # Shuffles to randomly add new active users 83 | shuffle(active_users) 84 | bothArrays = set(active_users).intersection(bot.user_queue) 85 | for user in active_users: 86 | if user not in bothArrays: 87 | bot.user_queue.append(user) 88 | 89 | # The max number of users we are willing to look forward 90 | # to try and find a good match 91 | sliding_window = bot.sliding_window_size 92 | 93 | # find a user to draw, priority going to first in 94 | for i in range(len(bot.user_queue)): 95 | user = bot.user_queue[i] 96 | 97 | # User should be active and not have done exercise yet 98 | if user in active_users and not user.hasDoneExercise(exercise): 99 | bot.user_queue.remove(user) 100 | return user 101 | elif user in active_users: 102 | # Decrease sliding window by one. Basically, we don't want to jump 103 | # too far ahead in our queue 104 | sliding_window -= 1 105 | if sliding_window <= 0: 106 | break 107 | 108 | # If everybody has done exercises or we didn't find a person within our sliding window, 109 | for user in bot.user_queue: 110 | if user in active_users: 111 | bot.user_queue.remove(user) 112 | return user 113 | 114 | # If we weren't able to select one, just pick a random 115 | print "Selecting user at random (queue length was " + str(len(bot.user_queue)) + ")" 116 | return active_users[random.randrange(0, len(active_users))] 117 | 118 | 119 | ''' 120 | Fetches a list of all active users in the channel 121 | ''' 122 | def fetchActiveUsers(bot): 123 | # Check for new members 124 | params = {"token": USER_TOKEN_STRING, "channel": bot.channel_id} 125 | response = requests.get("https://slack.com/api/channels.info", params=params) 126 | user_ids = json.loads(response.text, encoding='utf-8')["channel"]["members"] 127 | 128 | active_users = [] 129 | 130 | for user_id in user_ids: 131 | # Add user to the cache if not already 132 | if user_id not in bot.user_cache: 133 | bot.user_cache[user_id] = User(user_id) 134 | if not bot.first_run: 135 | # Push our new users near the front of the queue! 136 | bot.user_queue.insert(2,bot.user_cache[user_id]) 137 | 138 | if bot.user_cache[user_id].isActive(): 139 | active_users.append(bot.user_cache[user_id]) 140 | 141 | if bot.first_run: 142 | bot.first_run = False 143 | 144 | return active_users 145 | 146 | ''' 147 | Selects an exercise and start time, and sleeps until the time 148 | period has past. 149 | ''' 150 | def selectExerciseAndStartTime(bot): 151 | next_time_interval = selectNextTimeInterval(bot) 152 | minute_interval = next_time_interval/60 153 | exercise = selectExercise(bot) 154 | 155 | # Announcement String of next lottery time 156 | lottery_announcement = "NEXT LOTTERY FOR " + exercise["name"].upper() + " IS IN " + str(minute_interval) + (" MINUTES" if minute_interval != 1 else " MINUTE") 157 | 158 | # Announce the exercise to the thread 159 | if not bot.debug: 160 | requests.post(bot.post_URL, data=lottery_announcement) 161 | print lottery_announcement 162 | 163 | # Sleep the script until time is up 164 | if not bot.debug: 165 | time.sleep(next_time_interval) 166 | else: 167 | # If debugging, once every 5 seconds 168 | time.sleep(5) 169 | 170 | return exercise 171 | 172 | 173 | ''' 174 | Selects the next exercise 175 | ''' 176 | def selectExercise(bot): 177 | idx = random.randrange(0, len(bot.exercises)) 178 | return bot.exercises[idx] 179 | 180 | 181 | ''' 182 | Selects the next time interval 183 | ''' 184 | def selectNextTimeInterval(bot): 185 | return random.randrange(bot.min_countdown * 60, bot.max_countdown * 60) 186 | 187 | 188 | ''' 189 | Selects a person to do the already-selected exercise 190 | ''' 191 | def assignExercise(bot, exercise): 192 | # Select number of reps 193 | exercise_reps = random.randrange(exercise["minReps"], exercise["maxReps"]+1) 194 | 195 | winner_announcement = str(exercise_reps) + " " + str(exercise["units"]) + " " + exercise["name"] + " RIGHT NOW " 196 | 197 | # EVERYBODY 198 | if random.random() < bot.group_callout_chance: 199 | winner_announcement += "@channel!" 200 | 201 | for user_id in bot.user_cache: 202 | user = bot.user_cache[user_id] 203 | user.addExercise(exercise, exercise_reps) 204 | 205 | logExercise(bot,"@channel",exercise["name"],exercise_reps,exercise["units"]) 206 | 207 | else: 208 | winners = [selectUser(bot, exercise) for i in range(bot.num_people_per_callout)] 209 | 210 | for i in range(bot.num_people_per_callout): 211 | winner_announcement += str(winners[i].getUserHandle()) 212 | if i == bot.num_people_per_callout - 2: 213 | winner_announcement += ", and " 214 | elif i == bot.num_people_per_callout - 1: 215 | winner_announcement += "!" 216 | else: 217 | winner_announcement += ", " 218 | 219 | winners[i].addExercise(exercise, exercise_reps) 220 | logExercise(bot,winners[i].getUserHandle(),exercise["name"],exercise_reps,exercise["units"]) 221 | 222 | # Announce the user 223 | if not bot.debug: 224 | requests.post(bot.post_URL, data=winner_announcement) 225 | print winner_announcement 226 | 227 | 228 | def logExercise(bot,username,exercise,reps,units): 229 | filename = bot.csv_filename + "_DEBUG" if bot.debug else bot.csv_filename 230 | with open(filename, 'a') as f: 231 | writer = csv.writer(f) 232 | 233 | writer.writerow([str(datetime.datetime.now()),username,exercise,reps,units,bot.debug]) 234 | 235 | def saveUsers(bot): 236 | # Write to the command console today's breakdown 237 | s = "```\n" 238 | #s += "Username\tAssigned\tComplete\tPercent 239 | s += "Username".ljust(15) 240 | for exercise in bot.exercises: 241 | s += exercise["name"] + " " 242 | s += "\n---------------------------------------------------------------\n" 243 | 244 | for user_id in bot.user_cache: 245 | user = bot.user_cache[user_id] 246 | s += user.username.ljust(15) 247 | for exercise in bot.exercises: 248 | if exercise["id"] in user.exercises: 249 | s += str(user.exercises[exercise["id"]]).ljust(len(exercise["name"]) + 2) 250 | else: 251 | s += str(0).ljust(len(exercise["name"]) + 2) 252 | s += "\n" 253 | 254 | user.storeSession(str(datetime.datetime.now())) 255 | 256 | s += "```" 257 | 258 | if not bot.debug: 259 | requests.post(bot.post_URL, data=s) 260 | print s 261 | 262 | 263 | # write to file 264 | with open('user_cache.save','wb') as f: 265 | pickle.dump(bot.user_cache,f) 266 | 267 | def isOfficeHours(bot): 268 | if not bot.office_hours_on: 269 | if bot.debug: 270 | print "not office hours" 271 | return True 272 | now = datetime.datetime.now() 273 | now_time = now.time() 274 | if now_time >= datetime.time(bot.office_hours_begin) and now_time <= datetime.time(bot.office_hours_end): 275 | if bot.debug: 276 | print "in office hours" 277 | return True 278 | else: 279 | if bot.debug: 280 | print "out office hours" 281 | return False 282 | 283 | def main(): 284 | bot = Bot() 285 | 286 | try: 287 | while True: 288 | if isOfficeHours(bot): 289 | # Re-fetch config file if settings have changed 290 | bot.setConfiguration() 291 | 292 | # Get an exercise to do 293 | exercise = selectExerciseAndStartTime(bot) 294 | 295 | # Assign the exercise to someone 296 | assignExercise(bot, exercise) 297 | 298 | else: 299 | # Sleep the script and check again for office hours 300 | if not bot.debug: 301 | time.sleep(5*60) # Sleep 5 minutes 302 | else: 303 | # If debugging, check again in 5 seconds 304 | time.sleep(5) 305 | 306 | except KeyboardInterrupt: 307 | saveUsers(bot) 308 | 309 | 310 | main() 311 | --------------------------------------------------------------------------------