From: Carl Worth Date: Mon, 11 Jan 2021 20:21:29 +0000 (-0800) Subject: Add an initial implementation of /help X-Git-Url: https://git.cworth.org/git?p=turbot;a=commitdiff_plain;h=5e7ee7c20f71996cedd04586d7139250c6704c0d Add an initial implementation of /help So that Turbot can be self-documenting. --- diff --git a/turbot/help.py b/turbot/help.py new file mode 100644 index 0000000..dbecfe3 --- /dev/null +++ b/turbot/help.py @@ -0,0 +1,224 @@ +overview = """ +*Turbot help* +Here is an overview of commands that Turbot makes available. You can +actually get at all functionality by just typing `/hunt` and then +clicking buttons that are provided in the result. But many operations +will be much more efficient by using a special-purpose command, (such +as `/solved SOLUTION`). + +Type `/help ` for more details on any command below. + +*Hunt creation and editing* + `/new [hunt]`: Create a new hunt (from any channel) + `/edit [hunt]`: Edit the current hunt (in a hunt or puzzle channel) + +*Puzzle creation and editing* (in a puzzle channel unless specified) + `/new [puzzle]`: Create a new puzzle (in a hunt or puzzle channel) + `/edit [puzzle]`: Edit the current puzzle + `/state `: Capture the current state + `/solved `: Mark current puzzle solved + `/tag [+ADD_TAG|-REMOVE_TAG]`: Tag/untag current puzzle + +*Hunt overview and searching for puzzles* + `/hunt `: Search for puzzles (in a hunt/puzzle channel) + `/round `: Show puzzles in current round (puzzle channel) + `/puzzle`: Display state of current puzzle (puzzle channel) + +*Solving support* (from any channel) + `/rot [*|COUNT] `: Perform a Caesar shift on text +""" + +commands = { + "edit": { + "summary": "Edit the puzzle or hunt for the current channel", + "usage": "`/edit`, `/edit hunt`, `/edit puzzle`", + "details": ("This command will bring up a dialog window where you " + + "can edit the current hunt or puzzle. With `/edit` " + + "alone it will edit whatever is associated with the " + + "current channel (hunt or puzzle). With `/edit hunt` " + + "you can edit the hunt from a puzzle channel.") + }, + "hunt": { + "summary": "Search for and display puzzles in the current hunt", + "usage": "`/hunt [unsolved|solved|all] [meta|plain] `", + "details": ("With `/hunt` alone, this command will display all " + + "unsolved puzzles in the current hunt. All output from " + + "`/hunt` is entirely private to you, so you do not need " + + "to worry about cluttering up the channel's chat " + + "traffic with this command." + + "\n\n" + + "The output for each puzzle will include all of the " + + "information displayed by the `/puzzle` command so see " + + "`/help puzzle` for details." + + "\n\n" + "An initial argument can be used to instead display " + + "only `solved` puzzles or `all` puzzles (both unsolved " + + "and solved). Additional arguments are search terms. " + + "Only puzzles that match all provided terms will be " + + " displayed. The terms can match the puzzle title, " + + " round title, puzzle URL, puzzle type (`meta` or " + + "`plain`), tags, or a puzzle's solution." + + "\n\n" + + "Use quotation marks in search terms to search for a " + + "phrase such as `/hunt \"good times\"`." + + "\n\n" + + "Search terms can include regular expression syntax." + + "\n\n" + + "This command also supports `/hunt new` and " + + "`/hunt edit` which are identical to `/new hunt` " + + "and `/edit hunt` so see `/help new` or `/help edit`" + + "for details.") + }, + "new": { + "summary": "Create a new puzzle (or a new hunt)", + "usage": "`/new`, `/new puzzle`, `/new hunt`", + "details": ("With `/new` alone, this command will bring up a " + + "dialog window where you will be prompted for " + + "information to create a new puzzle. The only field " + + "required for a new puzzle is the puzzle's name " + + "but any additional information you have will be " + + "useful to add (such as an external URL for the puzzle.)" + + "\n\n" + + "A puzzle can be marked as a meta-puzzle and can be " + + "assigned to one or more rounds. If the round(s) for " + + "this puzzle already exist, simply click on them in " + + "the \"Round(s)\" field. If this is the first puzzle " + + "for a new round, then type the name of the round in " + + "the \"New round(s)\" field." + + "\n\n" + + "For the new puzzle, Turbot will create a Slack channel " + + "and a shared spreadsheet for the puzzle. It will also " + + "announce the new puzzle in the hunt's channel, where " + + "members of the hunt can click to join the puzzle's " + + "channel and then click through to the puzzle's sheet." + + "\n\n" + + "This command can also be issued as `/new hunt` from " + + "any channel to create an entirely new hunt.") + }, + "puzzle": { + "summary": "Display the status of the current puzzle", + "usage": "`/puzzle`, `/puzzle new`, `/puzzle edit", + "details": ("When you issue `/puzzle` alone in a puzzle channel, " + + "Turbot will reply (privately to you) with all the " + + "information it has about the current puzzle. For all " + + "puzzles this includes the solved status (either an " + + "unchecked or checked checkbox), the puzzle title, " + + "links to the puzzle's external web page and sheet, " + + "the rounds of the puzzle, and tags, and the puzzle's " + + "state string." + + "\n\n" + + "If this is a meta-puzzle the output will also include " + + "the names (and solutions where known) of all other " + + "puzzles in the same round as the current puzzle." + + "\n\n" + + "This command also support `/puzzle new` and " + + "`/puzzle edit` which are identical to `/new puzzle` " + + "and `/edit puzzle` so see `/help new` and `/help edit` " + + "for details.") + }, + "round": { + "summary": "Display puzzles in the same round as the current puzzle", + "usage": "`/round [unsolved|solved|all] [meta|plain] `", + "details": ("The `/round` command can be issued from any puzzle " + + "channel and will display (by default) all puzzles " + + "belonging to the same round(s) as the current puzzle." + + "\n\n" + "To filter to only solved or unsolved puzzles, issue " + + "`/round solved` or `/round unsolved` instead." + + "\n\n" + + "This command also supports all of the same options as " + + "the `/hunt` command for searching/filtering which " + + "puzzles to display. So see `/help hunt` for details.") + }, + "rot": { + "summary": "Perform a Caesar shift rotation on some text", + "usage": "`/rot TEXT`, `/rot * TEXT`, `/rot COUNT TEXT`", + "details": ("This command performs a Caesar shift rotation on the " + + "text you provide to the command. An initial numeric " + + "argument indicates how many places through the " + + "alphabet each character should be rotated." + + "\n\n" + + "If the numeric argument is omitted, or is the " + + "character `*` this command will emit every possible " + + "rotation of the provided text." + + "\n\n" + "Note: Unlike commands such as `/hunt` or `/puzzle` the " + + "output from `/rot` will be presented to the current " + + "channel rather than privately to you. The idea behind " + + "this is that others working on the same puzzle might " + + "benefit from this output as well." + + "\n\n" + + "If you would like to execute this command privately, " + + "may issue it in a direct-message conversation with " + + "Turbot.") + }, + "state": { + "summary": "Updates the state of this puzzle", + "usage": "`/state `", + "details": ("This command allows you to capture a high-level " + + "description of the state of a puzzle. This is " + + "particularly useful when you are switching away from " + + "a puzzle to let the next solvers know the state of " + + "things." + + "\n\n" + + "Turbot will present the state string in every view it " + + "generates for the puzzle." + + "\n\n" + + "An example state string might be something like: " + + "`/state Grid is filled, but we need help on extraction`" + + " or similar.") + }, + "solved": { + "summary": "Record the solution of a puzzle", + "usage": "`/solved SOLUTION HERE`", + "details": ("Once you've submitted a solution to the hunt's website " + + "and it has been confirmed, issue the `/solved` command " + + "so that Turbot knows the puzzle is solved. Turbot will " + + "report to the hunt's channel that the puzzle has been " + + "solved.") + }, + "tag": { + "summary": "Add or remove a tag to a puzzle", + "usage": "`/tag NEW_TAG`", + "details": ("This command allows you to add brief descriptive tags " + + "to a puzzle. This might include puzzle types, specific " + + "domain knowledge, or metadata from the puzzle’s " + + "presentation which might be used elsewhere." + + "\n\n" + + "Tags can only consist of letters, numbers, and the " + + "underscore character `_`." + + "\n\n" + + "You can also add a tag by typing `/tag +TAG_TO_ADD` " + + "and remove a tag with `/tag -TAG_TO_REMOVE`.") + } +} + +def turbot_help(args): + """The top-level implementation of the Turbot help system + + With empty args, gives a summary of available commands. + + With any command name, gives detailed help on that command. + """ + + if not args: + return overview + + # We only look at the first word here + command = args.split(" ", 1)[0] + + # Ignore any slash prefix the user may have given + if command[0] == '/': + command = command[1:] + + if command not in commands: + valid = list(commands.keys()) + valid.sort() + return "Unknown command: {}. Valid commands are: {}".format( + command, ", ".join(valid)) + + dict = commands[command] + return ("*Help on `/{}`*".format(command) + + "\n`/{}`: {}".format(command, dict["summary"]) + + "\nUsage: {}".format(dict["usage"]) + + "\n\n{}".format(dict["details"])) diff --git a/turbot/interaction.py b/turbot/interaction.py index 69d4f25..53f45a7 100644 --- a/turbot/interaction.py +++ b/turbot/interaction.py @@ -17,6 +17,7 @@ from turbot.puzzle import ( puzzle_copy ) from turbot.round import round_quoted_puzzles_titles_answers +from turbot.help import turbot_help import turbot.rot import turbot.sheets import turbot.slack @@ -1326,3 +1327,29 @@ def round(turb, body, args): return lambda_ok commands["/round"] = round + +def help_command(turb, body, args): + """Implementation of the /help command + + Displays help on how to use Turbot. + """ + + channel_id = body['channel_id'][0] + response_url = body['response_url'][0] + + help_string = turbot_help(args) + + # The "/help me" command is special in that it reports in the + # current channel, (where all other commands report privately to + # the invoking user). + if args == "me": + turb.slack_client.chat_postMessage( + channel=channel_id, text=help_string) + else: + requests.post(response_url, + json = {"text": help_string}, + headers = {"Content-type": "application/json"}) + + return lambda_ok + +commands["/help"] = help_command