aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/core
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts/core')
-rw-r--r--bot/exts/core/error_handler.py46
-rw-r--r--bot/exts/core/help.py49
-rw-r--r--bot/exts/core/internal_eval/_internal_eval.py5
-rw-r--r--bot/exts/core/source.py4
4 files changed, 65 insertions, 39 deletions
diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py
index 676a1e70..4578f734 100644
--- a/bot/exts/core/error_handler.py
+++ b/bot/exts/core/error_handler.py
@@ -1,4 +1,3 @@
-import difflib
import logging
import math
import random
@@ -11,6 +10,7 @@ from sentry_sdk import push_scope
from bot.bot import Bot
from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput
+from bot.utils.commands import get_command_suggestions
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingError
@@ -98,7 +98,8 @@ class CommandErrorHandler(commands.Cog):
if isinstance(error, commands.NoPrivateMessage):
await ctx.send(
embed=self.error_embed(
- f"This command can only be used in the server. Go to <#{Channels.community_bot_commands}> instead!",
+ "This command can only be used in the server. "
+ f"Go to <#{Channels.sir_lancebot_playground}> instead!",
NEGATIVE_REPLIES
)
)
@@ -157,31 +158,32 @@ class CommandErrorHandler(commands.Cog):
async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None:
"""Sends user similar commands if any can be found."""
- raw_commands = []
- for cmd in self.bot.walk_commands():
- if not cmd.hidden:
- raw_commands += (cmd.name, *cmd.aliases)
- if similar_command_data := difflib.get_close_matches(command_name, raw_commands, 1):
- similar_command_name = similar_command_data[0]
- similar_command = self.bot.get_command(similar_command_name)
-
- if not similar_command:
- return
-
- log_msg = "Cancelling attempt to suggest a command due to failed checks."
- try:
- if not await similar_command.can_run(ctx):
+ command_suggestions = []
+ if similar_command_names := get_command_suggestions(list(self.bot.all_commands.keys()), command_name):
+ for similar_command_name in similar_command_names:
+ similar_command = self.bot.get_command(similar_command_name)
+
+ if not similar_command:
+ continue
+
+ log_msg = "Cancelling attempt to suggest a command due to failed checks."
+ try:
+ if not await similar_command.can_run(ctx):
+ log.debug(log_msg)
+ continue
+ except commands.errors.CommandError as cmd_error:
log.debug(log_msg)
- return
- except commands.errors.CommandError as cmd_error:
- log.debug(log_msg)
- await self.on_command_error(ctx, cmd_error)
- return
+ await self.on_command_error(ctx, cmd_error)
+ continue
+
+ command_suggestions.append(similar_command_name)
misspelled_content = ctx.message.content
e = Embed()
e.set_author(name="Did you mean:", icon_url=QUESTION_MARK_ICON)
- e.description = misspelled_content.replace(command_name, similar_command_name, 1)
+ e.description = "\n".join(
+ misspelled_content.replace(command_name, cmd, 1) for cmd in command_suggestions
+ )
await ctx.send(embed=e, delete_after=RedirectOutput.delete_delay)
diff --git a/bot/exts/core/help.py b/bot/exts/core/help.py
index db3c2aa6..eb7a9762 100644
--- a/bot/exts/core/help.py
+++ b/bot/exts/core/help.py
@@ -3,16 +3,17 @@ import asyncio
import itertools
import logging
from contextlib import suppress
-from typing import NamedTuple, Union
+from typing import NamedTuple, Optional, Union
from discord import Colour, Embed, HTTPException, Message, Reaction, User
from discord.ext import commands
from discord.ext.commands import CheckFailure, Cog as DiscordCog, Command, Context
-from rapidfuzz import process
from bot import constants
from bot.bot import Bot
from bot.constants import Emojis
+from bot.utils.commands import get_command_suggestions
+from bot.utils.decorators import whitelist_override
from bot.utils.pagination import FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI
DELETE_EMOJI = Emojis.trashcan
@@ -41,14 +42,18 @@ class HelpQueryNotFound(ValueError):
"""
Raised when a HelpSession Query doesn't match a command or cog.
- Contains the custom attribute of ``possible_matches``.
- Instances of this object contain a dictionary of any command(s) that were close to matching the
- query, where keys are the possible matched command names and values are the likeness match scores.
+ Params:
+ possible_matches: list of similar command names.
+ parent_command: parent command of an invalid subcommand. Only available when an invalid subcommand
+ has been passed.
"""
- def __init__(self, arg: str, possible_matches: dict = None):
+ def __init__(
+ self, arg: str, possible_matches: Optional[list[str]] = None, *, parent_command: Optional[Command] = None
+ ) -> None:
super().__init__(arg)
self.possible_matches = possible_matches
+ self.parent_command = parent_command
class HelpSession:
@@ -153,12 +158,17 @@ class HelpSession:
Will pass on possible close matches along with the `HelpQueryNotFound` exception.
"""
- # Combine command and cog names
- choices = list(self._bot.all_commands) + list(self._bot.cogs)
+ # Check if parent command is valid in case subcommand is invalid.
+ if " " in query:
+ parent, *_ = query.split()
+ parent_command = self._bot.get_command(parent)
+
+ if parent_command:
+ raise HelpQueryNotFound('Invalid Subcommand.', parent_command=parent_command)
- result = process.extract(query, choices, score_cutoff=90)
+ similar_commands = get_command_suggestions(list(self._bot.all_commands.keys()), query)
- raise HelpQueryNotFound(f'Query "{query}" not found.', dict(result))
+ raise HelpQueryNotFound(f'Query "{query}" not found.', similar_commands)
async def timeout(self, seconds: int = 30) -> None:
"""Waits for a set number of seconds, then stops the help session."""
@@ -277,7 +287,7 @@ class HelpSession:
else:
results.append(f"<{name}>")
- return f"{cmd.name} {' '.join(results)}"
+ return f"{cmd.qualified_name} {' '.join(results)}"
async def build_pages(self) -> None:
"""Builds the list of content pages to be paginated through in the help message, as a list of str."""
@@ -304,9 +314,10 @@ class HelpSession:
prefix = constants.Client.prefix
signature = self._get_command_params(self.query)
+ paginator.add_line(f"**```\n{prefix}{signature}\n```**")
+
parent = self.query.full_parent_name + " " if self.query.parent else ""
- paginator.add_line(f"**```\n{prefix}{parent}{signature}\n```**")
- aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases]
+ aliases = [f"`{alias}`" if not parent else f"`{parent}{alias}`" for alias in self.query.aliases]
aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())]
aliases = ", ".join(sorted(aliases))
if aliases:
@@ -502,18 +513,26 @@ class Help(DiscordCog):
"""Custom Embed Pagination Help feature."""
@commands.command("help")
+ @whitelist_override(allow_dm=True)
async def new_help(self, ctx: Context, *commands) -> None:
"""Shows Command Help."""
try:
await HelpSession.start(ctx, *commands)
except HelpQueryNotFound as error:
+
+ # Send help message of parent command if subcommand is invalid.
+ if cmd := error.parent_command:
+ await ctx.send(str(error))
+ await self.new_help(ctx, cmd.qualified_name)
+ return
+
embed = Embed()
embed.colour = Colour.red()
embed.title = str(error)
if error.possible_matches:
- matches = "\n".join(error.possible_matches.keys())
- embed.description = f"**Did you mean:**\n`{matches}`"
+ matches = "\n".join(error.possible_matches)
+ embed.description = f"**Did you mean:**\n{matches}"
await ctx.send(embed=embed)
diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py
index 5b5461f0..190a15ec 100644
--- a/bot/exts/core/internal_eval/_internal_eval.py
+++ b/bot/exts/core/internal_eval/_internal_eval.py
@@ -34,6 +34,8 @@ RAW_CODE_REGEX = re.compile(
re.DOTALL # "." also matches newlines
)
+MAX_LENGTH = 99980
+
class InternalEval(commands.Cog):
"""Top secret code evaluation for admins and owners."""
@@ -85,9 +87,10 @@ class InternalEval(commands.Cog):
async def _upload_output(self, output: str) -> Optional[str]:
"""Upload `internal eval` output to our pastebin and return the url."""
+ data = self.shorten_output(output, max_length=MAX_LENGTH)
try:
async with self.bot.http_session.post(
- "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True
+ "https://paste.pythondiscord.com/documents", data=data, raise_for_status=True
) as resp:
data = await resp.json()
diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py
index 7572ce51..2801be0f 100644
--- a/bot/exts/core/source.py
+++ b/bot/exts/core/source.py
@@ -6,14 +6,16 @@ from discord import Embed
from discord.ext import commands
from bot.bot import Bot
-from bot.constants import Source
+from bot.constants import Channels, Source, WHITELISTED_CHANNELS
from bot.utils.converters import SourceConverter, SourceType
+from bot.utils.decorators import whitelist_override
class BotSource(commands.Cog):
"""Displays information about the bot's source code."""
@commands.command(name="source", aliases=("src",))
+ @whitelist_override(channels=WHITELISTED_CHANNELS+(Channels.community_meta, Channels.dev_contrib))
async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None:
"""Display information and a GitHub link to the source code of a command, tag, or cog."""
if not source_item: