1 from turbot.blocks import section_block, text_block, divider_block
2 from turbot.round import round_blocks
3 from turbot.puzzle import puzzle_block, puzzle_matches_all
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
7 def find_hunt_for_hunt_id(turb, hunt_id):
8 """Given a hunt ID find the database item for that hunt
10 Returns None if hunt ID is not found, otherwise a
11 dictionary with all fields from the hunt's row in the table,
12 (channel_id, active, hunt_id, name, url, sheet_url, etc.).
15 response = turb.table.get_item(
18 'SK': 'hunt-{}'.format(hunt_id)
21 if 'Item' in response:
22 return response['Item']
26 def quote_if_has_space(term):
28 return '"{}"'.format(term)
32 def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
33 """Generate Slack blocks for a hunt
35 The hunt argument should be a dictionary as returned from the
38 Two optional arguments can be used to filter which puzzles to
39 include in the result:
41 puzzle_status: If either 'solved' or 'unsolved' only puzzles
42 with that status will be included in the
43 result. If any other value, all puzzles in the
44 hunt will be considered.
46 search_terms: A list of search terms. Only puzzles that match
47 all of these terms will be included in the
48 result. A match will be considered on any of
49 puzzle title, round title, puzzle URL, puzzle
50 state or solution string. Terms can include
51 regular expression syntax.
53 The return value can be used in a Slack command expecting blocks to
54 provide all the details of a hunt, (puzzles, their state,
55 solution, links to channels and sheets, etc.).
60 hunt_id = hunt['hunt_id']
61 channel_id = hunt['channel_id']
63 response = turb.table.query(
64 KeyConditionExpression=(
65 Key('hunt_id').eq(hunt_id) &
66 Key('SK').begins_with('puzzle-')
69 puzzles = response['Items']
71 # Filter the set of puzzles according the the requested puzzle_status
72 if puzzle_status in ('solved', 'unsolved'):
73 puzzles = [p for p in puzzles if p['status'] == puzzle_status]
78 puzzles = [puzzle for puzzle in puzzles
79 if puzzle_matches_all(puzzle, search_terms)]
81 # Compute the set of rounds across all puzzles
83 for puzzle in puzzles:
84 if 'rounds' not in puzzle:
86 for round in puzzle['rounds']:
89 hunt_text = "*{}* puzzles in hunt <{}|{}>".format(
90 puzzle_status.capitalize(),
91 channel_url(channel_id),
95 quoted_terms = [quote_if_has_space(term) for term in search_terms]
96 hunt_text += " matching {}".format(" AND ".join(quoted_terms))
99 section_block(text_block(hunt_text)),
104 section_block(text_block("No puzzles found."))
107 # Construct blocks for each round
109 blocks += round_blocks(round, puzzles)
111 # Also blocks for any puzzles not in any round
112 stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
113 if len(stray_puzzles):
114 stray_text = "*Puzzles with no assigned round*"
115 blocks.append(section_block(text_block(stray_text)))
116 for puzzle in stray_puzzles:
117 blocks.append(puzzle_block(puzzle))
119 blocks.append(divider_block())