-from turbot.blocks import input_block
+from slack.errors import SlackApiError
+from turbot.blocks import input_block, section_block, text_block
+import turbot.rot
import turbot.sheets
+import turbot.slack
import json
import re
import requests
-import turbot.rot
+
+TURBOT_USER_ID = 'U01B9QM4P9R'
+
+actions = {}
+commands = {}
+submission_handlers = {}
+
+# Hunt and Puzzle IDs are restricted to letters, numbers, and underscores
+valid_id_re = r'^[_a-zA-Z0-9]+$'
+
+def bot_reply(message):
+ """Construct a return value suitable for a bot reply
+
+ This is suitable as a way to give an error back to the user who
+ initiated a slash command, for example."""
+
+ return {
+ 'statusCode': 200,
+ 'body': message
+ }
+
+def submission_error(field, error):
+ """Construct an error suitable for returning for an invalid submission.
+
+ Returning this value will prevent a submission and alert the user that
+ the given field is invalid because of the given error."""
+
+ print("Rejecting invalid modal submission: {}".format(error))
+
+ return {
+ 'statusCode': 200,
+ 'headers': {
+ "Content-Type": "application/json"
+ },
+ 'body': json.dumps({
+ "response_action": "errors",
+ "errors": {
+ field: error
+ }
+ })
+ }
def new_hunt(turb, payload):
"""Handler for the action of user pressing the new_hunt button"""
view = {
"type": "modal",
- "private_metadata": "new_hunt",
+ "private_metadata": json.dumps({}),
"title": { "type": "plain_text", "text": "New Hunt" },
"submit": { "type": "plain_text", "text": "Create" },
"blocks": [
'body': 'OK'
}
-def new_hunt_submission(turb, payload):
+actions['button'] = {"new_hunt": new_hunt}
+
+def new_hunt_submission(turb, payload, metadata):
"""Handler for the user submitting the new hunt modal
This is the modal view presented to the user by the new_hunt
url = state['url']['url']['value']
# Validate that the hunt_id contains no invalid characters
- if not re.match(r'[_a-zA-Z0-9]+$', hunt_id):
- print("Hunt ID field is invalid. Attmpting to return a clean error.")
- return {
- 'statusCode': 200,
- 'headers': {
- "Content-Type": "application/json"
- },
- 'body': json.dumps({
- "response_action": "errors",
- "errors": {
- "hunt_id": "Hunt ID can only contain letters, "
- + "numbers, and underscores"
- }
- })
- }
+ if not re.match(valid_id_re, hunt_id):
+ return submission_error("hunt_id",
+ "Hunt ID can only contain letters, "
+ + "numbers, and underscores")
# Create a channel for the hunt
- response = turb.slack_client.conversations_create(name=hunt_id)
+ try:
+ response = turb.slack_client.conversations_create(name=hunt_id)
+ except SlackApiError as e:
+ return submission_error("hunt_id",
+ "Error creating Slack channel: {}"
+ .format(e.response['error']))
if not response['ok']:
- print("Error creating channel for hunt {}: {}"
- .format(name, str(response)))
- return {
- 'statusCode': 400
- }
+ return submission_error("name",
+ "Error occurred creating Slack channel "
+ + "(see CloudWatch log")
user_id = payload['user']['id']
channel_id = response['channel']['id']
text="Sheet created for this hunt: {}"
.format(sheet['url']))
+ # Create a database table for this hunt's puzzles
+ table = turb.db.create_table(
+ TableName=hunt_id,
+ AttributeDefinitions=[
+ {'AttributeName': 'channel_id', 'AttributeType': 'S'}
+ ],
+ KeySchema=[
+ {'AttributeName': 'channel_id', 'KeyType': 'HASH'}
+ ],
+ ProvisionedThroughput={
+ 'ReadCapacityUnits': 5,
+ 'WriteCapacityUnits': 4
+ }
+ )
+
+ # Message the hunt channel that the database is ready
+ turb.slack_client.chat_postMessage(
+ channel=channel_id,
+ text="Welcome to your new hunt! "
+ + "Use `/puzzle` to create puzzles for the hunt.")
+
return {
'statusCode': 200,
}
Specifically, those that have a payload type of 'view_submission'"""
- view_id = payload['view']['private_metadata']
+ view_id = payload['view']['id']
+ metadata = payload['view']['private_metadata']
if view_id in submission_handlers:
- return submission_handlers[view_id](turb, payload)
+ return submission_handlers[view_id](turb, payload, metadata)
print("Error: Unknown view ID: {}".format(view_id))
return {
'body': ""
}
-actions = {
- "button": {
- "new_hunt": new_hunt
+commands["/rot"] = rot
+
+def puzzle(turb, body, args):
+ """Implementation of the /puzzle command
+
+ The args string is currently ignored (this command will bring up
+ a modal dialog for user input instead)."""
+
+ channel_id = body['channel_id'][0]
+ trigger_id = body['trigger_id'][0]
+
+ hunts_table = turb.db.Table("hunts")
+ response = hunts_table.get_item(Key={'channel_id': channel_id})
+
+ if 'Item' in response:
+ hunt_name = response['Item']['name']
+ hunt_id = response['Item']['hunt_id']
+ else:
+ return bot_reply("Sorry, this channel doesn't appear to "
+ + "be a hunt channel")
+
+ view = {
+ "type": "modal",
+ "private_metadata": json.dumps({
+ "hunt_id": hunt_id,
+ "hunt_channel_id": channel_id
+ }),
+ "title": {"type": "plain_text", "text": "New Puzzle"},
+ "submit": { "type": "plain_text", "text": "Create" },
+ "blocks": [
+ section_block(text_block("*For {}*".format(hunt_name))),
+ input_block("Puzzle name", "name", "Name of the puzzle"),
+ input_block("Puzzle ID", "puzzle_id",
+ "Used as part of channel name "
+ + "(no spaces nor punctuation)"),
+ input_block("Puzzle URL", "url", "External URL of puzzle",
+ optional=True)
+ ]
+ }
+
+ result = turb.slack_client.views_open(trigger_id=trigger_id,
+ view=view)
+
+ if (result['ok']):
+ submission_handlers[result['view']['id']] = puzzle_submission
+
+ return {
+ 'statusCode': 200
}
-}
-commands = {
- "/rot": rot
-}
+commands["/puzzle"] = puzzle
+
+def puzzle_submission(turb, payload, metadata):
+ """Handler for the user submitting the new puzzle modal
+
+ This is the modal view presented to the user by the puzzle function
+ above."""
+
+ print("In puzzle_submission\npayload is: {}\nmetadata is {}"
+ .format(payload, metadata))
+
+ meta = json.loads(metadata)
+ hunt_id = meta['hunt_id']
+ hunt_channel_id = meta['hunt_channel_id']
+
+ state = payload['view']['state']['values']
+ name = state['name']['name']['value']
+ puzzle_id = state['puzzle_id']['puzzle_id']['value']
+ url = state['url']['url']['value']
+
+ # Validate that the puzzle_id contains no invalid characters
+ if not re.match(valid_id_re, puzzle_id):
+ return submission_error("puzzle_id",
+ "Puzzle ID can only contain letters, "
+ + "numbers, and underscores")
+
+ # Create a channel for the puzzle
+ hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
+
+ try:
+ response = turb.slack_client.conversations_create(
+ name=hunt_dash_channel)
+ except SlackApiError as e:
+ return submission_error("puzzle_id",
+ "Error creating Slack channel: {}"
+ .format(e.response['error']))
+
+ puzzle_channel_id = response['channel']['id']
+
+ # Create a sheet for the puzzle
+ sheet = turbot.sheets.sheets_create_for_puzzle(turb, hunt_dash_channel)
+
+ # Insert the newly-created puzzle into the database
+ table = turb.db.Table(hunt_id)
+
+ table.put_item(
+ Item={
+ "channel_id": puzzle_channel_id,
+ "solution": [],
+ "status": 'unsolved',
+ "name": name,
+ "puzzle_id": puzzle_id,
+ "url": url,
+ "sheet_url": sheet['url']
+ }
+ )
+
+ # Find all members of the hunt channel
+ members = turbot.slack.slack_channel_members(turb.slack_client,
+ hunt_channel_id)
+
+ # Filter out Turbot's own ID to avoid inviting itself
+ members = [m for m in members if m != TURBOT_USER_ID]
+
+ turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
+ text="Inviting members: {}".format(str(members)))
+
+ # Invite those members to the puzzle channel (in chunks of 500)
+ cursor = 0
+ while cursor < len(members):
+ turb.slack_client.conversations_invite(
+ channel=puzzle_channel_id,
+ users=members[cursor:cursor + 500])
+ cursor += 500
+
+ # Message the channel with the URL of the puzzle's sheet
+ turb.slack_client.chat_postMessage(channel=puzzle_channel_id,
+ text="Sheet created for this puzzle: {}"
+ .format(sheet['url']))
+ return {
+ 'statusCode': 200
+ }