async def fill_shortcut(bot, shortcut, parameters, message): parameters = split_parameters(parameters, include_quotes=True) stripped_parameters = parameters[::2] arguments_dictionary = {} current_index = -1 for current_index, current in enumerate(stripped_parameters): if current_index >= len(shortcut.args): invoker = utilities.get_invoker(bot, guild=message.guild) raise CBException( "Too many arguments.", embed_fields=shortcut.help_embed_fields, embed_format={'invoker': invoker}) else: arg = shortcut.args[current_index] if arg.argtype in (ArgTypes.SINGLE, ArgTypes.OPTIONAL): arguments_dictionary[arg.name] = current else: # Instant finish grouped arguments if arg.argtype in (ArgTypes.SPLIT, ArgTypes.SPLIT_OPTIONAL): arguments_dictionary[arg.name] = ''.join(stripped_parameters[current_index:]) else: # Merged arguments_dictionary[arg.name] = ''.join(parameters[current_index * 2:]) break # TODO: TEST THIS! logger.debug("Finished shortcut loop. %s", arguments_dictionary) if current_index < len(shortcut.args) - 1: # Check for optional arguments arg = shortcut.args[current_index + 1] if arg.argtype is ArgTypes.OPTIONAL: while (arg and arg.argtype is ArgTypes.OPTIONAL and current_index < len(shortcut.args)): arguments_dictionary[arg.name] = '' if arg.default is None else arg.default current_index += 1 try: arg = shortcut.args[current_index] except: arg = None if arg and arg.argtype in (ArgTypes.SPLIT_OPTIONAL, ArgTypes.MERGED_OPTIONAL): arguments_dictionary[arg.name] = '' if arg.default is None else arg.default elif arg: invoker = utilities.get_invoker(bot, guild=message.guild) raise CBException( "Not enough arguments.", embed_fields=shortcut.help_embed_fields, embed_format={'invoker': invoker}) logger.debug("Finished checking for optional arguments. %s", arguments_dictionary) for arg in shortcut.args: value = arguments_dictionary[arg.name] if value is not None: new_value = await arg.convert_and_check(bot, message, value) arguments_dictionary[arg.name] = new_value return shortcut.replacement.format(**arguments_dictionary).strip()
async def role_list(bot, context): """Lists the available roles for self-assignment.""" if context.arguments[0]: # List members that have this role role = context.arguments[0] if role.is_default(): raise CBException("Cannot list members with the @everyone role.") if not role.members: raise CBException("No members have the role {}.".format(role.mention)) elif len(role.members) > 80: name_file = discord.File( utilities.get_text_as_file('\n'.join(str(it) for it in role.members)), filename='members.txt') embed = discord.Embed( description='{} members have this role.'.format(len(role.members))) return Response(file=name_file, embed=embed) else: plural = len(role.members) > 1 return Response(embed=discord.Embed( title='{} member{} {} this role:'.format( len(role.members), 's' if plural else '', 'have' if plural else 'has'), description=', '.join(it.mention for it in role.members))) else: # List self-assignable roles available_roles = _check_roles(bot, context.guild) if not available_roles: raise CBException("There are no self-assignable roles available.") embed = discord.Embed( title='Self-assignable roles:', description='\n'.join(it.mention for it in available_roles)) embed.set_footer(text='To join a role, use {}role join'.format( utilities.get_invoker(bot, guild=context.guild))) return Response(embed=embed)
def get_manual(bot, subject, entry, guild=None): """Gets the given manual entry as a tuple: (topic, [text, text2]).""" invoker = utilities.get_invoker(bot, guild=guild) base_invoker = utilities.get_invoker(bot) if entry <= 0: raise CBException("Invalid manual entry.") for manual in bot.manuals: manual_length = len(manual[1]['order']) if manual_length >= entry: entry_title = manual[1]['order'][entry - 1] found_entry = manual[1]['entries'][entry_title] response = '***`{0}`*** -- {1}\n\n'.format(manual[0], entry_title) return response + found_entry.format(invoker=invoker, base_invoker=base_invoker) else: entry -= manual_length raise CBException("Invalid manual entry.")
def get_general_help(bot, guild=None, is_owner=False): """Gets the general help. Lists all base commands that aren't shortcuts.""" response = "Here is a list of commands by group:\n" invoker = utilities.get_invoker(bot, guild=guild) group_dictionary = {} for plugin_name, plugin in bot.plugins.items(): visible_commands = [] for command in plugin[1]: level = command.elevated_level hidden = command.hidden group = command.group if (((level < 3 and not hidden) or is_owner) and command not in visible_commands): if command.description: description = command.description else: description = '[Description not provided]' if group_dictionary.get(group) is None: group_dictionary[group] = [] group_dictionary[group].append('**`{0}`** -- {1}'.format( command.base, description)) listing = [] for group, entries in sorted(list(group_dictionary.items())): listing.append('\n***`{0}`***\n\t{1}'.format( group, '\n\t'.join(sorted(entries)))) response += '\n'.join(listing) + '\n' response += ("\nGet help on a command with `{0}help <command>`\n" "Confused by the syntax? See `{0}manual 3`").format(invoker) return response
async def on_server_join(bot, server): # Add server to the list logging.debug("Joining server") data.add_server(bot, server) if bot.selfbot: # Don't send DMs if in selfbot mode return invoker = utilities.get_invoker(bot) text = ( "Hello! You are receiving this notification because this bot was " "added to one of your servers, specifically '{0.name}' (ID: {0.id}). " "If you are aware of this and approve of the addition, feel free to " "continue and use the bot. However, if you did not approve of this " "addition, it is highly recommended you kick or ban this bot as there " "may be potential for users to use the bot to spam. Only users that " "have the administrator permission can add bots to servers. " "Unfortunately, there is no way to track who added the bot.\n\n" "To read more about the functionality and usage of the bot, type " "`{1}manual` to see a list of topics, and `{1}help` to see a list of " "commands. **As a server owner, it is highly recommended that you " "read `{1}manual 5` and `{1}manual 4` for moderating and configuring " "the bot.**\n\nThat's all for now. If you have any questions, please " "refer to the manual, or send the bot owners a message using " "`{1}owner feedback <message>`.\n\nCheck out the Wiki for more: " "https://github.com/jkchen2/JshBot/wiki").format(server, invoker) await bot.send_message(server.owner, text)
def get_manual(bot, entry, server=None): """Gets the given manual entry.""" invoker = utilities.get_invoker(bot, server=server) base_invoker = utilities.get_invoker(bot) if entry <= 0: raise BotException(EXCEPTION, "Invalid manual entry.") for manual in bot.manuals: manual_length = len(manual[1]['order']) if manual_length >= entry: entry_title = manual[1]['order'][entry - 1] found_entry = manual[1]['entries'][entry_title] response = '***`{0}`*** -- {1}\n\n'.format(manual[0], entry_title) return response + found_entry.format(invoker=invoker, base_invoker=base_invoker) else: entry -= manual_length raise BotException(EXCEPTION, "Invalid manual entry.")
def get_help(bot, base, topic=None, is_owner=False, guild=None): """Gets the help of the base command, or the topic of a help command.""" try: base = base.lower() command = bot.commands[base] except KeyError: raise CBException( "Invalid command base. Ensure sure you are not including the command invoker." ) if command.hidden and not is_owner: return '```\nCommand is hidden.```' if command.shortcut and base in command.shortcut.bases: shortcut_index = command.shortcut.bases.index(base) return usage_reminder(bot, base, index=shortcut_index, shortcut=True, is_owner=is_owner, guild=guild) # Handle specific topic help if topic is not None: try: topic_index = int(topic) except: # Guess the help index guess = parser.guess_index(bot, '{0} {1}'.format(base, topic)) topic_index = None if guess[1] == -1 else guess[1] + 1 return get_help(bot, base, topic=topic_index, is_owner=is_owner, guild=guild) else: # Proper index given return usage_reminder(bot, base, index=topic_index, guild=guild) response = '' invoker = utilities.get_invoker(bot, guild=guild) if command.description: response += '**Description**:\n{}\n\n'.format( command.description.format(invoker=invoker)) response += usage_reminder(bot, base, is_owner=is_owner, guild=guild) + '\n' if command.shortcut: response += usage_reminder( bot, base, shortcut=True, is_owner=is_owner, guild=guild) + '\n' if command.other: response += '**Other information**:\n{}'.format( command.other.format(invoker=invoker)) return response.rstrip()
def get_general_manual(bot, guild=None): """Lists available manual entries and assigns each one an index.""" response = "Here is a list of manual entries by plugin:\n" invoker = utilities.get_invoker(bot, guild=guild) counter = 1 for manual in bot.manuals: response += '\n***`{}`***\n'.format(manual[0]) for entry in manual[1]['order']: response += '\t**`[{0: <2}]`** {1}\n'.format(counter, entry) counter += 1 response += ( '\nRead an entry with `{}manual <entry number>`\nNew? It is ' 'recommended that you read manual entries 1, 2, and 3.').format(invoker) return response
def usage_reminder(bot, base, index=None, shortcut=False, guild=None, is_owner=False): """Returns the usage syntax for the base command (simple format). Keyword arguments: index -- gets that specific index's help entry guild_id -- used to check the invoker is_mod -- used to check for private command visibility """ command = bot.commands[base] if command.hidden and not is_owner: return "`\nCommand is hidden.`" if shortcut: if base in command.shortcut.bases: base = command.base command = command.shortcut invoker = utilities.get_invoker(bot, guild=guild) if index is None: # List all commands or shortcuts if shortcut: response = '**Shortcuts**:\n' else: response = '**Usage**:\n' for topic_index, (syntax, details) in enumerate(command.help): details = details if details else '[Details not provided]' if shortcut: response += '`{0}{1} {2}`\t→\t`{0}{3} {4}`\n'.format( invoker, command.bases[topic_index], syntax, base, details) else: syntax = syntax if syntax else '[Syntax not provided]' response += '`{0}{1} {2}`\n'.format(invoker, base, syntax) else: # Help on a specific command if shortcut: syntax, result = command.help[index] response = '`{0}{1} {2}`\t→\t`{0}{3} {4}`'.format( invoker, command.bases[index], syntax, base, result) elif 0 < index <= len(command.help): syntax, details = command.help[index - 1] syntax = syntax if syntax else '[Syntax not provided]' details = details if details else '[Details not provided]' response = '`{0}{1} {2}`\n{3}'.format(invoker, base, syntax, details) else: raise CBException("Invalid help index.") return response
def get_general_manual(bot, guild=None): """Lists available manual entries and assigns each one an index.""" response = "Here is a list of manual entries by plugin:\n" invoker = utilities.get_invoker(bot, guild=guild) counter = 1 for manual in bot.manuals: response += '\n***`{}`***\n'.format(manual[0]) for entry in manual[1]['order']: response += '\t**`[{0: <2}]`** {1}\n'.format(counter, entry) counter += 1 response += ('\nRead an entry with `{}manual <entry number>`\nNew? It is ' 'recommended that you read manual entries 1, 2, and 3.' ).format(invoker) return response
def _user_character_search(bot, command_author, owner=None, character_search=None): """Finds characters under the given owner, search, or both.""" # Setup select arguments where_args, input_args = [], [] if not (owner or character_search): where_args.append('owner_id=%s') input_args.append(command_author.id) if owner: where_args.append('owner_id=%s') input_args.append(owner.id) if character_search: where_args.append('clean_name=%s') input_args.append(character_search) # Get character list cursor = data.db_select( bot, from_arg='characters', where_arg=' AND '.join(where_args), input_args=input_args) characters = cursor.fetchall() if cursor else [] if not characters: if owner: cursor = data.db_select( bot, from_arg='characters', where_arg='owner_id=%s', input_args=[owner.id]) owner_characters = cursor.fetchall() if cursor else [] if owner_characters: raise CBException( "{} has no character named \"{}\".".format( owner.mention, character_search)) else: raise CBException("{} has no character entries.".format(owner.mention)) elif character_search: raise CBException("No character named \"{}\" was found.".format(character_search)) else: raise CBException( "You have no character entries!\n" "You can create one with `{}character create`".format( utilities.get_invoker(bot, getattr(command_author, 'guild', None)))) # Check if character list contains characters made by the command author character_index = 0 if not owner: for index, character in enumerate(characters): if character.owner_id == command_author.id: character_index = index break return [character_index, characters]
def usage_reminder( bot, base, index=None, shortcut=False, guild=None, is_owner=False): """Returns the usage syntax for the base command (simple format). Keyword arguments: index -- gets that specific index's help entry guild_id -- used to check the invoker is_mod -- used to check for private command visibility """ command = bot.commands[base] if command.hidden and not is_owner: return "`\nCommand is hidden.`" if shortcut: if base in command.shortcut.bases: base = command.base command = command.shortcut invoker = utilities.get_invoker(bot, guild=guild) if index is None: # List all commands or shortcuts if shortcut: response = '**Shortcuts**:\n' else: response = '**Usage**:\n' for topic_index, (syntax, details) in enumerate(command.help): details = details if details else '[Details not provided]' if shortcut: response += '`{0}{1} {2}`\t→\t`{0}{3} {4}`\n'.format( invoker, command.bases[topic_index], syntax, base, details) else: syntax = syntax if syntax else '[Syntax not provided]' response += '`{0}{1} {2}`\n'.format(invoker, base, syntax) else: # Help on a specific command if shortcut: syntax, result = command.help[index] response = '`{0}{1} {2}`\t→\t`{0}{3} {4}`'.format( invoker, command.bases[index], syntax, base, result) elif 0 < index <= len(command.help): syntax, details = command.help[index - 1] syntax = syntax if syntax else '[Syntax not provided]' details = details if details else '[Details not provided]' response = '`{0}{1} {2}`\n{3}'.format( invoker, base, syntax, details) else: raise CBException("Invalid help index.") return response
async def tagremote(bot, context): """Gets the current session data as a link.""" session_data = data.get(bot, __name__, 'data', guild_id=context.guild.id) if not session_data: raise CBException( "No session available.\nStart one with `{}tagremote start`".format( utilities.get_invoker(bot, guild=context.guild))) channel_id, session_code = session_data['channel'], session_data['session'] voice_channel_id = session_data['voice_channel'] channel_mention = data.get_channel(bot, channel_id, guild=context.guild).mention voice_channel_mention = data.get_channel(bot, voice_channel_id, guild=context.guild).mention description = 'The session code is:\n`{}`\nThe session is attached to {} and {}'.format( session_code, channel_mention, voice_channel_mention) return Response(embed=discord.Embed( title='Tap here on your phone to use the tag remote', url='https://jkchen2.github.io/tag-remote/#{}'.format(session_code), description=description))
def get_help(bot, base, topic=None, is_owner=False, guild=None): """Gets the help of the base command, or the topic of a help command.""" try: base = base.lower() command = bot.commands[base] except KeyError: raise CBException( "Invalid command base. Ensure sure you are not including the command invoker.") if command.hidden and not is_owner: return '```\nCommand is hidden.```' if command.shortcut and base in command.shortcut.bases: shortcut_index = command.shortcut.bases.index(base) return usage_reminder( bot, base, index=shortcut_index, shortcut=True, is_owner=is_owner, guild=guild) # Handle specific topic help if topic is not None: try: topic_index = int(topic) except: # Guess the help index guess = parser.guess_index(bot, '{0} {1}'.format(base, topic)) topic_index = None if guess[1] == -1 else guess[1] + 1 return get_help( bot, base, topic=topic_index, is_owner=is_owner, guild=guild) else: # Proper index given return usage_reminder(bot, base, index=topic_index, guild=guild) response = '' invoker = utilities.get_invoker(bot, guild=guild) if command.description: response += '**Description**:\n{}\n\n'.format( command.description.format(invoker=invoker)) response += usage_reminder( bot, base, is_owner=is_owner, guild=guild) + '\n' if command.shortcut: response += usage_reminder( bot, base, shortcut=True, is_owner=is_owner, guild=guild) + '\n' if command.other: response += '**Other information**:\n{}'.format( command.other.format(invoker=invoker)) return response.rstrip()
async def guess_command( bot, text, message, safe=True, substitute_shortcuts=True, suggest_help=True): """Guesses the closest command or subcommand. Keyword arguments: safe -- Returns None if no command was guessed substitute_shortcuts -- Fills in the shortcut (if found) and guesses a command from that suggest_help -- Suggests that the user run the regular help command """ if not text: if safe: return None else: raise CBException("No guess text.") text = text.strip() split_content = text.split(' ', 1) if len(split_content) == 1: split_content.append('') base, parameters = split_content base = base.lower() try: command = bot.commands[base] except KeyError: if safe: return None else: if suggest_help: invoker = utilities.get_invoker(bot, message=message) additional = ' To see the menu, type `{}help`'.format(invoker) else: additional = '' raise CBException("Invalid base.{}".format(additional)) if isinstance(command, commands.Shortcut) and substitute_shortcuts: try: parameters = await fill_shortcut(bot, command, parameters, message) command = command.command except BotException: return command.command if not parameters or isinstance(command, commands.Shortcut): return command else: return await match_subcommand(bot, command, parameters, message, match_closest=True)
def get_general_help(bot, guild=None, is_owner=False): """Gets the general help. Lists all base commands that aren't shortcuts.""" response = "Here is a list of commands by group:\n" invoker = utilities.get_invoker(bot, guild=guild) ''' plugin_pairs = [] for plugin_name, plugin in bot.plugins.items(): plugin_pairs.append((plugin_name, plugin[1])) plugin_pairs.sort() ''' group_dictionary = {} for plugin_name, plugin in bot.plugins.items(): visible_commands = [] for command in plugin[1]: level = command.elevated_level hidden = command.hidden group = command.group if (((level < 3 and not hidden) or is_owner) and command not in visible_commands): if command.description: description = command.description else: description = '[Description not provided]' if group_dictionary.get(group) is None: group_dictionary[group] = [] group_dictionary[group].append('**`{0}`** -- {1}'.format( command.base, description)) listing = [] for group, entries in sorted(list(group_dictionary.items())): listing.append('\n***`{0}`***\n\t{1}'.format( group, '\n\t'.join(sorted(entries)))) response += '\n'.join(listing) + '\n' response += ("\nGet help on a command with `{0}help <command>`\n" "Confused by the syntax? See `{0}manual 3`").format(invoker) return response
async def get_response(bot, context): """Gets a response given the parsed input. context attributes: bot -- A reference to the bot itself. message -- The discord.message object obtained from on_message. base -- The base command name that immediately follows the invoker. subcommand -- The subcommand that matched the parameters. index -- The index of the found subcommand. options -- A dictionary representing the options and potential positional arguments that are attached to them. arguments -- A list of strings that follow the syntax of the blueprint index for arguments following the options. keywords -- Another list of strings that holds all option keywords. These can be used to prevent database conflicts with user commands. cleaned_content -- Simply the message content without the invoker. """ # This is what the bot will say when it returns from this function. # The response object can be manipulated in many ways. The attributes of # the response will be passed into the send function. response = Response() response.content = '' # Default # Set to True if you want your message read with /tts (not recommended). response.tts = False # Default # The message type dictates how the bot handles your returned message. # # NORMAL - Normal. The issuing command can be edited. # PERMANENT - Message is not added to the edit dictionary. # REPLACE - Deletes the issuing command after 'extra' seconds. Defaults # to 0 seconds if 'extra' is not given. # ACTIVE - The message reference is passed back to the function defined # with 'extra_function'. If 'extra_function' is not defined, it will call # plugin.handle_active_message. # INTERACTIVE - Assembles reaction buttons given by extra['buttons'] and # calls 'extra_function' whenever one is pressed. # WAIT - Wait for event. Calls 'extra_function' with the result, or None # if the wait timed out. # # Only the NORMAL message type can be edited. response.message_type = MessageTypes.NORMAL # Default # The extra variable is used for some message types. response.extra = None # Default # Initially, check to make sure that you've matched the proper command. # If there is only one command specified, this may not be necessary. index, options, arguments = context.index, context.options, context.arguments if context.base == 'mycommand': # Then, the subcommand index will tell you which command syntax was # satisfied. The order is the same as was specified initially. if index == 0: # myoption response.content = "You called the first subcommand!" # Do other stuff... elif index == 1: # custom/attached # To see if an optional option was included in the command, use: if 'custom' in options: response.content += "You included the \"custom\" flag!\n" # Do stuff relevant to this flag here... # To get the parameter attached to an option, simply access it from # the options dictionary. if 'attached' in options: response.content += "The attached parmeter: {}\n".format( options['attached']) # In case somebody was looking for the help... if len(options) == 0: invoker = utilities.get_invoker(bot, guild=context.guild) response.content += ( "You didn't use either flag...\n" "For help, try `{}help mycommand`".format(invoker)) elif index == 2: # trailing arguments # If arguments are specified as trailing, they will be in a list. response.content += "The list of trailing arguments: {}".format( arguments) elif index == 3: # grouped arguments # All arguments are grouped together as the first element response.message_type = MessageTypes.PERMANENT response.content = ("You can't edit your command here.\n" "Single grouped argument: {}").format( arguments[0]) elif index == 4: # complex # This mixes elements of both examples seen above. response.content = ("The argument attached to the complex " "option: {}").format(options['complex']) if 'other' in options: response.content += "\nThe other option has attached: {}".format( options['other']) response.content += "\nLastly, the trailing arguments: {}".format( arguments) elif index == 5: # (Very slow) marquee # This demonstrates the active message type. # Check active_marquee to see how it works. response.message_type = MessageTypes.ACTIVE response.extra_function = active_marquee # The function to call response.extra = arguments[0] # The text to use response.content = "Setting up marquee..." # This will be shown first # Here's another command base. elif context.base == 'myothercommand': if index == 0: # keyword checker text = arguments[0] if not text: response.content = "You didn't say anything...\n" else: response.content = "This is your input: {}\n".format(text) if text in context.keywords: response.content += "Your input was in the list of keywords!\n" else: response.content += ( "Your input was not in the list of keywords. " "They are: {}\n").format(context.keywords) response.message_type = MessageTypes.PERMANENT response.delete_after = 15 response.content += "This message will self destruct in 15 seconds." else: # impossible command??? raise CBException( "This is a bug! You should never see this message.") elif context.base == 'wait': response.message_type = MessageTypes.WAIT # The extra attribute should consist of a dictionary containing the # event and any other kwargs. Most notably, you will likely want to # define the check used in wait_for. response.extra_function = custom_interaction response.extra = { 'event': 'message', 'kwargs': { 'timeout': 30, # Default 300 'check': lambda m: m.author == context.author, } } response.content = "Say something, {}.".format(context.author) return response
async def get_response(bot, context): """Gets a response given the parsed input. context attributes: bot -- A reference to the bot itself. message -- The discord.message object obtained from on_message. base -- The base command name that immediately follows the invoker. subcommand -- The subcommand that matched the parameters. index -- The index of the found subcommand. options -- A dictionary representing the options and potential positional arguments that are attached to them. arguments -- A list of strings that follow the syntax of the blueprint index for arguments following the options. keywords -- Another list of strings that holds all option keywords. These can be used to prevent database conflicts with user commands. cleaned_content -- Simply the message content without the invoker. """ # This is what the bot will say when it returns from this function. # The response object can be manipulated in many ways. The attributes of # the response will be passed into the send function. response = Response() response.content = '' # Default # Set to True if you want your message read with /tts (not recommended). response.tts = False # Default # The message type dictates how the bot handles your returned message. # # NORMAL - Normal. The issuing command can be edited. # PERMANENT - Message is not added to the edit dictionary. # REPLACE - Deletes the issuing command after 'extra' seconds. Defaults # to 0 seconds if 'extra' is not given. # ACTIVE - The message reference is passed back to the function defined # with 'extra_function'. If 'extra_function' is not defined, it will call # plugin.handle_active_message. # INTERACTIVE - Assembles reaction buttons given by extra['buttons'] and # calls 'extra_function' whenever one is pressed. # WAIT - Wait for event. Calls 'extra_function' with the result, or None # if the wait timed out. # # Only the NORMAL message type can be edited. response.message_type = MessageTypes.NORMAL # Default # The extra variable is used for some message types. response.extra = None # Default # Initially, check to make sure that you've matched the proper command. # If there is only one command specified, this may not be necessary. index, options, arguments = context.index, context.options, context.arguments if context.base == 'mycommand': # Then, the subcommand index will tell you which command syntax was # satisfied. The order is the same as was specified initially. if index == 0: # myoption response.content = "You called the first subcommand!" # Do other stuff... elif index == 1: # custom/attached # To see if an optional option was included in the command, use: if 'custom' in options: response.content += "You included the \"custom\" flag!\n" # Do stuff relevant to this flag here... # To get the parameter attached to an option, simply access it from # the options dictionary. if 'attached' in options: response.content += "The attached parmeter: {}\n".format(options['attached']) # In case somebody was looking for the help... if len(options) == 0: invoker = utilities.get_invoker(bot, guild=context.guild) response.content += ("You didn't use either flag...\n" "For help, try `{}help mycommand`".format(invoker)) elif index == 2: # trailing arguments # If arguments are specified as trailing, they will be in a list. response.content += "The list of trailing arguments: {}".format(arguments) elif index == 3: # grouped arguments # All arguments are grouped together as the first element response.message_type = MessageTypes.PERMANENT response.content = ("You can't edit your command here.\n" "Single grouped argument: {}").format(arguments[0]) elif index == 4: # complex # This mixes elements of both examples seen above. response.content = ("The argument attached to the complex " "option: {}").format(options['complex']) if 'other' in options: response.content += "\nThe other option has attached: {}".format(options['other']) response.content += "\nLastly, the trailing arguments: {}".format(arguments) elif index == 5: # (Very slow) marquee # This demonstrates the active message type. # Check active_marquee to see how it works. response.message_type = MessageTypes.ACTIVE response.extra_function = active_marquee # The function to call response.extra = arguments[0] # The text to use response.content = "Setting up marquee..." # This will be shown first # Here's another command base. elif context.base == 'myothercommand': if index == 0: # keyword checker text = arguments[0] if not text: response.content = "You didn't say anything...\n" else: response.content = "This is your input: {}\n".format(text) if text in context.keywords: response.content += "Your input was in the list of keywords!\n" else: response.content += ("Your input was not in the list of keywords. " "They are: {}\n").format(context.keywords) response.message_type = MessageTypes.PERMANENT response.delete_after = 15 response.content += "This message will self destruct in 15 seconds." else: # impossible command??? raise CBException("This is a bug! You should never see this message.") elif context.base == 'wait': response.message_type = MessageTypes.WAIT # The extra attribute should consist of a dictionary containing the # event and any other kwargs. Most notably, you will likely want to # define the check used in wait_for. response.extra_function = custom_interaction response.extra = { 'event': 'message', 'kwargs': { 'timeout': 30, # Default 300 'check': lambda m: m.author == context.author, } } response.content = "Say something, {}.".format(context.author) return response
def get_manual(bot, subject_id=None, topic_index=None, page=None, guild=None, safe=True): """ Gets the given manual entry depending on depth. If subject is not provided: returns a tuple: ('Manual menu', subject_listing) If subject is provided: return a tuple: (subject_name, topic_listing) If topic is provivded: return a tuple: (topic_name, text) All return values also include (1 indexed): (page_number, total_pages, crumbs) These return values are to be used as embed fields. """ MAX_E = 5 invoker = utilities.get_invoker(bot, guild=guild) base_invoker = utilities.get_invoker(bot) crumbs = 'Manual menu' if page is None: page = 0 subjects = list(bot.manuals.values()) if subject_id is not None: # Subject selected; browsing topics try: subject_index = int(subject_id) assert subject_index >= 0 # TODO: Better checking except ValueError: # Subject text given subject_name = subject_id.lower() if subject_name not in bot.manuals: if safe: return raise CBException("Invalid subject.") subject = bot.manuals[subject_name] else: # Subject index given try: subject = subjects[subject_index] except IndexError: if safe: return raise CBException( "Invalid subject index. Must be between 1 and {}.".format(len(bot.manuals))) topics = subject['topics'] crumbs += '\t→\t{}'.format(subject['subject']) if topic_index is not None: # Topic selected; browsing text pages try: topic = topics[topic_index] except: if safe: return raise CBException( "Invalid topic index. Must be between 1 and {}.".format(len(topics))) if not 0 <= page < len(topic[1]): if safe: return raise CBException( "Invalid page number. Must be between 1 and {}.".format(len(topic[1]))) text = topic[1][page].format(invoker=invoker, base_invoker=base_invoker) crumbs += '\t→\t{}'.format(topic[0]) return crumbs, text, page, len(topic[1]) - 1 else: # Subject selected; browsing topics total_topic_pages = int((len(topics)-1) / MAX_E) if not 0 <= page <= total_topic_pages: if safe: return raise CBException( "Invalid topic page number. Must be between 1 and {}.".format( total_topic_pages+1)) topics = topics[page*MAX_E:(page + 1)*MAX_E] raw_list = [] for index, topic in enumerate(topics): raw_list.append('{} {}'.format(numeric_words[index + 1], topic[0])) topic_listing = '\n'.join(raw_list) return crumbs, topic_listing, page, total_topic_pages else: # No subject_id given; get general subject listing total_subject_pages = int((len(subjects)-1) / MAX_E) if not 0 <= page <= total_subject_pages: if safe: return raise CBException( "Invalid subject page. Must be between 1 and {}.".format(total_subject_pages+1)) subjects = subjects[page*MAX_E:(page + 1)*MAX_E] raw_list = [] for index, subject_pair in enumerate(subjects): raw_list.append('{} {}'.format(numeric_words[index + 1], subject_pair['subject'])) subject_listing = '\n'.join(raw_list) return crumbs, subject_listing, page, total_subject_pages
def get_help( bot, category_id=None, command_index=None, subcommand_index=None, page=None, guild=None, safe=True, using_menu=True, elevation=0): """ Gets the help entry depending on depth. Help me. """ MAX_E = 5 invoker = utilities.get_invoker(bot, guild=guild) base_invoker = utilities.get_invoker(bot) crumbs = 'Help menu' if page is None: page = 0 categories = OrderedDict([('Core', [])]) for command in bot.commands.values(): if isinstance(command, commands.Command): if not command.hidden or elevation >= 3: if command.category in categories: categories[command.category].append(command) else: categories[command.category] = [command] categories = OrderedDict(sorted(list(categories.items()))) if category_id is not None: # Category selected; browsing commands listing try: category_index = int(category_id) assert category_index >= 0 # TODO: Better checking except ValueError: # Category text given category_name = category_id.title() if category_name not in categories: if safe: return raise CBException("Invalid category") commands_listing = sorted(categories[category_name]) else: categories_listing = list(categories.values()) try: commands_listing = sorted(categories_listing[category_index]) except: if safe: return raise CBException( "Invalid category index. Must be between 1 and {}".format( len(categories_listing))) category_name = list(categories)[category_index] crumbs += '\t→\t{}'.format(category_name) if command_index is not None: # Command selected; browsing subcommands try: command = commands_listing[command_index] except: if safe: return raise CBException( "Invalid command index. Must be between 1 and {}.".format( len(commands_listing))) crumbs += '\t→\t{}'.format(command.base) subcommands = command.subcommands if subcommand_index is not None: # Subcommand selected; browsing detailed help try: subcommand = subcommands[subcommand_index] except: if safe: return raise CBException( "Invalid subcommand index. Must be between 1 and {}.".format( len(subcommands))) crumbs += '\t→\tSubcommand {}'.format(subcommand.index + 1) if using_menu: # Add crumbs embed_fields = [(crumbs, '\u200b')] + subcommand.help_embed_fields else: embed_fields = subcommand.help_embed_fields return embed_fields, 0, 0 else: # No subcommand selected; browsing command still total_pages = int((len(command.help_lines)-1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid subcommand page. Must be between 1 and {}".format( total_pages+1)) if using_menu: # Add numbers help_lines = command.help_lines[page*MAX_E:(page + 1)*MAX_E] raw_list = [] for index, entry in enumerate(help_lines): raw_list.append('{} {}'.format(numeric_words[index + 1], entry)) embed_fields = [(crumbs, '\u200b')] + command.help_embed_fields embed_fields[command.usage_embed_index + 1] = ('Usage:', '\n'.join(raw_list)) else: embed_fields = command.help_embed_fields return embed_fields, page, total_pages else: # No command selected; browsing commands listing still total_pages = int((len(commands_listing)-1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid command page. Must be between 1 and {}".format(total_pages+1)) commands_listing = commands_listing[page*MAX_E:(page + 1)*MAX_E] raw_list = [] for index, command in enumerate(commands_listing): raw_list.append('{} **`{}`** -- {}'.format( numeric_words[index + 1], command.base, command.description)) embed_fields = [(crumbs, '\n'.join(raw_list))] return embed_fields, page, total_pages else: # Nothing selected. Get category listing total_pages = int((len(categories)-1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid category page. Must be between 1 and {}".format(total_pages+1)) category_pairs = list(categories.items())[page*MAX_E:(page + 1)*MAX_E] raw_list = [] for index, category_pair in enumerate(category_pairs): category_name, category_commands = category_pair peek_commands = [command.base for command in category_commands[:3]] if len(category_commands) > 3: peek_commands.append('...') peek = '[`{}`]'.format('`, `'.join(peek_commands)) raw_list.append('{} **{}**\n\t\t{}'.format( numeric_words[index + 1], category_name, peek)) embed_fields = [(crumbs, '\n'.join(raw_list))] return embed_fields, page, total_pages pass
def get_manual(bot, subject_id=None, topic_index=None, page=None, guild=None, safe=True): """ Gets the given manual entry depending on depth. If subject is not provided: returns a tuple: ('Manual menu', subject_listing) If subject is provided: return a tuple: (subject_name, topic_listing) If topic is provivded: return a tuple: (topic_name, text) All return values also include (1 indexed): (page_number, total_pages, crumbs) These return values are to be used as embed fields. """ MAX_E = 5 invoker = utilities.get_invoker(bot, guild=guild) base_invoker = utilities.get_invoker(bot) crumbs = 'Manual menu' if page is None: page = 0 subjects = list(bot.manuals.values()) if subject_id is not None: # Subject selected; browsing topics try: subject_index = int(subject_id) assert subject_index >= 0 # TODO: Better checking except ValueError: # Subject text given subject_name = subject_id.lower() if subject_name not in bot.manuals: if safe: return raise CBException("Invalid subject.") subject = bot.manuals[subject_name] else: # Subject index given try: subject = subjects[subject_index] except IndexError: if safe: return raise CBException( "Invalid subject index. Must be between 1 and {}.".format( len(bot.manuals))) topics = subject['topics'] crumbs += '\t→\t{}'.format(subject['subject']) if topic_index is not None: # Topic selected; browsing text pages try: topic = topics[topic_index] except: if safe: return raise CBException( "Invalid topic index. Must be between 1 and {}.".format( len(topics))) if not 0 <= page < len(topic[1]): if safe: return raise CBException( "Invalid page number. Must be between 1 and {}.".format( len(topic[1]))) text = topic[1][page].format(invoker=invoker, base_invoker=base_invoker) crumbs += '\t→\t{}'.format(topic[0]) return crumbs, text, page, len(topic[1]) - 1 else: # Subject selected; browsing topics total_topic_pages = int((len(topics) - 1) / MAX_E) if not 0 <= page <= total_topic_pages: if safe: return raise CBException( "Invalid topic page number. Must be between 1 and {}.". format(total_topic_pages + 1)) topics = topics[page * MAX_E:(page + 1) * MAX_E] raw_list = [] for index, topic in enumerate(topics): raw_list.append('{} {}'.format(numeric_words[index + 1], topic[0])) topic_listing = '\n'.join(raw_list) return crumbs, topic_listing, page, total_topic_pages else: # No subject_id given; get general subject listing total_subject_pages = int((len(subjects) - 1) / MAX_E) if not 0 <= page <= total_subject_pages: if safe: return raise CBException( "Invalid subject page. Must be between 1 and {}.".format( total_subject_pages + 1)) subjects = subjects[page * MAX_E:(page + 1) * MAX_E] raw_list = [] for index, subject_pair in enumerate(subjects): raw_list.append('{} {}'.format(numeric_words[index + 1], subject_pair['subject'])) subject_listing = '\n'.join(raw_list) return crumbs, subject_listing, page, total_subject_pages
async def match_subcommand(bot, command, parameters, message, match_closest=False): """Matches the given parameters to a valid subcommand from the command. Returns a tuple of the subcommand, options, and arguments. If match_closest is True, returns the closest matching subcommand or None. No processing (conversion, checking) is done, and returns only the subcommand or None. """ parameters, quoted_indices = split_parameters(parameters, quote_list=True) closest_index = -1 closest_index_matches = 0 closest_index_error = None stripped_parameters = parameters[::2] for subcommand in command.subcommands: current_index = 0 matches = 0 options = {} arguments = [] last_opt_index = -1 arg_index = -1 used_opts = [] exhausted_opts = len(subcommand.opts) == 0 not_found_error = None while current_index < len(stripped_parameters): current = stripped_parameters[current_index] if not exhausted_opts: # Check opts if current_index * 2 in quoted_indices: # Quoted elements are always arguments exhausted_opts = True found_opt = subcommand.opts.get(current.lower(), None) if not exhausted_opts and found_opt: if subcommand.strict_syntax: # Check strict syntax if found_opt.index < last_opt_index: # Syntax out of order exhausted_opts = True else: last_opt_index = found_opt.index if not exhausted_opts: if found_opt.name in options: # Duplicate. Skip to args exhausted_opts = True else: # Check for attached argument if found_opt.attached: # Required attached argument if current_index + 1 >= len(stripped_parameters): not_found_error = ( 'Option {opt.name_string} requires an attached parameter, ' '{opt.attached_string}.'.format(opt=found_opt)) matches += 3 else: current_index += 1 options[found_opt.name] = stripped_parameters[current_index] matches += 6 else: # No attached argument required options[found_opt.name] = None matches += 5 used_opts.append(found_opt) else: # Option not found. Skip to args exhausted_opts = True if exhausted_opts: # No more matching opts - check for optional opts current_index -= 1 # Search args where we left off remaining_opts = [o for o in subcommand.opts.values() if o not in used_opts] for opt in remaining_opts: if opt.optional: matches += 1 if opt.always_include: options[opt.name] = opt.default else: # Not optional. Unfit subcommand not_found_error = 'Option {} is required.'.format(opt.name_string) break else: # Check args arg_index += 1 if arg_index >= len(subcommand.args): # Too many arguments not_found_error = 'Too many arguments.' else: matches += 1 arg = subcommand.args[arg_index] if arg.argtype in (ArgTypes.SINGLE, ArgTypes.OPTIONAL): arguments.append(current) else: # Instant finish grouped arguments if arg.argtype in (ArgTypes.SPLIT, ArgTypes.SPLIT_OPTIONAL): arguments += stripped_parameters[current_index:] else: # Merged split_arguments = [] quote_index = current_index * 2 for segment in parameters[current_index * 2:]: if quote_index in quoted_indices: # Add quotes back in split_arguments.append('"{}"'.format(segment)) else: split_arguments.append(segment) quote_index += 1 arguments += [''.join(split_arguments)] break if not_found_error: # Skip rest of loop and evaluate matches break current_index += 1 # Finished opt/arg while loop if not not_found_error and not exhausted_opts: # Opts remain remaining_opts = [o for o in subcommand.opts.values() if o not in used_opts] for opt in remaining_opts: if opt.optional: matches += 1 if opt.always_include: options[opt.name] = opt.default else: # Not optional. Unfit subcommand not_found_error = 'Option {} is required.'.format(opt.name_string) break if not not_found_error and arg_index < len(subcommand.args) - 1: # Optional arguments arg_index += 1 arg = subcommand.args[arg_index] if arg.argtype is ArgTypes.OPTIONAL: matches += 1 while (arg and arg.argtype is ArgTypes.OPTIONAL and arg_index < len(subcommand.args)): arguments.append(arg.default) arg_index += 1 try: arg = subcommand.args[arg_index] except: arg = None if arg and arg.argtype in (ArgTypes.SPLIT_OPTIONAL, ArgTypes.MERGED_OPTIONAL): matches += 1 arguments.append(arg.default) elif arg: not_found_error = 'No value given for argument {}.'.format(arg.help_string) if not not_found_error: # Check for message attachment if subcommand.attaches: if message.attachments or subcommand.attaches.optional: matches += 6 else: not_found_error = 'Missing attachment **__`{name}`__**'.format( name=subcommand.attaches.name) elif message.attachments: # No attachment argument, but attachment was provided not_found_error = 'No attachment required, but one was given.' if not_found_error: # Find closest subcommand if matches > closest_index_matches: closest_index = subcommand.index closest_index_matches = matches closest_index_error = not_found_error else: # Subcommand found. Convert and check if subcommand.confidence_threshold is not None: # Confidence threshold if closest_index_matches >= subcommand.confidence_threshold: continue # Skip valid match due to low confidence # No additional processing if match_closest: if matches <= 1 and matches < closest_index_matches: # No confidence continue else: return subcommand # Cannot match parameters in a direct message if disabled elif not subcommand.allow_direct and isinstance(message.channel, PrivateChannel): return subcommand, {}, [] # Fill in options and arguments else: for option_name, value in options.items(): # Check options current_opt = subcommand.opts[option_name] if value is not None: new_value = await current_opt.convert_and_check(bot, message, value) if new_value is not None: options[option_name] = new_value for index, pair in enumerate(zip(subcommand.args, arguments)): # Check arguments arg, value = pair if (value is not None or arg.argtype in (ArgTypes.SINGLE, ArgTypes.SPLIT, ArgTypes.MERGED)): if arg.argtype not in (ArgTypes.SINGLE, ArgTypes.OPTIONAL): new_values = await arg.convert_and_check( bot, message, arguments[index:]) arguments = arguments[:index] + new_values break else: new_value = await arg.convert_and_check(bot, message, value) arguments[index] = new_value return subcommand, options, arguments # Looped through all subcommands. Not found if closest_index == -1 or closest_index_matches <= 1: # Low confidence guess = command else: guess = command.subcommands[closest_index] if match_closest: return guess else: if isinstance(guess, commands.SubCommand): syntax_error = 'Invalid syntax: {}'.format(closest_index_error) else: guess = command syntax_error = 'Invalid syntax.' invoker = utilities.get_invoker(bot, guild=message.guild) raise CBException( syntax_error, embed_fields=guess.help_embed_fields, embed_format={'invoker': invoker})
def get_help(bot, category_id=None, command_index=None, subcommand_index=None, page=None, guild=None, safe=True, using_menu=True, elevation=0): """ Gets the help entry depending on depth. Help me. """ MAX_E = 5 invoker = utilities.get_invoker(bot, guild=guild) base_invoker = utilities.get_invoker(bot) crumbs = 'Help menu' if page is None: page = 0 categories = OrderedDict([('Core', [])]) for command in bot.commands.values(): if isinstance(command, commands.Command): if not command.hidden or elevation >= 3: if command.category in categories: categories[command.category].append(command) else: categories[command.category] = [command] categories = OrderedDict(sorted(list(categories.items()))) if category_id is not None: # Category selected; browsing commands listing try: category_index = int(category_id) assert category_index >= 0 # TODO: Better checking except ValueError: # Category text given category_name = subject_id.title() if category_name not in categories: if safe: return raise CBException("Invalid category") commands_listing = sorted(categories[category_name]) else: categories_listing = list(categories.values()) try: commands_listing = sorted(categories_listing[category_index]) except: if safe: return raise CBException( "Invalid category index. Must be between 1 and {}".format( len(categories_listing))) category_name = list(categories)[category_index] crumbs += '\t→\t{}'.format(category_name) if command_index is not None: # Command selected; browsing subcommands try: command = commands_listing[command_index] except: if safe: return raise CBException( "Invalid command index. Must be between 1 and {}.".format( len(commands_listing))) crumbs += '\t→\t{}'.format(command.base) subcommands = command.subcommands if subcommand_index is not None: # Subcommand selected; browsing detailed help try: subcommand = subcommands[subcommand_index] except: if safe: return raise CBException( "Invalid subcommand index. Must be between 1 and {}.". format(len(subcommands))) crumbs += '\t→\tSubcommand {}'.format(subcommand.index + 1) if using_menu: # Add crumbs embed_fields = [(crumbs, '\u200b') ] + subcommand.help_embed_fields else: embed_fields = subcommand.help_embed_fields return embed_fields, 0, 0 else: # No subcommand selected; browsing command still total_pages = int((len(command.help_lines) - 1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid subcommand page. Must be between 1 and {}". format(total_pages + 1)) if using_menu: # Add numbers help_lines = command.help_lines[page * MAX_E:(page + 1) * MAX_E] raw_list = [] for index, entry in enumerate(help_lines): raw_list.append('{} {}'.format( numeric_words[index + 1], entry)) embed_fields = [(crumbs, '\u200b') ] + command.help_embed_fields embed_fields[command.usage_embed_index + 1] = ('Usage:', '\n'.join(raw_list)) else: embed_fields = command.help_embed_fields return embed_fields, page, total_pages else: # No command selected; browsing commands listing still total_pages = int((len(commands_listing) - 1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid command page. Must be between 1 and {}".format( total_pages + 1)) commands_listing = commands_listing[page * MAX_E:(page + 1) * MAX_E] raw_list = [] for index, command in enumerate(commands_listing): raw_list.append('{} **`{}`** -- {}'.format( numeric_words[index + 1], command.base, command.description)) embed_fields = [(crumbs, '\n'.join(raw_list))] return embed_fields, page, total_pages else: # Nothing selected. Get category listing total_pages = int((len(categories) - 1) / MAX_E) if not 0 <= page <= total_pages: if safe: return raise CBException( "Invalid category page. Must be between 1 and {}".format( total_pages + 1)) category_pairs = list(categories.items())[page * MAX_E:(page + 1) * MAX_E] raw_list = [] for index, category_pair in enumerate(category_pairs): category_name, category_commands = category_pair peek_commands = [command.base for command in category_commands[:3]] if len(category_commands) > 3: peek_commands.append('...') peek = '[`{}`]'.format('`, `'.join(peek_commands)) raw_list.append('{} **{}**\n\t\t{}'.format( numeric_words[index + 1], category_name, peek)) embed_fields = [(crumbs, '\n'.join(raw_list))] return embed_fields, page, total_pages pass