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