+
+def solved(turb, body, args):
+ """Implementation of the /solved command
+
+ The args string should be a confirmed solution."""
+
+ channel_id = body['channel_id'][0]
+ user_name = body['user_name'][0]
+
+ puzzle = puzzle_for_channel(turb, channel_id)
+
+ if not 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`")
+
+ # Set the status and solution fields in the database
+ puzzle['status'] = 'solved'
+ puzzle['solution'].append(args)
+ if 'state' in puzzle:
+ del puzzle['state']
+ turb.table.put_item(Item=puzzle)
+
+ # Report the solution to the puzzle's channel
+ slack_send_message(
+ turb.slack_client, channel_id,
+ "Puzzle mark solved by {}: `{}`".format(user_name, args))
+
+ # Also report the solution to the hunt channel
+ hunt = find_hunt_for_hunt_id(turb, puzzle['hunt_id'])
+ slack_send_message(
+ turb.slack_client, hunt['channel_id'],
+ "Puzzle <{}|{}> has been solved!".format(
+ puzzle['channel_url'],
+ puzzle['name'])
+ )
+
+ # 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)
+
+ return lambda_ok
+
+commands["/solved"] = solved
+
+
+def hunt(turb, body, args):
+ """Implementation of the /hunt command
+
+ The (optional) args string can be used to filter which puzzles to
+ display. The first word can be one of 'all', 'unsolved', or
+ 'solved' and can be used to display only puzzles with the given
+ status. Any remaining text in the args string will be interpreted
+ as search terms. These will be split into separate terms on space
+ characters, (though quotation marks can be used to include a space
+ character in a term). All terms must match on a puzzle in order
+ for that puzzle to be included. But a puzzle will be considered to
+ match if any of the puzzle title, round title, puzzle URL, puzzle
+ state, or puzzle solution match. Matching will be performed
+ without regard to case sensitivity and the search terms can
+ include regular expression syntax.
+ """
+
+ channel_id = body['channel_id'][0]
+ response_url = body['response_url'][0]
+
+ terms = None
+ if args:
+ # The first word can be a puzzle status and all remaining word
+ # (if any) are search terms. _But_, if the first word is not a
+ # valid puzzle status ('all', 'unsolved', 'solved'), then all
+ # words are search terms and we default status to 'unsolved'.
+ split_args = args.split(' ', 1)
+ status = split_args[0]
+ if (len(split_args) > 1):
+ terms = split_args[1]
+ if status not in ('unsolved', 'solved', 'all'):
+ terms = args
+ status = 'unsolved'
+ else:
+ status = 'unsolved'
+
+ # Separate search terms on spaces (but allow for quotation marks
+ # to capture spaces in a search term)
+ if terms:
+ terms = shlex.split(terms)
+
+ hunt = hunt_for_channel(turb, channel_id)
+
+ if not hunt:
+ return bot_reply("Sorry, this channel doesn't appear to "
+ + "be a hunt or puzzle channel")
+
+ blocks = hunt_blocks(turb, hunt, puzzle_status=status, search_terms=terms)
+
+ requests.post(response_url,
+ json = { 'blocks': blocks },
+ headers = {'Content-type': 'application/json'}
+ )
+
+ return lambda_ok
+
+commands["/hunt"] = hunt