]> git.cworth.org Git - turbot/blob - turbot/interaction.py
Defer all sheet creation until after the channel is created
[turbot] / turbot / interaction.py
1 from slack.errors import SlackApiError
2 from turbot.blocks import input_block, section_block, text_block
3 import turbot.rot
4 import turbot.sheets
5 import turbot.slack
6 import json
7 import re
8 import requests
9 from botocore.exceptions import ClientError
10
11 actions = {}
12 commands = {}
13 submission_handlers = {}
14
15 # Hunt and Puzzle IDs are restricted to letters, numbers, and underscores
16 valid_id_re = r'^[_a-zA-Z0-9]+$'
17
18 def bot_reply(message):
19     """Construct a return value suitable for a bot reply
20
21     This is suitable as a way to give an error back to the user who
22     initiated a slash command, for example."""
23
24     return {
25         'statusCode': 200,
26         'body': message
27     }
28
29 def submission_error(field, error):
30     """Construct an error suitable for returning for an invalid submission.
31
32     Returning this value will prevent a submission and alert the user that
33     the given field is invalid because of the given error."""
34
35     print("Rejecting invalid modal submission: {}".format(error))
36
37     return {
38         'statusCode': 200,
39         'headers': {
40             "Content-Type": "application/json"
41         },
42         'body': json.dumps({
43             "response_action": "errors",
44             "errors": {
45                 field: error
46             }
47         })
48     }
49
50 def new_hunt(turb, payload):
51     """Handler for the action of user pressing the new_hunt button"""
52
53     view = {
54         "type": "modal",
55         "private_metadata": json.dumps({}),
56         "title": { "type": "plain_text", "text": "New Hunt" },
57         "submit": { "type": "plain_text", "text": "Create" },
58         "blocks": [
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",
64                         optional=True)
65         ],
66     }
67
68     result = turb.slack_client.views_open(trigger_id=payload['trigger_id'],
69                                           view=view)
70     if (result['ok']):
71         submission_handlers[result['view']['id']] = new_hunt_submission
72
73     return {
74         'statusCode': 200,
75         'body': 'OK'
76     }
77
78 actions['button'] = {"new_hunt": new_hunt}
79
80 def new_hunt_submission(turb, payload, metadata):
81     """Handler for the user submitting the new hunt modal
82
83     This is the modal view presented to the user by the new_hunt
84     function above."""
85
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']
91
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")
97
98     # Check to see if the hunts table exists
99     hunts_table = turb.db.Table("hunts")
100
101     try:
102         exists = hunts_table.table_status in ("CREATING", "UPDATING",
103                                               "ACTIVE")
104     except ClientError:
105         exists = False
106
107     # Create the hunts table if necessary.
108     if not exists:
109         hunts_table = turb.db.create_table(
110             TableName='hunts',
111             KeySchema=[
112                 {'AttributeName': 'channel_id', 'KeyType': 'HASH'},
113             ],
114             AttributeDefinitions=[
115                 {'AttributeName': 'channel_id', 'AttributeType': 'S'},
116             ],
117             ProvisionedThroughput={
118                 'ReadCapacityUnits': 5,
119                 'WriteCapacityUnits': 5
120             }
121         )
122         return submission_error("hunt_id",
123                                 "Still bootstrapping hunts table. Try again.")
124
125     # Create a channel for the hunt
126     try:
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']))
132
133     channel_id = response['channel']['id']
134
135     # Create a sheet for the channel
136     sheet = turbot.sheets.sheets_create(turb, hunt_id)
137
138     channel_id = response['channel']['id']
139
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(
144         Item={
145             'channel_id': channel_id,
146             "active": False,
147             "name": name,
148             "hunt_id": hunt_id,
149             "url": url
150         }
151     )
152
153     # Invite the initiating user to the channel
154     turb.slack_client.conversations_invite(channel=channel_id, users=user_id)
155
156     return {
157         'statusCode': 200,
158     }
159
160 def view_submission(turb, payload):
161     """Handler for Slack interactive view submission
162
163     Specifically, those that have a payload type of 'view_submission'"""
164
165     view_id = payload['view']['id']
166     metadata = payload['view']['private_metadata']
167
168     if view_id in submission_handlers:
169         return submission_handlers[view_id](turb, payload, metadata)
170
171     print("Error: Unknown view ID: {}".format(view_id))
172     return {
173         'statusCode': 400
174     }
175
176 def rot(turb, body, args):
177     """Implementation of the /rot command
178
179     The args string should be as follows:
180
181         [count|*] String to be rotated
182
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
187     values.
188
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."""
192
193     channel_name = body['channel_name'][0]
194     response_url = body['response_url'][0]
195     channel_id = body['channel_id'][0]
196
197     result = turbot.rot.rot(args)
198
199     if (channel_name == "directmessage"):
200         requests.post(response_url,
201                       json = {"text": result},
202                       headers = {"Content-type": "application/json"})
203     else:
204         turb.slack_client.chat_postMessage(channel=channel_id, text=result)
205
206     return {
207         'statusCode': 200,
208         'body': ""
209     }
210
211 commands["/rot"] = rot
212
213 def puzzle(turb, body, args):
214     """Implementation of the /puzzle command
215
216     The args string is currently ignored (this command will bring up
217     a modal dialog for user input instead)."""
218
219     channel_id = body['channel_id'][0]
220     trigger_id = body['trigger_id'][0]
221
222     hunts_table = turb.db.Table("hunts")
223     response = hunts_table.get_item(Key={'channel_id': channel_id})
224
225     if 'Item' in response:
226         hunt_name = response['Item']['name']
227         hunt_id = response['Item']['hunt_id']
228     else:
229         return bot_reply("Sorry, this channel doesn't appear to "
230                          + "be a hunt channel")
231
232     view = {
233         "type": "modal",
234         "private_metadata": json.dumps({
235             "hunt_id": hunt_id,
236             "hunt_channel_id": channel_id
237         }),
238         "title": {"type": "plain_text", "text": "New Puzzle"},
239         "submit": { "type": "plain_text", "text": "Create" },
240         "blocks": [
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",
247                         optional=True)
248         ]
249     }
250
251     result = turb.slack_client.views_open(trigger_id=trigger_id,
252                                           view=view)
253
254     if (result['ok']):
255         submission_handlers[result['view']['id']] = puzzle_submission
256
257     return {
258         'statusCode': 200
259     }
260
261 commands["/puzzle"] = puzzle
262
263 def puzzle_submission(turb, payload, metadata):
264     """Handler for the user submitting the new puzzle modal
265
266     This is the modal view presented to the user by the puzzle function
267     above."""
268
269     meta = json.loads(metadata)
270     hunt_id = meta['hunt_id']
271     hunt_channel_id = meta['hunt_channel_id']
272
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']
277
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")
283
284     # Create a channel for the puzzle
285     hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
286
287     try:
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']))
294
295     puzzle_channel_id = response['channel']['id']
296
297     # Insert the newly-created puzzle into the database
298     table = turb.db.Table(hunt_id)
299     table.put_item(
300         Item={
301             "channel_id": puzzle_channel_id,
302             "solution": [],
303             "status": 'unsolved',
304             "name": name,
305             "puzzle_id": puzzle_id,
306             "url": url,
307         }
308     )
309
310     return {
311         'statusCode': 200
312     }