+
+def puzzle_blocks(puzzle):
+ """Generate Slack blocks for a puzzle
+
+ The puzzle argument should be a dictionary as returned from the
+ database. The return value can be used in a Slack command
+ expecting blocks to provide all the details of a puzzle, (its
+ state, solution, links to channel and sheet, etc.).
+ """
+
+ name = puzzle['name']
+ status = puzzle['status']
+ solution = puzzle['solution']
+ channel_id = puzzle['channel_id']
+ url = puzzle.get('url', None)
+ sheet_url = puzzle.get('sheet_url', None)
+ state = puzzle.get('state', None)
+ status_emoji = ''
+ solution_str = ''
+
+ if status == 'solved':
+ status_emoji = ":ballot_box_with_check:"
+ else:
+ status_emoji = ":white_square:"
+
+ if len(solution):
+ solution_str = "*`" + '`, `'.join(solution) + "`*"
+
+ links = []
+ if url:
+ links.append("<{}|Puzzle>".format(url))
+ if sheet_url:
+ links.append("<{}|Sheet>".format(sheet_url))
+
+ state_str = ''
+ if state:
+ state_str = "\n{}".format(state)
+
+ puzzle_text = "{}{} <{}|{}> ({}){}".format(
+ status_emoji, solution_str,
+ channel_url(channel_id), name,
+ ', '.join(links), state_str
+ )
+
+ # Combining hunt ID and puzzle ID together here is safe because
+ # both IDs are restricted to not contain a hyphen, (see
+ # valid_id_re in interaction.py)
+ hunt_and_puzzle = "{}-{}".format(puzzle['hunt_id'], puzzle['puzzle_id'])
+
+ return [
+ accessory_block(
+ section_block(text_block(puzzle_text)),
+ button_block("✏", "edit_puzzle", hunt_and_puzzle)
+ )
+ ]
+
+def puzzle_matches_one(puzzle, pattern):
+ """Returns True if this puzzle matches the given string (regexp)
+
+ A match will be considered on any of puzzle title, round title,
+ puzzle URL, puzzle state, or solution string. The string can
+ include regular expression syntax. Matching is case insensitive.
+ """
+
+ p = re.compile('.*'+pattern+'.*', re.IGNORECASE)
+
+ if p.match(puzzle['name']):
+ return True
+
+ if 'rounds' in puzzle:
+ for round in puzzle['rounds']:
+ if p.match(round):
+ return True
+
+ if 'url' in puzzle:
+ if p.match(puzzle['url']):
+ return True
+
+ if 'state' in puzzle:
+ if p.match(puzzle['state']):
+ return True
+
+ if 'solution' in puzzle:
+ for solution in puzzle['solution']:
+ if p.match(solution):
+ return True
+
+ return False
+
+def puzzle_matches_all(puzzle, patterns):
+ """Returns True if this puzzle matches all of the given list of patterns
+
+ A match will be considered on any of puzzle title, round title,
+ puzzle URL, puzzle state, or solution string. All patterns must
+ match the puzzle somewhere, (that is, there is an implicit logical
+ AND between patterns). Patterns can include regular expression
+ syntax. Matching is case insensitive.
+ """
+
+ for pattern in patterns:
+ if not puzzle_matches_one(puzzle, pattern):
+ return False
+
+ return True
+
+def puzzle_id_from_name(name):
+ return re.sub(r'[^a-zA-Z0-9_]', '', name).lower()
+
+def puzzle_update_channel_and_sheet(turb, puzzle):
+
+ channel_id = puzzle['channel_id']
+ name = puzzle['name']
+ url = puzzle.get('url', None)
+ sheet_url = puzzle.get('sheet_url', None)
+ state = puzzle.get('state', None)
+ status = puzzle['status']
+
+ topic = ''
+
+ if status == 'solved':
+ topic += "SOLVED: `{}` ".format('`, `'.join(puzzle['solution']))
+
+ topic += name
+
+ links = []
+ if url:
+ links.append("<{}|Puzzle>".format(url))
+ if sheet_url:
+ links.append("<{}|Sheet>".format(sheet_url))
+
+ if len(links):
+ topic += "({})".format(', '.join(links))
+
+ if state:
+ topic += " {}".format(state)
+
+ # Slack only allows 250 characters for a topic
+ if len(topic) > 250:
+ topic = topic[:247] + "..."
+
+ turb.slack_client.conversations_setTopic(channel=channel_id,
+ topic=topic)
+
+ # Rename the sheet to include indication of solved/solution status
+ sheet_name = puzzle['name']
+ if puzzle['status'] == 'solved':
+ sheet_name += " - Solved {}".format(", ".join(puzzle['solution']))
+
+ turbot.sheets.renameSheet(turb, puzzle['sheet_url'], sheet_name)
+
+ # Finally, rename the Slack channel to reflect the latest name and
+ # the solved status
+ #
+ # Note: We don't use puzzle['hunt_id'] here because we're keeping
+ # that as a persistent identifier in the database. Instead we
+ # create a new ID-like identifier from the current name.
+ channel_name = "{}-{}".format(
+ puzzle['hunt_id'],
+ puzzle_id_from_name(puzzle['name'])
+ )
+ if puzzle['status'] == 'solved':
+ channel_name += "-solved"
+
+ turb.slack_client.conversations_rename(
+ channel=puzzle['channel_id'],
+ name=channel_name
+ )