X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=turbot%2Fhunt.py;h=3063d879cb1069745a5405683b87f733dea58c1f;hb=a2f4a28d815e0a8448fb6d565592bb662843964f;hp=87e47fa1324cf603423bdcbebff6c65b1f78056a;hpb=ef8d8f359be149adbbdbeda88ec4b6bbbebf928e;p=turbot diff --git a/turbot/hunt.py b/turbot/hunt.py index 87e47fa..3063d87 100644 --- a/turbot/hunt.py +++ b/turbot/hunt.py @@ -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,165 @@ 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 "" + ) + 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 + +def hunt_update_topic(turb, hunt): + + channel_id = hunt['channel_id'] + + topic = '' + + url = hunt.get('url', None) + if url: + topic += "<{}|Hunt website> ".format(url) + + topic += " ".format( + hunt['channel_id']) + + state = hunt.get('state', None) + if state: + topic += 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)