]> git.cworth.org Git - turbot/blobdiff - turbot/interaction.py
Report change in a puzzle's solved status to the main hunt channel
[turbot] / turbot / interaction.py
index d14c3d4ad07195aeefc916b9e0649ca664b9e98b..bfb759ada53f9395ac0f8c629ec0fd0991a4b55f 100644 (file)
@@ -3,7 +3,13 @@ from turbot.blocks import (
     input_block, section_block, text_block, multi_select_block, checkbox_block
 )
 from turbot.hunt import find_hunt_for_hunt_id, hunt_blocks
-from turbot.puzzle import find_puzzle_for_url, find_puzzle_for_puzzle_id
+from turbot.puzzle import (
+    find_puzzle_for_url,
+    find_puzzle_for_puzzle_id,
+    puzzle_update_channel_and_sheet,
+    puzzle_id_from_name,
+    puzzle_blocks
+)
 import turbot.rot
 import turbot.sheets
 import turbot.slack
@@ -138,7 +144,7 @@ def edit_puzzle(turb, payload):
                 "Puzzle status", "Solved", "solved",
                 checked=(puzzle.get('status', 'unsolved') == 'solved')),
             input_block("Solution", "solution",
-                        "Solutions (comma-separated if multiple",
+                        "Solution(s) (comma-separated if multiple)",
                         initial_value=solution_str,
                         optional=True),
         ]
@@ -161,6 +167,104 @@ def edit_puzzle_submission(turb, payload, metadata):
     function above.
     """
 
+    puzzle={}
+
+    # First, read all the various data from the request
+    meta = json.loads(metadata)
+    puzzle['hunt_id'] = meta['hunt_id']
+    puzzle['SK'] = meta['SK']
+    puzzle['puzzle_id'] = meta['puzzle_id']
+    puzzle['channel_id'] = meta['channel_id']
+    puzzle['channel_url'] = meta['channel_url']
+    puzzle['sheet_url'] = meta['sheet_url']
+
+    state = payload['view']['state']['values']
+    user_id = payload['user']['id']
+
+    puzzle['name'] = state['name']['name']['value']
+    url = state['url']['url']['value']
+    if url:
+        puzzle['url'] = url
+    rounds = [option['value'] for option in
+              state['rounds']['rounds']['selected_options']]
+    if rounds:
+        puzzle['rounds'] = rounds
+    new_rounds = state['new_rounds']['new_rounds']['value']
+    puzzle_state = state['state']['state']['value']
+    if puzzle_state:
+        puzzle['state'] = puzzle_state
+    if state['solved']['solved']['selected_options']:
+        puzzle['status'] = 'solved'
+    else:
+        puzzle['status'] = 'unsolved'
+    puzzle['solution'] = []
+    solution = state['solution']['solution']['value']
+    if solution:
+        puzzle['solution'] = [
+            sol.strip() for sol in solution.split(',')
+        ]
+
+    # Verify that there's a solution if the puzzle is mark solved
+    if puzzle['status'] == 'solved' and not puzzle['solution']:
+        return submission_error("solution",
+                                "A solved puzzle requires a solution.")
+
+    if puzzle['status'] == 'unsolved' and puzzle['solution']:
+        return submission_error("solution",
+                                "An unsolved puzzle should have no solution.")
+
+    # Add any new rounds to the database
+    if new_rounds:
+        if 'rounds' not in puzzle:
+            puzzle['rounds'] = []
+        for round in new_rounds.split(','):
+            # Drop any leading/trailing spaces from the round name
+            round = round.strip()
+            # Ignore any empty string
+            if not len(round):
+                continue
+            puzzle['rounds'].append(round)
+            turb.table.put_item(
+                Item={
+                    'hunt_id': puzzle['hunt_id'],
+                    'SK': 'round-' + round
+                }
+            )
+
+    # Get old puzzle from the database (to determine what's changed)
+    old_puzzle = find_puzzle_for_puzzle_id(turb,
+                                           puzzle['hunt_id'],
+                                           puzzle['puzzle_id'])
+
+    # Update the puzzle in the database
+    turb.table.put_item(Item=puzzle)
+
+    # Inform the puzzle channel about the edit
+    edit_message = "Puzzle edited by <@{}>".format(user_id)
+    blocks = ([section_block(text_block(edit_message+":\n"))] +
+              puzzle_blocks(puzzle, include_rounds=True))
+    slack_send_message(
+        turb.slack_client, puzzle['channel_id'],
+        edit_message, blocks=blocks)
+
+    # Also inform the hunt if the puzzle's solved status changed
+    if puzzle['status'] != old_puzzle['status']:
+        hunt = find_hunt_for_hunt_id(turb, puzzle['hunt_id'])
+        if puzzle['status'] == 'solved':
+            message = "Puzzle <{}|{}> has been solved!".format(
+                puzzle['channel_url'],
+                puzzle['name'])
+        else:
+            message = "Oops. Puzzle <{}|{}> has been marked unsolved!".format(
+                puzzle['channel_url'],
+                puzzle['name'])
+        slack_send_message(turb.slack_client, hunt['channel_id'], message)
+
+    # We need to set the channel topic if any of puzzle name, url,
+    # state, status, or solution, has changed. Let's just do that
+    # unconditionally here.
+    puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=old_puzzle)
+
     return lambda_ok
 
 def new_hunt(turb, payload):
@@ -465,8 +569,56 @@ def hunt_rounds(turb, hunt_id):
 def puzzle(turb, body, args):
     """Implementation of the /puzzle command
 
-    The args string is currently ignored (this command will bring up
-    a modal dialog for user input instead)."""
+    The args string can be a sub-command:
+
+        /puzzle new: Bring up a dialog to create a new puzzle
+
+    Or with no argument at all:
+
+        /puzzle: Print details of the current puzzle (if in a puzzle channel)
+    """
+
+    if args == 'new':
+        return new_puzzle(turb, body)
+
+    if len(args):
+        return bot_reply("Unknown syntax for `/puzzle` command. " +
+                         "Use `/puzzle new` to create a new puzzle.")
+
+    # For no arguments we print the current puzzle as a reply
+    channel_id = body['channel_id'][0]
+    response_url = body['response_url'][0]
+
+    puzzle = puzzle_for_channel(turb, channel_id)
+
+    if not puzzle:
+        hunt = hunt_for_channel(turb, channel_id)
+        if hunt:
+            return bot_reply(
+                "This is not a puzzle channel, but is a hunt channel. "
+                + "If you want to create a new puzzle for this hunt, use "
+                + "`/puzzle new`.")
+        else:
+            return bot_reply(
+                "Sorry, this channel doesn't appear to be a hunt or a puzzle "
+                + "channel, so the `/puzzle` command cannot work here.")
+
+    blocks = puzzle_blocks(puzzle, include_rounds=True)
+
+    requests.post(response_url,
+                  json = {'blocks': blocks},
+                  headers = {'Content-type': 'application/json'}
+                  )
+
+    return lambda_ok
+
+commands["/puzzle"] = puzzle
+
+def new_puzzle(turb, body):
+    """Implementation of the "/puzzle new" command
+
+    This brings up a dialog box for creating a new puzzle.
+    """
 
     channel_id = body['channel_id'][0]
     trigger_id = body['trigger_id'][0]
@@ -512,17 +664,16 @@ def puzzle(turb, body, args):
                                           view=view)
 
     if (result['ok']):
-        submission_handlers[result['view']['id']] = puzzle_submission
+        submission_handlers[result['view']['id']] = new_puzzle_submission
 
     return lambda_ok
 
-commands["/puzzle"] = puzzle
-
-def puzzle_submission(turb, payload, metadata):
+def new_puzzle_submission(turb, payload, metadata):
     """Handler for the user submitting the new puzzle modal
 
-    This is the modal view presented to the user by the puzzle function
-    above."""
+    This is the modal view presented to the user by the new_puzzle
+    function above.
+    """
 
     # First, read all the various data from the request
     meta = json.loads(metadata)
@@ -548,7 +699,7 @@ def puzzle_submission(turb, payload, metadata):
                 "Error: A puzzle with this URL already exists.")
 
     # Create a Slack-channel-safe puzzle_id
-    puzzle_id = re.sub(r'[^a-zA-Z0-9_]', '', name).lower()
+    puzzle_id = puzzle_id_from_name(name)
 
     # Create a channel for the puzzle
     hunt_dash_channel = "{}-{}".format(hunt_id, puzzle_id)
@@ -598,41 +749,6 @@ def puzzle_submission(turb, payload, metadata):
 
     return lambda_ok
 
-# XXX: This duplicates functionality eith events.py:set_channel_description
-def set_channel_topic(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']
-
-    description = ''
-
-    if status == 'solved':
-        description += "SOLVED: `{}` ".format('`, `'.join(puzzle['solution']))
-
-    description += name
-
-    links = []
-    if url:
-        links.append("<{}|Puzzle>".format(url))
-    if sheet_url:
-        links.append("<{}|Sheet>".format(sheet_url))
-
-    if len(links):
-        description += "({})".format(', '.join(links))
-
-    if state:
-        description += " {}".format(state)
-
-    # Slack only allows 250 characters for a topic
-    if len(description) > 250:
-        description = description[:247] + "..."
-
-    turb.slack_client.conversations_setTopic(channel=channel_id,
-                                             topic=description)
-
 def state(turb, body, args):
     """Implementation of the /state command
 
@@ -641,17 +757,20 @@ def state(turb, body, args):
 
     channel_id = body['channel_id'][0]
 
-    puzzle = puzzle_for_channel(turb, channel_id)
+    old_puzzle = puzzle_for_channel(turb, channel_id)
 
-    if not puzzle:
+    if not old_puzzle:
         return bot_reply(
             "Sorry, the /state command only works in a puzzle channel")
 
-    # Set the state field in the database
+    # Make a copy of the puzzle object
+    puzzle = old_puzzle.copy()
+
+    # Update the puzzle in the database
     puzzle['state'] = args
     turb.table.put_item(Item=puzzle)
 
-    set_channel_topic(turb, puzzle)
+    puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=old_puzzle)
 
     return lambda_ok
 
@@ -665,15 +784,18 @@ def solved(turb, body, args):
     channel_id = body['channel_id'][0]
     user_name = body['user_name'][0]
 
-    puzzle = puzzle_for_channel(turb, channel_id)
+    old_puzzle = puzzle_for_channel(turb, channel_id)
 
-    if not puzzle:
+    if not old_puzzle:
         return bot_reply("Sorry, this is not a puzzle channel.")
 
     if not args:
         return bot_reply(
             "Error, no solution provided. Usage: `/solved SOLUTION HERE`")
 
+    # Make a copy of the puzzle object
+    puzzle = old_puzzle.copy()
+
     # Set the status and solution fields in the database
     puzzle['status'] = 'solved'
     puzzle['solution'].append(args)
@@ -696,19 +818,7 @@ def solved(turb, body, args):
     )
 
     # And update the puzzle's description
-    set_channel_topic(turb, puzzle)
-
-    # And rename the sheet to suffix with "-SOLVED"
-    turbot.sheets.renameSheet(turb, puzzle['sheet_url'],
-                              puzzle['name'] + "-SOLVED")
-
-    # Finally, rename the Slack channel to add the suffix '-solved'
-    channel_name = "{}-{}-solved".format(
-        puzzle['hunt_id'],
-        puzzle['puzzle_id'])
-    turb.slack_client.conversations_rename(
-        channel=puzzle['channel_id'],
-        name=channel_name)
+    puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=old_puzzle)
 
     return lambda_ok