]> git.cworth.org Git - turbot/blob - turbot/puzzle.py
Implement a dialog box to edit a puzzle
[turbot] / turbot / puzzle.py
1 from turbot.blocks import (
2     section_block, text_block, button_block, accessory_block
3 )
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
6 import re
7
8 def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id):
9     """Given a hunt_id and puzzle_id, return that puzzle
10
11     Returns None if no puzzle with the given hunt_id and puzzle_id
12     exists in the database, otherwise a dictionary with all fields
13     from the puzzle's row in the database.
14     """
15
16     response = turb.table.get_item(
17         Key={
18             'hunt_id': hunt_id,
19             'SK': 'puzzle-{}'.format(puzzle_id)
20         })
21
22     if 'Item' in response:
23         return response['Item']
24     else:
25         return None
26
27 def find_puzzle_for_url(turb, hunt_id, url):
28     """Given a hunt_id and URL, return the puzzle with that URL
29
30     Returns None if no puzzle with the given URL exists in the database,
31     otherwise a dictionary with all fields from the puzzle's row in
32     the database.
33     """
34
35     response = turb.table.query(
36         IndexName='url_index',
37         KeyConditionExpression=(
38             Key('hunt_id').eq(hunt_id) &
39             Key('url').eq(url)
40         )
41     )
42
43     if response['Count'] == 0:
44         return None
45
46     return response['Items'][0]
47
48 def puzzle_blocks(puzzle):
49     """Generate Slack blocks for a puzzle
50
51     The puzzle argument should be a dictionary as returned from the
52     database. The return value can be used in a Slack command
53     expecting blocks to provide all the details of a puzzle, (its
54     state, solution, links to channel and sheet, etc.).
55     """
56
57     name = puzzle['name']
58     status = puzzle['status']
59     solution = puzzle['solution']
60     channel_id = puzzle['channel_id']
61     url = puzzle.get('url', None)
62     sheet_url = puzzle.get('sheet_url', None)
63     state = puzzle.get('state', None)
64     status_emoji = ''
65     solution_str = ''
66
67     if status == 'solved':
68         status_emoji = ":ballot_box_with_check:"
69     else:
70         status_emoji = ":white_square:"
71
72     if len(solution):
73         solution_str = "*`" + '`, `'.join(solution) + "`*"
74
75     links = []
76     if url:
77         links.append("<{}|Puzzle>".format(url))
78     if sheet_url:
79         links.append("<{}|Sheet>".format(sheet_url))
80
81     state_str = ''
82     if state:
83         state_str = "\n{}".format(state)
84
85     puzzle_text = "{}{} <{}|{}> ({}){}".format(
86         status_emoji, solution_str,
87         channel_url(channel_id), name,
88         ', '.join(links), state_str
89     )
90
91     # Combining hunt ID and puzzle ID together here is safe because
92     # both IDs are restricted to not contain a hyphen, (see
93     # valid_id_re in interaction.py)
94     hunt_and_puzzle = "{}-{}".format(puzzle['hunt_id'], puzzle['puzzle_id'])
95
96     return [
97         accessory_block(
98             section_block(text_block(puzzle_text)),
99             button_block("✏", "edit_puzzle", hunt_and_puzzle)
100         )
101     ]
102
103 def puzzle_matches_one(puzzle, pattern):
104     """Returns True if this puzzle matches the given string (regexp)
105
106     A match will be considered on any of puzzle title, round title,
107     puzzle URL, puzzle state, or solution string. The string can
108     include regular expression syntax. Matching is case insensitive.
109     """
110
111     p = re.compile('.*'+pattern+'.*', re.IGNORECASE)
112
113     if p.match(puzzle['name']):
114         return True
115
116     if 'rounds' in puzzle:
117         for round in puzzle['rounds']:
118             if p.match(round):
119                 return True
120
121     if 'url' in puzzle:
122         if p.match(puzzle['url']):
123             return True
124
125     if 'state' in puzzle:
126         if p.match(puzzle['state']):
127             return True
128
129     if 'solution' in puzzle:
130         for solution in puzzle['solution']:
131             if p.match(solution):
132                 return True
133
134     return False
135
136 def puzzle_matches_all(puzzle, patterns):
137     """Returns True if this puzzle matches all of the given list of patterns
138
139     A match will be considered on any of puzzle title, round title,
140     puzzle URL, puzzle state, or solution string. All patterns must
141     match the puzzle somewhere, (that is, there is an implicit logical
142     AND between patterns). Patterns can include regular expression
143     syntax. Matching is case insensitive.
144     """
145
146     for pattern in patterns:
147         if not puzzle_matches_one(puzzle, pattern):
148             return False
149
150     return True