From 5e9eac54ab60e977a067760f1dc44f1e23f3b311 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Fri, 1 Jan 2021 10:17:35 -0800 Subject: [PATCH 1/1] Refuse to create a new puzzle with the same URL as an existing puzzle Just a simple safety check when multiple people might be creating the same puzzle at roughly the same time. And one less item left on the TODO list. --- TODO | 3 --- turbot/interaction.py | 26 +++++++++++++++++++++++++- turbot/puzzle.py | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 turbot/puzzle.py diff --git a/TODO b/TODO index edbafc5..67a4830 100644 --- a/TODO +++ b/TODO @@ -9,9 +9,6 @@ Low-hanging fruit [These are not highest priority, but are so easy that they are worth not delaying until after bigger features below.] -• Fix /puzzle dialog to reject a puzzle with the same URL as an - existing puzzle. - • Make /solved clear the state string for a puzzle. Round management diff --git a/turbot/interaction.py b/turbot/interaction.py index 76eb061..f2a35ee 100644 --- a/turbot/interaction.py +++ b/turbot/interaction.py @@ -3,6 +3,7 @@ from turbot.blocks import ( input_block, section_block, text_block, multi_select_block ) from turbot.hunt import find_hunt_for_hunt_id +from turbot.puzzle import find_puzzle_for_url import turbot.rot import turbot.sheets import turbot.slack @@ -119,13 +120,14 @@ def new_hunt_submission(turb, payload, metadata): TableName='turbot', KeySchema=[ {'AttributeName': 'hunt_id', 'KeyType': 'HASH'}, - {'AttributeName': 'SK', 'KeyType': 'RANGE'}, + {'AttributeName': 'SK', 'KeyType': 'RANGE'} ], AttributeDefinitions=[ {'AttributeName': 'hunt_id', 'AttributeType': 'S'}, {'AttributeName': 'SK', 'AttributeType': 'S'}, {'AttributeName': 'channel_id', 'AttributeType': 'S'}, {'AttributeName': 'is_hunt', 'AttributeType': 'S'}, + {'AttributeName': 'url', 'AttributeType': 'S'} ], ProvisionedThroughput={ 'ReadCapacityUnits': 5, @@ -158,6 +160,18 @@ def new_hunt_submission(turb, payload, metadata): 'WriteCapacityUnits': 5 } } + ], + LocalSecondaryIndexes = [ + { + 'IndexName': 'url_index', + 'KeySchema': [ + {'AttributeName': 'hunt_id', 'KeyType': 'HASH'}, + {'AttributeName': 'url', 'KeyType': 'RANGE'}, + ], + 'Projection': { + 'ProjectionType': 'ALL' + } + } ] ) return submission_error( @@ -409,6 +423,7 @@ def puzzle_submission(turb, payload, metadata): This is the modal view presented to the user by the puzzle function above.""" + # First, read all the various data from the request meta = json.loads(metadata) hunt_id = meta['hunt_id'] @@ -422,6 +437,15 @@ def puzzle_submission(turb, payload, metadata): rounds = [] new_rounds = state['new_rounds']['new_rounds']['value'] + # Before doing anything, reject this puzzle if a puzzle already + # exists with the same URL. + if url: + existing = find_puzzle_for_url(turb, hunt_id, url) + if existing: + return submission_error( + "url", + "Error: A puzzle with this URL already exists.") + # Create a Slack-channel-safe puzzle_id puzzle_id = re.sub(r'[^a-zA-Z0-9_]', '', name).lower() diff --git a/turbot/puzzle.py b/turbot/puzzle.py new file mode 100644 index 0000000..9f9b1c6 --- /dev/null +++ b/turbot/puzzle.py @@ -0,0 +1,22 @@ +from boto3.dynamodb.conditions import Key + +def find_puzzle_for_url(turb, hunt_id, url): + """Given a hunt_id and URL, return the puzzle with that URL + + Returns None if no puzzle with the given URL exists in the database, + otherwise a dictionary with all fields from the puzzle's row in + the database. + """ + + response = turb.table.query( + IndexName='url_index', + KeyConditionExpression=( + Key('hunt_id').eq(hunt_id) & + Key('url').eq(url) + ) + ) + + if response['Count'] == 0: + return None + + return response['Items'][0] -- 2.43.0