1 from slack.errors import SlackApiError
2 from turbot.blocks import input_block, section_block, text_block
10 TURBOT_USER_ID = 'U01B9QM4P9R'
14 submission_handlers = {}
16 # Hunt and Puzzle IDs are restricted to letters, numbers, and underscores
17 valid_id_re = r'^[_a-zA-Z0-9]+$'
19 def bot_reply(message):
20 """Construct a return value suitable for a bot reply
22 This is suitable as a way to give an error back to the user who
23 initiated a slash command, for example."""
30 def submission_error(field, error):
31 """Construct an error suitable for returning for an invalid submission.
33 Returning this value will prevent a submission and alert the user that
34 the given field is invalid because of the given error."""
36 print("Rejecting invalid modal submission: {}".format(error))
41 "Content-Type": "application/json"
44 "response_action": "errors",
51 def new_hunt(turb, payload):
52 """Handler for the action of user pressing the new_hunt button"""
56 "private_metadata": json.dumps({}),
57 "title": { "type": "plain_text", "text": "New Hunt" },
58 "submit": { "type": "plain_text", "text": "Create" },
60 input_block("Hunt name", "name", "Name of the hunt"),
61 input_block("Hunt ID", "hunt_id",
62 "Used as puzzle channel prefix "
63 + "(no spaces nor punctuation)"),
64 input_block("Hunt URL", "url", "External URL of hunt",
69 result = turb.slack_client.views_open(trigger_id=payload['trigger_id'],
72 submission_handlers[result['view']['id']] = new_hunt_submission
79 actions['button'] = {"new_hunt": new_hunt}
81 def new_hunt_submission(turb, payload, metadata):
82 """Handler for the user submitting the new hunt modal
84 This is the modal view presented to the user by the new_hunt
87 state = payload['view']['state']['values']
88 name = state['name']['name']['value']
89 hunt_id = state['hunt_id']['hunt_id']['value']
90 url = state['url']['url']['value']
92 # Validate that the hunt_id contains no invalid characters
93 if not re.match(valid_id_re, hunt_id):
94 return submission_error("hunt_id",
95 "Hunt ID can only contain letters, "
96 + "numbers, and underscores")
98 # Create a channel for the hunt
100 response = turb.slack_client.conversations_create(name=hunt_id)
101 except SlackApiError as e:
102 return submission_error("hunt_id",
103 "Error creating Slack channel: {}"
104 .format(e.response['error']))
106 if not response['ok']:
107 return submission_error("name",
108 "Error occurred creating Slack channel "
109 + "(see CloudWatch log")
111 user_id = payload['user']['id']
112 channel_id = response['channel']['id']
114 # Create a sheet for the channel
115 sheet = turbot.sheets.sheets_create(turb, hunt_id)
117 # Insert the newly-created hunt into the database
118 hunts_table = turb.db.Table("hunts")
119 hunts_table.put_item(
121 'channel_id': channel_id,
126 "sheet_url": sheet['url']
130 # Invite the initiating user to the channel
131 turb.slack_client.conversations_invite(channel=channel_id, users=user_id)
133 # Message the channel with the URL of the sheet
134 turb.slack_client.chat_postMessage(channel=channel_id,
135 text="Sheet created for this hunt: {}"
136 .format(sheet['url']))
138 # Create a database table for this hunt's puzzles
139 table = turb.db.create_table(
141 AttributeDefinitions=[
142 {'AttributeName': 'channel_id', 'AttributeType': 'S'}
145 {'AttributeName': 'channel_id', 'KeyType': 'HASH'}
147 ProvisionedThroughput={
148 'ReadCapacityUnits': 5,
149 'WriteCapacityUnits': 4
153 # Message the hunt channel that the database is ready
154 turb.slack_client.chat_postMessage(
156 text="Welcome to your new hunt! "
157 + "Use `/puzzle` to create puzzles for the hunt.")
163 def view_submission(turb, payload):
164 """Handler for Slack interactive view submission
166 Specifically, those that have a payload type of 'view_submission'"""
168 view_id = payload['view']['id']
169 metadata = payload['view']['private_metadata']
171 if view_id in submission_handlers:
172 return submission_handlers[view_id](turb, payload, metadata)
174 print("Error: Unknown view ID: {}".format(view_id))
179 def rot(turb, body, args):
180 """Implementation of the /rot command
182 The args string should be as follows:
184 [count|*] String to be rotated
186 That is, the first word of the string is an optional number (or
187 the character '*'). If this is a number it indicates an amount to
188 rotate each character in the string. If the count is '*' or is not
189 present, then the string will be rotated through all possible 25
192 The result of the rotation is returned (with Slack formatting) in
193 the body of the response so that Slack will provide it as a reply
194 to the user who submitted the slash command."""
196 channel_name = body['channel_name'][0]
197 response_url = body['response_url'][0]
198 channel_id = body['channel_id'][0]
200 result = turbot.rot.rot(args)
202 if (channel_name == "directmessage"):
203 requests.post(response_url,
204 json = {"text": result},
205 headers = {"Content-type": "application/json"})
207 turb.slack_client.chat_postMessage(channel=channel_id, text=result)
214 commands["/rot"] = rot
216 def puzzle(turb, body, args):
217 """Implementation of the /puzzle command
219 The args string is currently ignored (this command will bring up
220 a modal dialog for user input instead)."""
222 channel_id = body['channel_id'][0]
223 trigger_id = body['trigger_id'][0]
225 hunts_table = turb.db.Table("hunts")
226 response = hunts_table.get_item(Key={'channel_id': channel_id})
228 if 'Item' in response:
229 hunt_name = response['Item']['name']
230 hunt_id = response['Item']['hunt_id']
232 return bot_reply("Sorry, this channel doesn't appear to "
233 + "be a hunt channel")
237 "private_metadata": json.dumps({
239 "hunt_channel_id": channel_id
241 "title": {"type": "plain_text", "text": "New Puzzle"},
242 "submit": { "type": "plain_text", "text": "Create" },
244 section_block(text_block("*For {}*".format(hunt_name))),
245 input_block("Puzzle name", "name", "Name of the puzzle"),
246 input_block("Puzzle ID", "puzzle_id",
247 "Used as part of channel name "
248 + "(no spaces nor punctuation)"),
249 input_block("Puzzle URL", "url", "External URL of puzzle",
254 result = turb.slack_client.views_open(trigger_id=trigger_id,
258 submission_handlers[result['view']['id']] = puzzle_submission
264 commands["/puzzle"] = puzzle
266 def puzzle_submission(turb, payload, metadata):
267 """Handler for the user submitting the new puzzle modal
269 This is the modal view presented to the user by the puzzle function
272 print("In puzzle_submission\npayload is: {}\nmetadata is {}"
273 .format(payload, metadata))
275 meta = json.loads(metadata)
276 hunt_id = meta['hunt_id']
277 hunt_channel_id = meta['hunt_channel_id']
279 state = payload['view']['state']['values']
280 name = state['name']['name']['value']
281 puzzle_id = state['puzzle_id']['puzzle_id']['value']
282 url = state['url']['url']['value']
284 # Validate that the puzzle_id contains no invalid characters
285 if not re.match(valid_id_re, puzzle_id):
286 return submission_error("puzzle_id",
287 "Puzzle ID can only contain letters, "
288 + "numbers, and underscores")
290 # Create a channel for the puzzle
291 hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
294 response = turb.slack_client.conversations_create(
295 name=hunt_dash_channel)
296 except SlackApiError as e:
297 return submission_error("puzzle_id",
298 "Error creating Slack channel: {}"
299 .format(e.response['error']))
301 puzzle_channel_id = response['channel']['id']
303 # Create a sheet for the puzzle
304 sheet = turbot.sheets.sheets_create_for_puzzle(turb, hunt_dash_channel)
306 # Insert the newly-created puzzle into the database
307 table = turb.db.Table(hunt_id)
311 "channel_id": puzzle_channel_id,
313 "status": 'unsolved',
315 "puzzle_id": puzzle_id,
317 "sheet_url": sheet['url']
321 # Find all members of the hunt channel
322 members = turbot.slack.slack_channel_members(turb.slack_client,
325 # Filter out Turbot's own ID to avoid inviting itself
326 members = [m for m in members if m != TURBOT_USER_ID]
328 turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
329 text="Inviting members: {}".format(str(members)))
331 # Invite those members to the puzzle channel (in chunks of 500)
333 while cursor < len(members):
334 turb.slack_client.conversations_invite(
335 channel=puzzle_channel_id,
336 users=members[cursor:cursor + 500])
339 # Message the channel with the URL of the puzzle's sheet
340 turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
341 text="Sheet created for this puzzle: {}"
342 .format(sheet['url']))