]> git.cworth.org Git - turbot/blob - turbot/hunt.py
Implement a /round command
[turbot] / turbot / hunt.py
1 from turbot.blocks import section_block, text_block, divider_block
2 from turbot.round import round_blocks
3 from turbot.puzzle import puzzle_blocks, puzzle_matches_all
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
6
7 def find_hunt_for_hunt_id(turb, hunt_id):
8     """Given a hunt ID find the database item for that hunt
9
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.).
13     """
14
15     response = turb.table.get_item(
16         Key={
17             'hunt_id': hunt_id,
18             'SK': 'hunt-{}'.format(hunt_id)
19         })
20
21     if 'Item' in response:
22         return response['Item']
23     else:
24         return None
25
26 def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[],
27                 limit_to_rounds=None):
28     """Generate Slack blocks for a hunt
29
30     The hunt argument should be a dictionary as returned from the
31     database.
32
33     Three optional arguments can be used to filter which puzzles to
34     include in the result:
35
36       puzzle_status: If either 'solved' or 'unsolved' only puzzles
37                      with that status will be included in the
38                      result. If any other value, all puzzles in the
39                      hunt will be considered.
40
41       search_terms: A list of search terms. Only puzzles that match
42                     all of these terms will be included in the
43                     result. A match will be considered on any of
44                     puzzle title, round title, puzzle URL, puzzle
45                     state or solution string. Terms can include
46                     regular expression syntax.
47
48       limit_to_rounds: A list of rounds. If provided only the given
49                        rounds will be included in the output. Note:
50                        an empty list means to display only puzzles
51                        assigned to no rounds, while an argument of
52                        None means to display all puzzles with no
53                        limit on the rounds.
54
55     The return value can be used in a Slack command expecting blocks to
56     provide all the details of a hunt, (puzzles, their state,
57     solution, links to channels and sheets, etc.).
58
59     """
60
61     name = hunt['name']
62     hunt_id = hunt['hunt_id']
63     channel_id = hunt['channel_id']
64
65     response = turb.table.query(
66         KeyConditionExpression=(
67             Key('hunt_id').eq(hunt_id) &
68             Key('SK').begins_with('puzzle-')
69         )
70     )
71     puzzles = response['Items']
72
73     # Filter the set of puzzles according the the requested puzzle_status
74     if puzzle_status in ('solved', 'unsolved'):
75         puzzles = [p for p in puzzles if p['status'] == puzzle_status]
76     else:
77         puzzle_status = 'all'
78
79     if search_terms:
80         puzzles = [puzzle for puzzle in puzzles
81                    if puzzle_matches_all(puzzle, search_terms)]
82
83     # Compute the set of rounds across all puzzles
84     rounds = set()
85     for puzzle in puzzles:
86         if 'rounds' not in puzzle:
87             continue
88         for round in puzzle['rounds']:
89             rounds.add(round)
90
91     hunt_text = "*{} puzzles in hunt <{}|{}>*".format(
92         puzzle_status.capitalize(),
93         channel_url(channel_id),
94         name
95     )
96     if limit_to_rounds is not None:
97         hunt_text += " *in round{}: {}*".format(
98             "s" if len(limit_to_rounds) > 1 else "",
99             ", ".join(limit_to_rounds) if limit_to_rounds else "<no round>"
100         )
101     if search_terms:
102         quoted_terms = ['`{}`'.format(term) for term in search_terms]
103         hunt_text += " matching {}".format(" AND ".join(quoted_terms))
104
105     blocks = [
106         section_block(text_block(hunt_text)),
107     ]
108
109     if not len(puzzles):
110         blocks += [
111             section_block(text_block("No puzzles found."))
112         ]
113
114     # Construct blocks for each round
115     for round in rounds:
116         if limit_to_rounds is not None and round not in limit_to_rounds:
117             continue
118         # If we're only displaying one round the round header is redundant
119         if limit_to_rounds and len(limit_to_rounds) == 1:
120             blocks += round_blocks(round, puzzles, omit_header=True)
121         else:
122             blocks += round_blocks(round, puzzles)
123
124     # Also blocks for any puzzles not in any round
125     stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
126
127     # For this condition, either limit_to_rounds is None which
128     # means we definitely want to display these stray puzzles
129     # (since we are not limiting), _OR_ limit_to_rounds is not
130     # None but is a zero-length array, meaning we are limiting
131     # to rounds but specifically the round of unassigned puzzles
132     if len(stray_puzzles) and not limit_to_rounds:
133         stray_text = "*Puzzles with no assigned round*"
134         blocks.append(section_block(text_block(stray_text)))
135         for puzzle in stray_puzzles:
136             blocks += puzzle_blocks(puzzle)
137
138     blocks.append(divider_block())
139
140     return blocks