]> git.cworth.org Git - turbot/blobdiff - turbot/hunt.py
Live fix for apparently overflowing the Slack message limit with hunt details
[turbot] / turbot / hunt.py
index 87e47fa1324cf603423bdcbebff6c65b1f78056a..7b515d21c0012ccf5b8f74f9f7962f0ef00349cf 100644 (file)
@@ -1,3 +1,11 @@
+from turbot.blocks import (
+    section_block, text_block, divider_block, accessory_block, button_block
+)
+from turbot.round import round_blocks
+from turbot.puzzle import puzzle_blocks, puzzle_matches_all
+from turbot.channel import channel_url
+from boto3.dynamodb.conditions import Key
+
 def find_hunt_for_hunt_id(turb, hunt_id):
     """Given a hunt ID find the database item for that hunt
 
@@ -16,3 +24,140 @@ def find_hunt_for_hunt_id(turb, hunt_id):
         return response['Item']
     else:
         return None
+
+def hunt_puzzles_for_hunt_id(turb, hunt_id):
+    """Return all puzzles that belong to the given hunt_id"""
+
+    response = turb.table.query(
+        KeyConditionExpression=(
+            Key('hunt_id').eq(hunt_id) &
+            Key('SK').begins_with('puzzle-')
+        )
+    )
+
+    return response['Items']
+
+def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[],
+                limit_to_rounds=None):
+    """Generate Slack blocks for a hunt
+
+    Returns a list of lists of blocks, (broken up by round so that
+    the receiver should do one Slack post for each entry in the
+    outer array.
+
+    The hunt argument should be a dictionary as returned from the
+    database.
+
+    Three optional arguments can be used to filter which puzzles to
+    include in the result:
+
+      puzzle_status: If either 'solved' or 'unsolved' only puzzles
+                     with that status will be included in the
+                     result. If any other value, all puzzles in the
+                     hunt will be considered.
+
+      search_terms: A list of search terms. Only puzzles that match
+                    all of these terms will be included in the
+                    result. A match will be considered on any of
+                    puzzle title, round title, puzzle URL, puzzle
+                    state, puzzle type, tags, or solution
+                    string. Terms can include regular expression
+                    syntax.
+
+      limit_to_rounds: A list of rounds. If provided only the given
+                       rounds will be included in the output. Note:
+                       an empty list means to display only puzzles
+                       assigned to no rounds, while an argument of
+                       None means to display all puzzles with no
+                       limit on the rounds.
+
+    The return value can be used in a Slack command expecting blocks to
+    provide all the details of a hunt, (puzzles, their state,
+    solution, links to channels and sheets, etc.).
+
+    """
+
+    name = hunt['name']
+    hunt_id = hunt['hunt_id']
+    channel_id = hunt['channel_id']
+
+    puzzles = hunt_puzzles_for_hunt_id(turb, hunt_id)
+
+    # Filter the set of puzzles according the the requested puzzle_status
+    if puzzle_status in ('solved', 'unsolved'):
+        puzzles = [p for p in puzzles if p['status'] == puzzle_status]
+    else:
+        puzzle_status = 'all'
+
+    if search_terms:
+        puzzles = [puzzle for puzzle in puzzles
+                   if puzzle_matches_all(puzzle, search_terms)]
+
+    # Compute the set of rounds across all puzzles
+    rounds = set()
+    for puzzle in puzzles:
+        if 'rounds' not in puzzle:
+            continue
+        for round in puzzle['rounds']:
+            rounds.add(round)
+
+    hunt_text = "*{} puzzles in hunt <{}|{}>*".format(
+        puzzle_status.capitalize(),
+        channel_url(channel_id),
+        name
+    )
+    if limit_to_rounds is not None:
+        hunt_text += " *in round{}: {}*".format(
+            "s" if len(limit_to_rounds) > 1 else "",
+            ", ".join(limit_to_rounds) if limit_to_rounds else "<no round>"
+        )
+    if search_terms:
+        quoted_terms = ['`{}`'.format(term) for term in search_terms]
+        hunt_text += " matching {}".format(" AND ".join(quoted_terms))
+
+    blocks = [
+        accessory_block(
+            section_block(text_block(hunt_text)),
+            button_block("✏", "edit_hunt", hunt_id)
+        )
+    ]
+    block = blocks[0]
+
+    if not len(puzzles):
+        text = "No puzzles found."
+        if puzzle_status != 'all':
+            text += ' (Consider searching for "all" puzzles?)'
+        block += [
+            section_block(text_block(text))
+        ]
+
+    # Construct blocks for each round
+    for round in rounds:
+        if limit_to_rounds is not None and round not in limit_to_rounds:
+            continue
+        # If we're only displaying one round the round header is redundant
+        if limit_to_rounds and len(limit_to_rounds) == 1:
+            block += round_blocks(round, puzzles, omit_header=True)
+        else:
+            block += round_blocks(round, puzzles)
+        block.append(divider_block())
+        blocks.append([])
+        block = blocks[-1]
+
+    # Also blocks for any puzzles not in any round
+    stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
+
+    # For this condition, either limit_to_rounds is None which
+    # means we definitely want to display these stray puzzles
+    # (since we are not limiting), _OR_ limit_to_rounds is not
+    # None but is a zero-length array, meaning we are limiting
+    # to rounds but specifically the round of unassigned puzzles
+    if len(stray_puzzles) and not limit_to_rounds:
+        stray_text = "*Puzzles with no assigned round*"
+        block.append(section_block(text_block(stray_text)))
+        for puzzle in stray_puzzles:
+            block += puzzle_blocks(puzzle)
+
+    block.append(divider_block())
+
+    return blocks