1 from slack.errors import SlackApiError
2 from turbot.blocks import input_block, section_block, text_block
9 from botocore.exceptions import ClientError
13 submission_handlers = {}
15 # Hunt and Puzzle IDs are restricted to letters, numbers, and underscores
16 valid_id_re = r'^[_a-zA-Z0-9]+$'
18 def bot_reply(message):
19 """Construct a return value suitable for a bot reply
21 This is suitable as a way to give an error back to the user who
22 initiated a slash command, for example."""
29 def submission_error(field, error):
30 """Construct an error suitable for returning for an invalid submission.
32 Returning this value will prevent a submission and alert the user that
33 the given field is invalid because of the given error."""
35 print("Rejecting invalid modal submission: {}".format(error))
40 "Content-Type": "application/json"
43 "response_action": "errors",
50 def new_hunt(turb, payload):
51 """Handler for the action of user pressing the new_hunt button"""
55 "private_metadata": json.dumps({}),
56 "title": { "type": "plain_text", "text": "New Hunt" },
57 "submit": { "type": "plain_text", "text": "Create" },
59 input_block("Hunt name", "name", "Name of the hunt"),
60 input_block("Hunt ID", "hunt_id",
61 "Used as puzzle channel prefix "
62 + "(no spaces nor punctuation)"),
63 input_block("Hunt URL", "url", "External URL of hunt",
68 result = turb.slack_client.views_open(trigger_id=payload['trigger_id'],
71 submission_handlers[result['view']['id']] = new_hunt_submission
78 actions['button'] = {"new_hunt": new_hunt}
80 def new_hunt_submission(turb, payload, metadata):
81 """Handler for the user submitting the new hunt modal
83 This is the modal view presented to the user by the new_hunt
86 state = payload['view']['state']['values']
87 user_id = payload['user']['id']
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 # Check to see if the hunts table exists
99 hunts_table = turb.db.Table("hunts")
102 exists = hunts_table.table_status in ("CREATING", "UPDATING",
107 # Create the hunts table if necessary.
109 hunts_table = turb.db.create_table(
112 {'AttributeName': 'channel_id', 'KeyType': 'HASH'},
114 AttributeDefinitions=[
115 {'AttributeName': 'channel_id', 'AttributeType': 'S'},
117 ProvisionedThroughput={
118 'ReadCapacityUnits': 5,
119 'WriteCapacityUnits': 5
122 return submission_error("hunt_id",
123 "Still bootstrapping hunts table. Try again.")
125 # Create a channel for the hunt
127 response = turb.slack_client.conversations_create(name=hunt_id)
128 except SlackApiError as e:
129 return submission_error("hunt_id",
130 "Error creating Slack channel: {}"
131 .format(e.response['error']))
133 channel_id = response['channel']['id']
135 # Create a sheet for the channel
136 sheet = turbot.sheets.sheets_create(turb, hunt_id)
138 channel_id = response['channel']['id']
140 # Insert the newly-created hunt into the database
141 # (leaving it as non-active for now until the channel-created handler
142 # finishes fixing it up with a sheet and a companion table)
143 hunts_table.put_item(
145 'channel_id': channel_id,
153 # Invite the initiating user to the channel
154 turb.slack_client.conversations_invite(channel=channel_id, users=user_id)
160 def view_submission(turb, payload):
161 """Handler for Slack interactive view submission
163 Specifically, those that have a payload type of 'view_submission'"""
165 view_id = payload['view']['id']
166 metadata = payload['view']['private_metadata']
168 if view_id in submission_handlers:
169 return submission_handlers[view_id](turb, payload, metadata)
171 print("Error: Unknown view ID: {}".format(view_id))
176 def rot(turb, body, args):
177 """Implementation of the /rot command
179 The args string should be as follows:
181 [count|*] String to be rotated
183 That is, the first word of the string is an optional number (or
184 the character '*'). If this is a number it indicates an amount to
185 rotate each character in the string. If the count is '*' or is not
186 present, then the string will be rotated through all possible 25
189 The result of the rotation is returned (with Slack formatting) in
190 the body of the response so that Slack will provide it as a reply
191 to the user who submitted the slash command."""
193 channel_name = body['channel_name'][0]
194 response_url = body['response_url'][0]
195 channel_id = body['channel_id'][0]
197 result = turbot.rot.rot(args)
199 if (channel_name == "directmessage"):
200 requests.post(response_url,
201 json = {"text": result},
202 headers = {"Content-type": "application/json"})
204 turb.slack_client.chat_postMessage(channel=channel_id, text=result)
211 commands["/rot"] = rot
213 def puzzle(turb, body, args):
214 """Implementation of the /puzzle command
216 The args string is currently ignored (this command will bring up
217 a modal dialog for user input instead)."""
219 channel_id = body['channel_id'][0]
220 trigger_id = body['trigger_id'][0]
222 hunts_table = turb.db.Table("hunts")
223 response = hunts_table.get_item(Key={'channel_id': channel_id})
225 if 'Item' in response:
226 hunt_name = response['Item']['name']
227 hunt_id = response['Item']['hunt_id']
229 return bot_reply("Sorry, this channel doesn't appear to "
230 + "be a hunt channel")
234 "private_metadata": json.dumps({
236 "hunt_channel_id": channel_id
238 "title": {"type": "plain_text", "text": "New Puzzle"},
239 "submit": { "type": "plain_text", "text": "Create" },
241 section_block(text_block("*For {}*".format(hunt_name))),
242 input_block("Puzzle name", "name", "Name of the puzzle"),
243 input_block("Puzzle ID", "puzzle_id",
244 "Used as part of channel name "
245 + "(no spaces nor punctuation)"),
246 input_block("Puzzle URL", "url", "External URL of puzzle",
251 result = turb.slack_client.views_open(trigger_id=trigger_id,
255 submission_handlers[result['view']['id']] = puzzle_submission
261 commands["/puzzle"] = puzzle
263 def puzzle_submission(turb, payload, metadata):
264 """Handler for the user submitting the new puzzle modal
266 This is the modal view presented to the user by the puzzle function
269 meta = json.loads(metadata)
270 hunt_id = meta['hunt_id']
271 hunt_channel_id = meta['hunt_channel_id']
273 state = payload['view']['state']['values']
274 name = state['name']['name']['value']
275 puzzle_id = state['puzzle_id']['puzzle_id']['value']
276 url = state['url']['url']['value']
278 # Validate that the puzzle_id contains no invalid characters
279 if not re.match(valid_id_re, puzzle_id):
280 return submission_error("puzzle_id",
281 "Puzzle ID can only contain letters, "
282 + "numbers, and underscores")
284 # Create a channel for the puzzle
285 hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
288 response = turb.slack_client.conversations_create(
289 name=hunt_dash_channel)
290 except SlackApiError as e:
291 return submission_error("puzzle_id",
292 "Error creating Slack channel: {}"
293 .format(e.response['error']))
295 puzzle_channel_id = response['channel']['id']
297 # Insert the newly-created puzzle into the database
298 table = turb.db.Table(hunt_id)
301 "channel_id": puzzle_channel_id,
303 "status": 'unsolved',
305 "puzzle_id": puzzle_id,