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