1 from slack.errors import SlackApiError
2 from turbot.blocks import input_block, section_block, text_block
9 from botocore.exceptions import ClientError
11 TURBOT_USER_ID = 'U01B9QM4P9R'
15 submission_handlers = {}
17 # Hunt and Puzzle IDs are restricted to letters, numbers, and underscores
18 valid_id_re = r'^[_a-zA-Z0-9]+$'
20 def bot_reply(message):
21 """Construct a return value suitable for a bot reply
23 This is suitable as a way to give an error back to the user who
24 initiated a slash command, for example."""
31 def submission_error(field, error):
32 """Construct an error suitable for returning for an invalid submission.
34 Returning this value will prevent a submission and alert the user that
35 the given field is invalid because of the given error."""
37 print("Rejecting invalid modal submission: {}".format(error))
42 "Content-Type": "application/json"
45 "response_action": "errors",
52 def new_hunt(turb, payload):
53 """Handler for the action of user pressing the new_hunt button"""
57 "private_metadata": json.dumps({}),
58 "title": { "type": "plain_text", "text": "New Hunt" },
59 "submit": { "type": "plain_text", "text": "Create" },
61 input_block("Hunt name", "name", "Name of the hunt"),
62 input_block("Hunt ID", "hunt_id",
63 "Used as puzzle channel prefix "
64 + "(no spaces nor punctuation)"),
65 input_block("Hunt URL", "url", "External URL of hunt",
70 result = turb.slack_client.views_open(trigger_id=payload['trigger_id'],
73 submission_handlers[result['view']['id']] = new_hunt_submission
80 actions['button'] = {"new_hunt": new_hunt}
82 def new_hunt_submission(turb, payload, metadata):
83 """Handler for the user submitting the new hunt modal
85 This is the modal view presented to the user by the new_hunt
88 state = payload['view']['state']['values']
89 name = state['name']['name']['value']
90 hunt_id = state['hunt_id']['hunt_id']['value']
91 url = state['url']['url']['value']
93 # Validate that the hunt_id contains no invalid characters
94 if not re.match(valid_id_re, hunt_id):
95 return submission_error("hunt_id",
96 "Hunt ID can only contain letters, "
97 + "numbers, and underscores")
99 # Check to see if the hunts table exists
100 hunts_table = turb.db.Table("hunts")
103 exists = hunts_table.table_status in ("CREATING", "UPDATING",
108 # Create the hunts table if necessary.
110 hunts_table = turb.db.create_table(
113 {'AttributeName': 'channel_id', 'KeyType': 'HASH'},
115 AttributeDefinitions=[
116 {'AttributeName': 'channel_id', 'AttributeType': 'S'},
118 ProvisionedThroughput={
119 'ReadCapacityUnits': 5,
120 'WriteCapacityUnits': 5
123 return submission_error("hunt_id",
124 "Still bootstrapping hunts table. Try again.")
126 # Create a channel for the hunt
128 response = turb.slack_client.conversations_create(name=hunt_id)
129 except SlackApiError as e:
130 return submission_error("hunt_id",
131 "Error creating Slack channel: {}"
132 .format(e.response['error']))
134 if not response['ok']:
135 return submission_error("name",
136 "Error occurred creating Slack channel "
137 + "(see CloudWatch log")
139 user_id = payload['user']['id']
140 channel_id = response['channel']['id']
142 # Create a sheet for the channel
143 sheet = turbot.sheets.sheets_create(turb, hunt_id)
145 channel_id = response['channel']['id']
147 # Insert the newly-created hunt into the database
148 hunts_table.put_item(
150 'channel_id': channel_id,
155 "sheet_url": sheet['url']
159 # Invite the initiating user to the channel
160 turb.slack_client.conversations_invite(channel=channel_id, users=user_id)
162 # Message the channel with the URL of the sheet
163 turb.slack_client.chat_postMessage(channel=channel_id,
164 text="Sheet created for this hunt: {}"
165 .format(sheet['url']))
167 # Create a database table for this hunt's puzzles
168 table = turb.db.create_table(
170 AttributeDefinitions=[
171 {'AttributeName': 'channel_id', 'AttributeType': 'S'}
174 {'AttributeName': 'channel_id', 'KeyType': 'HASH'}
176 ProvisionedThroughput={
177 'ReadCapacityUnits': 5,
178 'WriteCapacityUnits': 4
182 # Message the hunt channel that the database is ready
183 turb.slack_client.chat_postMessage(
185 text="Welcome to your new hunt! "
186 + "Use `/puzzle` to create puzzles for the hunt.")
192 def view_submission(turb, payload):
193 """Handler for Slack interactive view submission
195 Specifically, those that have a payload type of 'view_submission'"""
197 view_id = payload['view']['id']
198 metadata = payload['view']['private_metadata']
200 if view_id in submission_handlers:
201 return submission_handlers[view_id](turb, payload, metadata)
203 print("Error: Unknown view ID: {}".format(view_id))
208 def rot(turb, body, args):
209 """Implementation of the /rot command
211 The args string should be as follows:
213 [count|*] String to be rotated
215 That is, the first word of the string is an optional number (or
216 the character '*'). If this is a number it indicates an amount to
217 rotate each character in the string. If the count is '*' or is not
218 present, then the string will be rotated through all possible 25
221 The result of the rotation is returned (with Slack formatting) in
222 the body of the response so that Slack will provide it as a reply
223 to the user who submitted the slash command."""
225 channel_name = body['channel_name'][0]
226 response_url = body['response_url'][0]
227 channel_id = body['channel_id'][0]
229 result = turbot.rot.rot(args)
231 if (channel_name == "directmessage"):
232 requests.post(response_url,
233 json = {"text": result},
234 headers = {"Content-type": "application/json"})
236 turb.slack_client.chat_postMessage(channel=channel_id, text=result)
243 commands["/rot"] = rot
245 def puzzle(turb, body, args):
246 """Implementation of the /puzzle command
248 The args string is currently ignored (this command will bring up
249 a modal dialog for user input instead)."""
251 channel_id = body['channel_id'][0]
252 trigger_id = body['trigger_id'][0]
254 hunts_table = turb.db.Table("hunts")
255 response = hunts_table.get_item(Key={'channel_id': channel_id})
257 if 'Item' in response:
258 hunt_name = response['Item']['name']
259 hunt_id = response['Item']['hunt_id']
261 return bot_reply("Sorry, this channel doesn't appear to "
262 + "be a hunt channel")
266 "private_metadata": json.dumps({
268 "hunt_channel_id": channel_id
270 "title": {"type": "plain_text", "text": "New Puzzle"},
271 "submit": { "type": "plain_text", "text": "Create" },
273 section_block(text_block("*For {}*".format(hunt_name))),
274 input_block("Puzzle name", "name", "Name of the puzzle"),
275 input_block("Puzzle ID", "puzzle_id",
276 "Used as part of channel name "
277 + "(no spaces nor punctuation)"),
278 input_block("Puzzle URL", "url", "External URL of puzzle",
283 result = turb.slack_client.views_open(trigger_id=trigger_id,
287 submission_handlers[result['view']['id']] = puzzle_submission
293 commands["/puzzle"] = puzzle
295 def puzzle_submission(turb, payload, metadata):
296 """Handler for the user submitting the new puzzle modal
298 This is the modal view presented to the user by the puzzle function
301 print("In puzzle_submission\npayload is: {}\nmetadata is {}"
302 .format(payload, metadata))
304 meta = json.loads(metadata)
305 hunt_id = meta['hunt_id']
306 hunt_channel_id = meta['hunt_channel_id']
308 state = payload['view']['state']['values']
309 name = state['name']['name']['value']
310 puzzle_id = state['puzzle_id']['puzzle_id']['value']
311 url = state['url']['url']['value']
313 # Validate that the puzzle_id contains no invalid characters
314 if not re.match(valid_id_re, puzzle_id):
315 return submission_error("puzzle_id",
316 "Puzzle ID can only contain letters, "
317 + "numbers, and underscores")
319 # Create a channel for the puzzle
320 hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
323 response = turb.slack_client.conversations_create(
324 name=hunt_dash_channel)
325 except SlackApiError as e:
326 return submission_error("puzzle_id",
327 "Error creating Slack channel: {}"
328 .format(e.response['error']))
330 puzzle_channel_id = response['channel']['id']
332 # Create a sheet for the puzzle
333 sheet = turbot.sheets.sheets_create_for_puzzle(turb, hunt_dash_channel)
335 # Insert the newly-created puzzle into the database
336 table = turb.db.Table(hunt_id)
340 "channel_id": puzzle_channel_id,
342 "status": 'unsolved',
344 "puzzle_id": puzzle_id,
346 "sheet_url": sheet['url']
350 # Find all members of the hunt channel
351 members = turbot.slack.slack_channel_members(turb.slack_client,
354 # Filter out Turbot's own ID to avoid inviting itself
355 members = [m for m in members if m != TURBOT_USER_ID]
357 turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
358 text="Inviting members: {}".format(str(members)))
360 # Invite those members to the puzzle channel (in chunks of 500)
362 while cursor < len(members):
363 turb.slack_client.conversations_invite(
364 channel=puzzle_channel_id,
365 users=members[cursor:cursor + 500])
368 # Message the channel with the URL of the puzzle's sheet
369 turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
370 text="Sheet created for this puzzle: {}"
371 .format(sheet['url']))