]> git.cworth.org Git - turbot/blobdiff - turbot/puzzle.py
Unify code to rename channel, set channel description, and rename sheet
[turbot] / turbot / puzzle.py
index 9f9b1c600b4002a5afa661cdf1d4c9b930639fd0..1a36bfb9be433460d850672733c4bfed92d6728e 100644 (file)
@@ -1,4 +1,29 @@
+from turbot.blocks import (
+    section_block, text_block, button_block, accessory_block
+)
+from turbot.channel import channel_url
 from boto3.dynamodb.conditions import Key
+import turbot.sheets
+import re
+
+def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id):
+    """Given a hunt_id and puzzle_id, return that puzzle
+
+    Returns None if no puzzle with the given hunt_id and puzzle_id
+    exists in the database, otherwise a dictionary with all fields
+    from the puzzle's row in the database.
+    """
+
+    response = turb.table.get_item(
+        Key={
+            'hunt_id': hunt_id,
+            'SK': 'puzzle-{}'.format(puzzle_id)
+        })
+
+    if 'Item' in response:
+        return response['Item']
+    else:
+        return None
 
 def find_puzzle_for_url(turb, hunt_id, url):
     """Given a hunt_id and URL, return the puzzle with that URL
@@ -20,3 +45,170 @@ def find_puzzle_for_url(turb, hunt_id, url):
         return None
 
     return response['Items'][0]
+
+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
+    )