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 hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
27 """Generate Slack blocks for a hunt
29 The hunt argument should be a dictionary as returned from the
32 Two optional arguments can be used to filter which puzzles to
33 include in the result:
35 puzzle_status: If either 'solved' or 'unsolved' only puzzles
36 with that status will be included in the
37 result. If any other value, all puzzles in the
38 hunt will be considered.
40 search_terms: A list of search terms. Only puzzles that match
41 all of these terms will be included in the
42 result. A match will be considered on any of
43 puzzle title, round title, puzzle URL, puzzle
44 state or solution string. Terms can include
45 regular expression syntax.
47 The return value can be used in a Slack command expecting blocks to
48 provide all the details of a hunt, (puzzles, their state,
49 solution, links to channels and sheets, etc.).
54 hunt_id = hunt['hunt_id']
55 channel_id = hunt['channel_id']
57 response = turb.table.query(
58 KeyConditionExpression=(
59 Key('hunt_id').eq(hunt_id) &
60 Key('SK').begins_with('puzzle-')
63 puzzles = response['Items']
65 # Filter the set of puzzles according the the requested puzzle_status
66 if puzzle_status in ('solved', 'unsolved'):
67 puzzles = [p for p in puzzles if p['status'] == puzzle_status]
72 puzzles = [puzzle for puzzle in puzzles
73 if puzzle_matches_all(puzzle, search_terms)]
75 # Compute the set of rounds across all puzzles
77 for puzzle in puzzles:
78 if 'rounds' not in puzzle:
80 for round in puzzle['rounds']:
83 hunt_text = "*{} puzzles in hunt <{}|{}>*".format(
84 puzzle_status.capitalize(),
85 channel_url(channel_id),
89 quoted_terms = ['`{}`'.format(term) for term in search_terms]
90 hunt_text += " matching {}".format(" AND ".join(quoted_terms))
93 section_block(text_block(hunt_text)),
98 section_block(text_block("No puzzles found."))
101 # Construct blocks for each round
103 blocks += round_blocks(round, puzzles)
105 # Also blocks for any puzzles not in any round
106 stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
107 if len(stray_puzzles):
108 stray_text = "*Puzzles with no assigned round*"
109 blocks.append(section_block(text_block(stray_text)))
110 for puzzle in stray_puzzles:
111 blocks.append(puzzle_block(puzzle))
113 blocks.append(divider_block())