├── .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 |
--------------------------------------------------------------------------------