mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-05 18:58:53 -05:00
* Temporarily set d.py to use latest git revision
* Remove `bot` param to Client.start
* Switch to aware datetimes
A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.
* Update to work with new Asset design
* [threads] Update core ModLog API to support threads
- Added proper support for passing `Thread` to `channel`
when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
- Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information
* [threads] Disallow usage of threads in some of the commands
- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
- permissions addglobalrule
- permissions removeglobalrule
- permissions removeserverrule
- Permissions cog does not perform any validation for IDs
when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination
* [threads] Handle threads in Red's permissions system (Requires)
- Made permissions system apply rules of (only) parent in threads
* [threads] Update embed_requested to support threads
- Threads don't have their own embed settings and inherit from parent
* [threads] Update Red.message_eligible_as_command to support threads
* [threads] Properly handle invocation of [p](un)mutechannel in threads
Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.
* [threads] Update Filter cog to properly handle threads
- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
channel's word list. There's no separate word list for threads.
* [threads] Support threads in Audio cog
- Handle threads being notify channels
- Update type hint for `is_query_allowed()`
* [threads] Update type hints and documentation to reflect thread support
- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
- Separate thread scope is still in the picture though
if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread
* Use less costy channel check in customcom's on_message_without_command
This isn't needed for d.py 2.0 but whatever...
* Update for in-place edits
* Embed's bool changed behavior, I'm hoping it doesn't affect us
* Address User.permissions_in() removal
* Swap VerificationLevel.extreme with VerificationLevel.highest
* Change to keyword-only parameters
* Change of `Guild.vanity_invite()` return type
* avatar -> display_avatar
* Fix metaclass shenanigans with Converter
* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`
This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.
* Address all DEP-WARNs
* Remove Context.clean_prefix and use upstream implementation instead
* Remove commands.Literal and use upstream implementation instead
Honestly, this was a rather bad implementation anyway...
Breaking but actually not really - it was provisional.
* Update Command.callback's setter
Support for functools.partial is now built into d.py
* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)
BTW, that should really be in core instead of what we have now...
* Remove the part of do_conversion that has not worked for a long while
* Stop wrapping BadArgument in ConversionFailure
This is breaking but it's best to resolve it like this.
The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.
* Add custom errors for int and float converters
* Remove Command.__call__ as it's now implemented in d.py
* Get rid of _dpy_reimplements
These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.
* Add return to Red.remove_cog
* Ensure we don't delete messages that differ only by used sticker
* discord.InvalidArgument->ValueError
* Move from raw <t:...> syntax to discord.utils.format_dt()
* Address AsyncIter removal
* Swap to pos-only for params that are pos-only in upstream
* Update for changes to Command.params
* [threads] Support threads in ignore checks and allow ignoring them
- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`
Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
- check if user has manage channels perm in channel
and allow command usage if so
- check if channel is ignored and disallow command usage if so
- allow command usage if none of the conditions above happened
- if channel is a thread:
- check if user has manage channels perm in parent channel
and allow command usage if so
- check if parent channel is ignored and disallow command usage
if so
- check if user has manage thread perm in parent channel
and allow command usage if so
- check if thread is ignored and disallow command usage if so
- allow command usage if none of the conditions above happened
* [partial] Raise TypeError when channel is of PartialMessageable type
- Red.embed_requested
- Red.ignored_channel_or_guild
* [partial] Discard command messages when channel is PartialMessageable
* [threads] Add utilities for checking appropriate perms in both channels & threads
* [threads] Update code to use can_react_in() and @bot_can_react()
* [threads] Update code to use can_send_messages_in
* [threads] Add send_messages_in_threads perm to mute role and overrides
* [threads] Update code to use (bot/user)_can_manage_channel
* [threads] Update [p]diagnoseissues to work with threads
* Type hint fix
* [threads] Patch vendored discord.ext.menus to check proper perms in threads
I guess we've reached time when we have to patch the lib we vendor...
* Make docs generation work with non-final d.py releases
* Update discord.utils.oauth_url() usage
* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None
* Update usage of Guild.member_count to work with `None`
* Switch from Guild.vanity_invite() to Guild.vanity_url
* Update startup process to work with d.py's new asynchronous startup
* Use setup_hook() for pre-connect actions
* Update core's add_cog, remove_cog, and load_extension methods
* Update all setup functions to async and add awaits to bot.add_cog calls
* Modernize cogs by using async cog_load and cog_unload
* Address StoreChannel removal
* [partial] Disallow passing PartialMessageable to Case.channel
* [partial] Update cogs and utils to work better with PartialMessageable
- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid
* Add few missing DEP-WARNs
500 lines
17 KiB
Python
500 lines
17 KiB
Python
"""
|
|
commands.converter
|
|
==================
|
|
This module contains useful functions and classes for command argument conversion.
|
|
|
|
Some of the converters within are included provisionally and are marked as such.
|
|
"""
|
|
import functools
|
|
import re
|
|
from datetime import timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Generic,
|
|
Optional,
|
|
Optional as NoParseOptional,
|
|
Tuple,
|
|
List,
|
|
Dict,
|
|
Type,
|
|
TypeVar,
|
|
Union as UserInputOptional,
|
|
)
|
|
|
|
import discord
|
|
from discord.ext import commands as dpy_commands
|
|
from discord.ext.commands import BadArgument
|
|
|
|
from ..i18n import Translator
|
|
from ..utils.chat_formatting import humanize_timedelta, humanize_list
|
|
|
|
if TYPE_CHECKING:
|
|
from .context import Context
|
|
|
|
__all__ = [
|
|
"RawUserIdConverter",
|
|
"DictConverter",
|
|
"UserInputOptional",
|
|
"NoParseOptional",
|
|
"RelativedeltaConverter",
|
|
"TimedeltaConverter",
|
|
"get_dict_converter",
|
|
"get_timedelta_converter",
|
|
"parse_relativedelta",
|
|
"parse_timedelta",
|
|
"CommandConverter",
|
|
"CogConverter",
|
|
]
|
|
|
|
_ = Translator("commands.converter", __file__)
|
|
|
|
ID_REGEX = re.compile(r"([0-9]{15,20})")
|
|
USER_MENTION_REGEX = re.compile(r"<@!?([0-9]{15,21})>$")
|
|
|
|
|
|
# Taken with permission from
|
|
# https://github.com/mikeshardmind/SinbadCogs/blob/816f3bc2ba860243f75112904b82009a8a9e1f99/scheduler/time_utils.py#L9-L19
|
|
TIME_RE_STRING = r"\s?".join(
|
|
[
|
|
r"((?P<years>\d+?)\s?(years?|y))?",
|
|
r"((?P<months>\d+?)\s?(months?|mo))?",
|
|
r"((?P<weeks>\d+?)\s?(weeks?|w))?",
|
|
r"((?P<days>\d+?)\s?(days?|d))?",
|
|
r"((?P<hours>\d+?)\s?(hours?|hrs|hr?))?",
|
|
r"((?P<minutes>\d+?)\s?(minutes?|mins?|m(?!o)))?", # prevent matching "months"
|
|
r"((?P<seconds>\d+?)\s?(seconds?|secs?|s))?",
|
|
]
|
|
)
|
|
|
|
TIME_RE = re.compile(TIME_RE_STRING, re.I)
|
|
|
|
|
|
def _parse_and_match(string_to_match: str, allowed_units: List[str]) -> Optional[Dict[str, int]]:
|
|
"""
|
|
Local utility function to match TIME_RE string above to user input for both parse_timedelta and parse_relativedelta
|
|
"""
|
|
matches = TIME_RE.fullmatch(string_to_match)
|
|
if matches:
|
|
params = {k: int(v) for k, v in matches.groupdict().items() if v is not None}
|
|
for k in params.keys():
|
|
if k not in allowed_units:
|
|
raise BadArgument(
|
|
_("`{unit}` is not a valid unit of time for this command").format(unit=k)
|
|
)
|
|
return params
|
|
return None
|
|
|
|
|
|
def parse_timedelta(
|
|
argument: str,
|
|
*,
|
|
maximum: Optional[timedelta] = None,
|
|
minimum: Optional[timedelta] = None,
|
|
allowed_units: Optional[List[str]] = None,
|
|
) -> Optional[timedelta]:
|
|
"""
|
|
This converts a user provided string into a timedelta
|
|
|
|
The units should be in order from largest to smallest.
|
|
This works with or without whitespace.
|
|
|
|
Parameters
|
|
----------
|
|
argument : str
|
|
The user provided input
|
|
maximum : Optional[datetime.timedelta]
|
|
If provided, any parsed value higher than this will raise an exception
|
|
minimum : Optional[datetime.timedelta]
|
|
If provided, any parsed value lower than this will raise an exception
|
|
allowed_units : Optional[List[str]]
|
|
If provided, you can constrain a user to expressing the amount of time
|
|
in specific units. The units you can chose to provide are the same as the
|
|
parser understands. (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
|
|
|
Returns
|
|
-------
|
|
Optional[datetime.timedelta]
|
|
If matched, the timedelta which was parsed. This can return `None`
|
|
|
|
Raises
|
|
------
|
|
BadArgument
|
|
If the argument passed uses a unit not allowed, but understood
|
|
or if the value is out of bounds.
|
|
"""
|
|
allowed_units = allowed_units or [
|
|
"weeks",
|
|
"days",
|
|
"hours",
|
|
"minutes",
|
|
"seconds",
|
|
]
|
|
params = _parse_and_match(argument, allowed_units)
|
|
if params:
|
|
try:
|
|
delta = timedelta(**params)
|
|
except OverflowError:
|
|
raise BadArgument(
|
|
_("The time set is way too high, consider setting something reasonable.")
|
|
)
|
|
if maximum and maximum < delta:
|
|
raise BadArgument(
|
|
_(
|
|
"This amount of time is too large for this command. (Maximum: {maximum})"
|
|
).format(maximum=humanize_timedelta(timedelta=maximum))
|
|
)
|
|
if minimum and delta < minimum:
|
|
raise BadArgument(
|
|
_(
|
|
"This amount of time is too small for this command. (Minimum: {minimum})"
|
|
).format(minimum=humanize_timedelta(timedelta=minimum))
|
|
)
|
|
return delta
|
|
return None
|
|
|
|
|
|
def parse_relativedelta(
|
|
argument: str, *, allowed_units: Optional[List[str]] = None
|
|
) -> Optional[relativedelta]:
|
|
"""
|
|
This converts a user provided string into a datetime with offset from NOW
|
|
|
|
The units should be in order from largest to smallest.
|
|
This works with or without whitespace.
|
|
|
|
Parameters
|
|
----------
|
|
argument : str
|
|
The user provided input
|
|
allowed_units : Optional[List[str]]
|
|
If provided, you can constrain a user to expressing the amount of time
|
|
in specific units. The units you can chose to provide are the same as the
|
|
parser understands. (``years``, ``months``, ``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
|
|
|
Returns
|
|
-------
|
|
Optional[dateutil.relativedelta.relativedelta]
|
|
If matched, the relativedelta which was parsed. This can return `None`
|
|
|
|
Raises
|
|
------
|
|
BadArgument
|
|
If the argument passed uses a unit not allowed, but understood
|
|
or if the value is out of bounds.
|
|
"""
|
|
allowed_units = allowed_units or [
|
|
"years",
|
|
"months",
|
|
"weeks",
|
|
"days",
|
|
"hours",
|
|
"minutes",
|
|
"seconds",
|
|
]
|
|
params = _parse_and_match(argument, allowed_units)
|
|
if params:
|
|
try:
|
|
delta = relativedelta(**params)
|
|
except OverflowError:
|
|
raise BadArgument(
|
|
_("The time set is way too high, consider setting something reasonable.")
|
|
)
|
|
return delta
|
|
return None
|
|
|
|
|
|
class RawUserIdConverter(dpy_commands.Converter):
|
|
"""
|
|
Converts ID or user mention to an `int`.
|
|
|
|
Useful for commands like ``[p]ban`` or ``[p]unban`` where the bot is not necessarily
|
|
going to share any servers with the user that a moderator wants to ban/unban.
|
|
|
|
This converter doesn't check if the ID/mention points to an actual user
|
|
but it won't match IDs and mentions that couldn't possibly be valid.
|
|
|
|
For example, the converter will not match on "123" because the number doesn't have
|
|
enough digits to be valid ID but, it will match on "12345678901234567" even though
|
|
there is no user with such ID.
|
|
"""
|
|
|
|
async def convert(self, ctx: "Context", argument: str) -> int:
|
|
# This is for the hackban and unban commands, where we receive IDs that
|
|
# are most likely not in the guild.
|
|
# Mentions are supported, but most likely won't ever be in cache.
|
|
|
|
if match := ID_REGEX.match(argument) or USER_MENTION_REGEX.match(argument):
|
|
return int(match.group(1))
|
|
|
|
raise BadArgument(_("'{input}' doesn't look like a valid user ID.").format(input=argument))
|
|
|
|
|
|
# Below this line are a lot of lies for mypy about things that *end up* correct when
|
|
# These are used for command conversion purposes. Please refer to the portion
|
|
# which is *not* for type checking for the actual implementation
|
|
# and ensure the lies stay correct for how the object should look as a typehint
|
|
|
|
if TYPE_CHECKING:
|
|
DictConverter = Dict[str, str]
|
|
else:
|
|
|
|
class DictConverter(dpy_commands.Converter):
|
|
"""
|
|
Converts pairs of space separated values to a dict
|
|
"""
|
|
|
|
def __init__(self, *expected_keys: str, delims: Optional[List[str]] = None):
|
|
self.expected_keys = expected_keys
|
|
self.delims = delims or [" "]
|
|
self.pattern = re.compile(r"|".join(re.escape(d) for d in self.delims))
|
|
|
|
async def convert(self, ctx: "Context", argument: str) -> Dict[str, str]:
|
|
ret: Dict[str, str] = {}
|
|
args = self.pattern.split(argument)
|
|
|
|
if len(args) % 2 != 0:
|
|
raise BadArgument()
|
|
|
|
iterator = iter(args)
|
|
|
|
for key in iterator:
|
|
if self.expected_keys and key not in self.expected_keys:
|
|
raise BadArgument(_("Unexpected key {key}").format(key=key))
|
|
|
|
ret[key] = next(iterator)
|
|
|
|
return ret
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
|
|
...
|
|
|
|
else:
|
|
|
|
def get_dict_converter(*expected_keys: str, delims: Optional[List[str]] = None) -> Type[dict]:
|
|
"""
|
|
Returns a typechecking safe `DictConverter` suitable for use with discord.py
|
|
"""
|
|
|
|
class PartialMeta(type(DictConverter)):
|
|
__call__ = functools.partialmethod(
|
|
type(DictConverter).__call__, *expected_keys, delims=delims
|
|
)
|
|
|
|
class ValidatedConverter(DictConverter, metaclass=PartialMeta):
|
|
pass
|
|
|
|
return ValidatedConverter
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
TimedeltaConverter = timedelta
|
|
else:
|
|
|
|
class TimedeltaConverter(dpy_commands.Converter):
|
|
"""
|
|
This is a converter for timedeltas.
|
|
The units should be in order from largest to smallest.
|
|
This works with or without whitespace.
|
|
|
|
See `parse_timedelta` for more information about how this functions.
|
|
|
|
Attributes
|
|
----------
|
|
maximum : Optional[datetime.timedelta]
|
|
If provided, any parsed value higher than this will raise an exception
|
|
minimum : Optional[datetime.timedelta]
|
|
If provided, any parsed value lower than this will raise an exception
|
|
allowed_units : Optional[List[str]]
|
|
If provided, you can constrain a user to expressing the amount of time
|
|
in specific units. The units you can choose to provide are the same as the
|
|
parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
|
default_unit : Optional[str]
|
|
If provided, it will additionally try to match integer-only input into
|
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
|
apply.
|
|
"""
|
|
|
|
def __init__(self, *, minimum=None, maximum=None, allowed_units=None, default_unit=None):
|
|
self.allowed_units = allowed_units
|
|
self.default_unit = default_unit
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
|
|
async def convert(self, ctx: "Context", argument: str) -> timedelta:
|
|
if self.default_unit and argument.isdecimal():
|
|
argument = argument + self.default_unit
|
|
|
|
delta = parse_timedelta(
|
|
argument,
|
|
minimum=self.minimum,
|
|
maximum=self.maximum,
|
|
allowed_units=self.allowed_units,
|
|
)
|
|
|
|
if delta is not None:
|
|
return delta
|
|
raise BadArgument() # This allows this to be a required argument.
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
def get_timedelta_converter(
|
|
*,
|
|
default_unit: Optional[str] = None,
|
|
maximum: Optional[timedelta] = None,
|
|
minimum: Optional[timedelta] = None,
|
|
allowed_units: Optional[List[str]] = None,
|
|
) -> Type[timedelta]:
|
|
...
|
|
|
|
else:
|
|
|
|
def get_timedelta_converter(
|
|
*,
|
|
default_unit: Optional[str] = None,
|
|
maximum: Optional[timedelta] = None,
|
|
minimum: Optional[timedelta] = None,
|
|
allowed_units: Optional[List[str]] = None,
|
|
) -> Type[timedelta]:
|
|
"""
|
|
This creates a type suitable for typechecking which works with discord.py's
|
|
commands.
|
|
|
|
See `parse_timedelta` for more information about how this functions.
|
|
|
|
Parameters
|
|
----------
|
|
maximum : Optional[datetime.timedelta]
|
|
If provided, any parsed value higher than this will raise an exception
|
|
minimum : Optional[datetime.timedelta]
|
|
If provided, any parsed value lower than this will raise an exception
|
|
allowed_units : Optional[List[str]]
|
|
If provided, you can constrain a user to expressing the amount of time
|
|
in specific units. The units you can choose to provide are the same as the
|
|
parser understands: (``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
|
default_unit : Optional[str]
|
|
If provided, it will additionally try to match integer-only input into
|
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
|
apply.
|
|
|
|
Returns
|
|
-------
|
|
type
|
|
The converter class, which will be a subclass of `TimedeltaConverter`
|
|
"""
|
|
|
|
class PartialMeta(type(DictConverter)):
|
|
__call__ = functools.partialmethod(
|
|
type(DictConverter).__call__,
|
|
allowed_units=allowed_units,
|
|
default_unit=default_unit,
|
|
minimum=minimum,
|
|
maximum=maximum,
|
|
)
|
|
|
|
class ValidatedConverter(TimedeltaConverter, metaclass=PartialMeta):
|
|
pass
|
|
|
|
return ValidatedConverter
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
RelativedeltaConverter = relativedelta
|
|
else:
|
|
|
|
class RelativedeltaConverter(dpy_commands.Converter):
|
|
"""
|
|
This is a converter for relative deltas.
|
|
|
|
The units should be in order from largest to smallest.
|
|
This works with or without whitespace.
|
|
|
|
See `parse_relativedelta` for more information about how this functions.
|
|
|
|
Attributes
|
|
----------
|
|
allowed_units : Optional[List[str]]
|
|
If provided, you can constrain a user to expressing the amount of time
|
|
in specific units. The units you can choose to provide are the same as the
|
|
parser understands: (``years``, ``months``, ``weeks``, ``days``, ``hours``, ``minutes``, ``seconds``)
|
|
default_unit : Optional[str]
|
|
If provided, it will additionally try to match integer-only input into
|
|
a timedelta, using the unit specified. Same units as in ``allowed_units``
|
|
apply.
|
|
"""
|
|
|
|
def __init__(self, *, allowed_units=None, default_unit=None):
|
|
self.allowed_units = allowed_units
|
|
self.default_unit = default_unit
|
|
|
|
async def convert(self, ctx: "Context", argument: str) -> relativedelta:
|
|
if self.default_unit and argument.isdecimal():
|
|
argument = argument + self.default_unit
|
|
|
|
delta = parse_relativedelta(argument, allowed_units=self.allowed_units)
|
|
|
|
if delta is not None:
|
|
return delta
|
|
raise BadArgument() # This allows this to be a required argument.
|
|
|
|
|
|
if not TYPE_CHECKING:
|
|
|
|
class NoParseOptional:
|
|
"""
|
|
This can be used instead of `typing.Optional`
|
|
to avoid discord.py special casing the conversion behavior.
|
|
|
|
.. seealso::
|
|
The `ignore_optional_for_conversion` option of commands.
|
|
"""
|
|
|
|
def __class_getitem__(cls, key):
|
|
if isinstance(key, tuple):
|
|
raise TypeError("Must only provide a single type to Optional")
|
|
return key
|
|
|
|
|
|
_T = TypeVar("_T")
|
|
|
|
if not TYPE_CHECKING:
|
|
#: This can be used when user input should be converted as discord.py
|
|
#: treats `typing.Optional`, but the type should not be equivalent to
|
|
#: ``typing.Union[DesiredType, None]`` for type checking.
|
|
#:
|
|
#: Note: In type checking context, this type hint can be passed
|
|
#: multiple types, but such usage is not supported and will fail at runtime
|
|
#:
|
|
#: .. warning::
|
|
#: This converter class is still provisional.
|
|
UserInputOptional = Optional
|
|
|
|
if TYPE_CHECKING:
|
|
CommandConverter = dpy_commands.Command
|
|
CogConverter = dpy_commands.Cog
|
|
else:
|
|
|
|
class CommandConverter(dpy_commands.Converter):
|
|
"""Converts a command name to the matching `redbot.core.commands.Command` object."""
|
|
|
|
async def convert(self, ctx: "Context", argument: str):
|
|
arg = argument.strip()
|
|
command = ctx.bot.get_command(arg)
|
|
if not command:
|
|
raise BadArgument(_('Command "{arg}" not found.').format(arg=arg))
|
|
return command
|
|
|
|
class CogConverter(dpy_commands.Converter):
|
|
"""Converts a cog name to the matching `redbot.core.commands.Cog` object."""
|
|
|
|
async def convert(self, ctx: "Context", argument: str):
|
|
arg = argument.strip()
|
|
cog = ctx.bot.get_cog(arg)
|
|
if not cog:
|
|
raise BadArgument(_('Cog "{arg}" not found.').format(arg=arg))
|
|
return cog
|