1 from turbot.blocks import (
2 section_block, text_block, button_block, accessory_block
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
8 def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id):
9 """Given a hunt_id and puzzle_id, return that puzzle
11 Returns None if no puzzle with the given hunt_id and puzzle_id
12 exists in the database, otherwise a dictionary with all fields
13 from the puzzle's row in the database.
16 response = turb.table.get_item(
19 'SK': 'puzzle-{}'.format(puzzle_id)
22 if 'Item' in response:
23 return response['Item']
27 def find_puzzle_for_url(turb, hunt_id, url):
28 """Given a hunt_id and URL, return the puzzle with that URL
30 Returns None if no puzzle with the given URL exists in the database,
31 otherwise a dictionary with all fields from the puzzle's row in
35 response = turb.table.query(
36 IndexName='url_index',
37 KeyConditionExpression=(
38 Key('hunt_id').eq(hunt_id) &
43 if response['Count'] == 0:
46 return response['Items'][0]
48 def puzzle_blocks(puzzle):
49 """Generate Slack blocks for a puzzle
51 The puzzle argument should be a dictionary as returned from the
52 database. The return value can be used in a Slack command
53 expecting blocks to provide all the details of a puzzle, (its
54 state, solution, links to channel and sheet, etc.).
58 status = puzzle['status']
59 solution = puzzle['solution']
60 channel_id = puzzle['channel_id']
61 url = puzzle.get('url', None)
62 sheet_url = puzzle.get('sheet_url', None)
63 state = puzzle.get('state', None)
67 if status == 'solved':
68 status_emoji = ":ballot_box_with_check:"
70 status_emoji = ":white_square:"
73 solution_str = "*`" + '`, `'.join(solution) + "`*"
77 links.append("<{}|Puzzle>".format(url))
79 links.append("<{}|Sheet>".format(sheet_url))
83 state_str = "\n{}".format(state)
85 puzzle_text = "{}{} <{}|{}> ({}){}".format(
86 status_emoji, solution_str,
87 channel_url(channel_id), name,
88 ', '.join(links), state_str
91 # Combining hunt ID and puzzle ID together here is safe because
92 # both IDs are restricted to not contain a hyphen, (see
93 # valid_id_re in interaction.py)
94 hunt_and_puzzle = "{}-{}".format(puzzle['hunt_id'], puzzle['puzzle_id'])
98 section_block(text_block(puzzle_text)),
99 button_block("✏", "edit_puzzle", hunt_and_puzzle)
103 def puzzle_matches_one(puzzle, pattern):
104 """Returns True if this puzzle matches the given string (regexp)
106 A match will be considered on any of puzzle title, round title,
107 puzzle URL, puzzle state, or solution string. The string can
108 include regular expression syntax. Matching is case insensitive.
111 p = re.compile('.*'+pattern+'.*', re.IGNORECASE)
113 if p.match(puzzle['name']):
116 if 'rounds' in puzzle:
117 for round in puzzle['rounds']:
122 if p.match(puzzle['url']):
125 if 'state' in puzzle:
126 if p.match(puzzle['state']):
129 if 'solution' in puzzle:
130 for solution in puzzle['solution']:
131 if p.match(solution):
136 def puzzle_matches_all(puzzle, patterns):
137 """Returns True if this puzzle matches all of the given list of patterns
139 A match will be considered on any of puzzle title, round title,
140 puzzle URL, puzzle state, or solution string. All patterns must
141 match the puzzle somewhere, (that is, there is an implicit logical
142 AND between patterns). Patterns can include regular expression
143 syntax. Matching is case insensitive.
146 for pattern in patterns:
147 if not puzzle_matches_one(puzzle, pattern):