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
9 def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id):
10 """Given a hunt_id and puzzle_id, return that puzzle
12 Returns None if no puzzle with the given hunt_id and puzzle_id
13 exists in the database, otherwise a dictionary with all fields
14 from the puzzle's row in the database.
17 response = turb.table.get_item(
20 'SK': 'puzzle-{}'.format(puzzle_id)
23 if 'Item' in response:
24 return response['Item']
28 def find_puzzle_for_url(turb, hunt_id, url):
29 """Given a hunt_id and URL, return the puzzle with that URL
31 Returns None if no puzzle with the given URL exists in the database,
32 otherwise a dictionary with all fields from the puzzle's row in
36 response = turb.table.query(
37 IndexName='url_index',
38 KeyConditionExpression=(
39 Key('hunt_id').eq(hunt_id) &
44 if response['Count'] == 0:
47 return response['Items'][0]
49 def puzzle_blocks(puzzle):
50 """Generate Slack blocks for a puzzle
52 The puzzle argument should be a dictionary as returned from the
53 database. The return value can be used in a Slack command
54 expecting blocks to provide all the details of a puzzle, (its
55 state, solution, links to channel and sheet, etc.).
59 status = puzzle['status']
60 solution = puzzle['solution']
61 channel_id = puzzle['channel_id']
62 url = puzzle.get('url', None)
63 sheet_url = puzzle.get('sheet_url', None)
64 state = puzzle.get('state', None)
68 if status == 'solved':
69 status_emoji = ":ballot_box_with_check:"
71 status_emoji = ":white_square:"
74 solution_str = "*`" + '`, `'.join(solution) + "`*"
78 links.append("<{}|Puzzle>".format(url))
80 links.append("<{}|Sheet>".format(sheet_url))
84 state_str = "\n{}".format(state)
86 puzzle_text = "{}{} <{}|{}> ({}){}".format(
87 status_emoji, solution_str,
88 channel_url(channel_id), name,
89 ', '.join(links), state_str
92 # Combining hunt ID and puzzle ID together here is safe because
93 # both IDs are restricted to not contain a hyphen, (see
94 # valid_id_re in interaction.py)
95 hunt_and_puzzle = "{}-{}".format(puzzle['hunt_id'], puzzle['puzzle_id'])
99 section_block(text_block(puzzle_text)),
100 button_block("✏", "edit_puzzle", hunt_and_puzzle)
104 def puzzle_matches_one(puzzle, pattern):
105 """Returns True if this puzzle matches the given string (regexp)
107 A match will be considered on any of puzzle title, round title,
108 puzzle URL, puzzle state, or solution string. The string can
109 include regular expression syntax. Matching is case insensitive.
112 p = re.compile('.*'+pattern+'.*', re.IGNORECASE)
114 if p.match(puzzle['name']):
117 if 'rounds' in puzzle:
118 for round in puzzle['rounds']:
123 if p.match(puzzle['url']):
126 if 'state' in puzzle:
127 if p.match(puzzle['state']):
130 if 'solution' in puzzle:
131 for solution in puzzle['solution']:
132 if p.match(solution):
137 def puzzle_matches_all(puzzle, patterns):
138 """Returns True if this puzzle matches all of the given list of patterns
140 A match will be considered on any of puzzle title, round title,
141 puzzle URL, puzzle state, or solution string. All patterns must
142 match the puzzle somewhere, (that is, there is an implicit logical
143 AND between patterns). Patterns can include regular expression
144 syntax. Matching is case insensitive.
147 for pattern in patterns:
148 if not puzzle_matches_one(puzzle, pattern):
153 def puzzle_id_from_name(name):
154 return re.sub(r'[^a-zA-Z0-9_]', '', name).lower()
156 def puzzle_update_channel_and_sheet(turb, puzzle):
158 channel_id = puzzle['channel_id']
159 name = puzzle['name']
160 url = puzzle.get('url', None)
161 sheet_url = puzzle.get('sheet_url', None)
162 state = puzzle.get('state', None)
163 status = puzzle['status']
167 if status == 'solved':
168 topic += "SOLVED: `{}` ".format('`, `'.join(puzzle['solution']))
174 links.append("<{}|Puzzle>".format(url))
176 links.append("<{}|Sheet>".format(sheet_url))
179 topic += "({})".format(', '.join(links))
182 topic += " {}".format(state)
184 # Slack only allows 250 characters for a topic
186 topic = topic[:247] + "..."
188 turb.slack_client.conversations_setTopic(channel=channel_id,
191 # Rename the sheet to include indication of solved/solution status
192 sheet_name = puzzle['name']
193 if puzzle['status'] == 'solved':
194 sheet_name += " - Solved {}".format(", ".join(puzzle['solution']))
196 turbot.sheets.renameSheet(turb, puzzle['sheet_url'], sheet_name)
198 # Finally, rename the Slack channel to reflect the latest name and
201 # Note: We don't use puzzle['hunt_id'] here because we're keeping
202 # that as a persistent identifier in the database. Instead we
203 # create a new ID-like identifier from the current name.
204 channel_name = "{}-{}".format(
206 puzzle_id_from_name(puzzle['name'])
208 if puzzle['status'] == 'solved':
209 channel_name += "-solved"
211 turb.slack_client.conversations_rename(
212 channel=puzzle['channel_id'],