X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=turbot%2Fpuzzle.py;h=1a36bfb9be433460d850672733c4bfed92d6728e;hb=65d277e6962108c5fdfe30cae4d66e4db29ce150;hp=9f9b1c600b4002a5afa661cdf1d4c9b930639fd0;hpb=5e9eac54ab60e977a067760f1dc44f1e23f3b311;p=turbot diff --git a/turbot/puzzle.py b/turbot/puzzle.py index 9f9b1c6..1a36bfb 100644 --- a/turbot/puzzle.py +++ b/turbot/puzzle.py @@ -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 + )