stormbrigade_sheriff/sbsheriff/Lib/site-packages/disnake/ext/commands/converter.py

1323 lines
43 KiB
Python

# SPDX-License-Identifier: MIT
from __future__ import annotations
import functools
import inspect
import re
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
Iterable,
List,
Literal,
Optional,
Protocol,
Tuple,
Type,
TypeVar,
Union,
runtime_checkable,
)
import disnake
from .context import AnyContext, Context
from .errors import (
BadArgument,
BadBoolArgument,
BadColourArgument,
BadInviteArgument,
BadLiteralArgument,
BadUnionArgument,
ChannelNotFound,
ChannelNotReadable,
CommandError,
ConversionError,
EmojiNotFound,
GuildNotFound,
GuildScheduledEventNotFound,
GuildStickerNotFound,
MemberNotFound,
MessageNotFound,
NoPrivateMessage,
ObjectNotFound,
PartialEmojiConversionFailure,
RoleNotFound,
ThreadNotFound,
UserNotFound,
)
if TYPE_CHECKING:
from disnake.abc import MessageableChannel
# TODO: USE ACTUAL FUNCTIONS INSTEAD OF USELESS CLASSES
__all__ = (
"Converter",
"IDConverter",
"ObjectConverter",
"MemberConverter",
"UserConverter",
"PartialMessageConverter",
"MessageConverter",
"GuildChannelConverter",
"TextChannelConverter",
"VoiceChannelConverter",
"StageChannelConverter",
"CategoryChannelConverter",
"ForumChannelConverter",
"ThreadConverter",
"ColourConverter",
"ColorConverter",
"RoleConverter",
"GameConverter",
"InviteConverter",
"GuildConverter",
"EmojiConverter",
"PartialEmojiConverter",
"GuildStickerConverter",
"PermissionsConverter",
"GuildScheduledEventConverter",
"clean_content",
"Greedy",
"run_converters",
)
_utils_get = disnake.utils.get
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
CT = TypeVar("CT", bound=disnake.abc.GuildChannel)
TT = TypeVar("TT", bound=disnake.Thread)
def _get_from_guilds(
client: disnake.Client, func: Callable[[disnake.Guild], Optional[T]]
) -> Optional[T]:
for guild in client.guilds:
if result := func(guild):
return result
return None
@runtime_checkable
class Converter(Protocol[T_co]):
"""The base class of custom converters that require the :class:`.Context`
or :class:`.ApplicationCommandInteraction` to be passed to be useful.
This allows you to implement converters that function similar to the
special cased ``disnake`` classes.
Classes that derive from this should override the :meth:`~.Converter.convert`
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
"""
async def convert(self, ctx: AnyContext, argument: str) -> T_co:
"""|coro|
The method to override to do conversion logic.
If an error is found while converting, it is recommended to
raise a :exc:`.CommandError` derived exception as it will
properly propagate to the error handlers.
Parameters
----------
ctx: Union[:class:`.Context`, :class:`.ApplicationCommandInteraction`]
The invocation context that the argument is being used in.
argument: :class:`str`
The argument that is being converted.
Raises
------
CommandError
A generic exception occurred when converting the argument.
BadArgument
The converter failed to convert the argument.
"""
raise NotImplementedError("Derived classes need to implement this.")
_ID_REGEX = re.compile(r"([0-9]{17,19})$")
class IDConverter(Converter[T_co]):
@staticmethod
def _get_id_match(argument: str) -> Optional[re.Match[str]]:
return _ID_REGEX.match(argument)
class ObjectConverter(IDConverter[disnake.Object]):
"""Converts to a :class:`~disnake.Object`.
The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`).
.. versionadded:: 2.0
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by member, role, or channel mention.
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Object:
match = self._get_id_match(argument) or re.match(
r"<(?:@(?:!|&)?|#)([0-9]{17,19})>$", argument
)
if match is None:
raise ObjectNotFound(argument)
result = int(match.group(1))
return disnake.Object(id=result)
class MemberConverter(IDConverter[disnake.Member]):
"""Converts to a :class:`~disnake.Member`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID
2. Lookup by mention
3. Lookup by username#discrim
4. Lookup by username#0
5. Lookup by nickname
6. Lookup by global name
7. Lookup by username
The name resolution order matches the one used by :meth:`.Guild.get_member_named`.
.. versionchanged:: 1.5
Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`
.. versionchanged:: 1.5.1
This converter now lazily fetches members from the gateway and HTTP APIs,
optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled.
.. versionchanged:: 2.9
Name resolution order changed from ``username > nick`` to
``nick > global_name > username`` to account for the username migration.
"""
async def query_member_named(
self, guild: disnake.Guild, argument: str
) -> Optional[disnake.Member]:
cache = guild._state.member_cache_flags.joined
username, _, discriminator = argument.rpartition("#")
if username and (
discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal())
):
# legacy behavior
members = await guild.query_members(username, limit=100, cache=cache)
return _utils_get(members, name=username, discriminator=discriminator)
else:
members = await guild.query_members(argument, limit=100, cache=cache)
return disnake.utils.find(
lambda m: m.nick == argument or m.global_name == argument or m.name == argument,
members,
)
async def query_member_by_id(
self, bot: disnake.Client, guild: disnake.Guild, user_id: int
) -> Optional[disnake.Member]:
ws = bot._get_websocket(shard_id=guild.shard_id)
cache = guild._state.member_cache_flags.joined
if ws.is_ratelimited():
# If we're being rate limited on the WS, then fall back to using the HTTP API
# So we don't have to wait ~60 seconds for the query to finish
try:
member = await guild.fetch_member(user_id)
except disnake.HTTPException:
return None
if cache:
guild._add_member(member)
return member
# If we're not being rate limited then we can use the websocket to actually query
members = await guild.query_members(limit=1, user_ids=[user_id], cache=cache)
if not members:
return None
return members[0]
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Member:
bot: disnake.Client = ctx.bot
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{17,19})>$", argument)
guild = ctx.guild
result: Optional[disnake.Member] = None
user_id: Optional[int] = None
if match is None:
# not a mention...
if guild:
result = guild.get_member_named(argument)
else:
result = _get_from_guilds(bot, lambda g: g.get_member_named(argument))
else:
user_id = int(match.group(1))
if guild:
mentions: Iterable[disnake.Member]
if isinstance(ctx, Context):
mentions = (
user for user in ctx.message.mentions if isinstance(user, disnake.Member)
)
else:
mentions = []
result = guild.get_member(user_id) or _utils_get(mentions, id=user_id)
else:
result = _get_from_guilds(bot, lambda g: g.get_member(user_id))
if result is None:
if guild is None:
raise MemberNotFound(argument)
if user_id is not None:
result = await self.query_member_by_id(bot, guild, user_id)
else:
result = await self.query_member_named(guild, argument)
if not result:
raise MemberNotFound(argument)
return result
class UserConverter(IDConverter[disnake.User]):
"""Converts to a :class:`~disnake.User`.
All lookups are via the global user cache.
The lookup strategy is as follows (in order):
1. Lookup by ID
2. Lookup by mention
3. Lookup by username#discrim
4. Lookup by username#0
5. Lookup by global name
6. Lookup by username
.. versionchanged:: 1.5
Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`
.. versionchanged:: 1.6
This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache.
.. versionchanged:: 2.9
Now takes :attr:`~disnake.User.global_name` into account.
No longer automatically removes ``"@"`` prefix from arguments.
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:
match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{17,19})>$", argument)
state = ctx._state
bot: disnake.Client = ctx.bot
result: Optional[Union[disnake.User, disnake.Member]] = None
if match is not None:
user_id = int(match.group(1))
mentions: Iterable[Union[disnake.User, disnake.Member]]
if isinstance(ctx, Context):
mentions = ctx.message.mentions
else:
mentions = []
result = bot.get_user(user_id) or _utils_get(mentions, id=user_id)
if result is None:
try:
result = await bot.fetch_user(user_id)
except disnake.HTTPException:
raise UserNotFound(argument) from None
if isinstance(result, disnake.Member):
return result._user
return result
username, _, discriminator = argument.rpartition("#")
# n.b. there's no builtin method that only matches arabic digits, `isdecimal` is the closest one.
# it really doesn't matter much, worst case is unnecessary computations
if username and (
discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal())
):
# legacy behavior
result = _utils_get(state._users.values(), name=username, discriminator=discriminator)
if result is not None:
return result
result = disnake.utils.find(
lambda u: u.global_name == argument or u.name == argument,
state._users.values(),
)
if result is None:
raise UserNotFound(argument)
return result
class PartialMessageConverter(Converter[disnake.PartialMessage]):
"""Converts to a :class:`~disnake.PartialMessage`.
.. versionadded:: 1.7
The creation strategy is as follows (in order):
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
2. By message ID (The message is assumed to be in the context channel.)
3. By message URL
"""
@staticmethod
def _get_id_matches(ctx: AnyContext, argument: str) -> Tuple[Optional[int], int, int]:
id_regex = re.compile(r"(?:(?P<channel_id>[0-9]{17,19})-)?(?P<message_id>[0-9]{17,19})$")
link_regex = re.compile(
r"https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/"
r"(?P<guild_id>[0-9]{17,19}|@me)"
r"/(?P<channel_id>[0-9]{17,19})/(?P<message_id>[0-9]{17,19})/?$"
)
match = id_regex.match(argument) or link_regex.match(argument)
if not match:
raise MessageNotFound(argument)
data = match.groupdict()
channel_id = disnake.utils._get_as_snowflake(data, "channel_id") or ctx.channel.id
message_id = int(data["message_id"])
guild_id_str: Optional[str] = data.get("guild_id")
if guild_id_str is None:
guild_id = ctx.guild and ctx.guild.id
elif guild_id_str == "@me":
guild_id = None
else:
guild_id = int(guild_id_str)
return guild_id, message_id, channel_id
@staticmethod
def _resolve_channel(
ctx: AnyContext, guild_id: Optional[int], channel_id: int
) -> Optional[MessageableChannel]:
bot: disnake.Client = ctx.bot
if guild_id is None:
return bot.get_channel(channel_id) if channel_id else ctx.channel # type: ignore
guild = bot.get_guild(guild_id)
if guild is not None:
return guild._resolve_channel(channel_id) # type: ignore
return None
async def convert(self, ctx: AnyContext, argument: str) -> disnake.PartialMessage:
guild_id, message_id, channel_id = self._get_id_matches(ctx, argument)
channel = self._resolve_channel(ctx, guild_id, channel_id)
if not channel:
raise ChannelNotFound(str(channel_id))
return disnake.PartialMessage(channel=channel, id=message_id)
class MessageConverter(IDConverter[disnake.Message]):
"""Converts to a :class:`~disnake.Message`.
.. versionadded:: 1.1
The lookup strategy is as follows (in order):
1. Lookup by "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
2. Lookup by message ID (the message **must** be in the context channel)
3. Lookup by message URL
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Message:
guild_id, message_id, channel_id = PartialMessageConverter._get_id_matches(ctx, argument)
bot: disnake.Client = ctx.bot
message = bot._connection._get_message(message_id)
if message:
return message
channel = PartialMessageConverter._resolve_channel(ctx, guild_id, channel_id)
if not channel:
raise ChannelNotFound(str(channel_id))
try:
return await channel.fetch_message(message_id)
except disnake.NotFound:
raise MessageNotFound(argument) from None
except disnake.Forbidden:
raise ChannelNotReadable(channel) from None # type: ignore
class GuildChannelConverter(IDConverter[disnake.abc.GuildChannel]):
"""Converts to a :class:`.abc.GuildChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name.
.. versionadded:: 2.0
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.abc.GuildChannel:
return self._resolve_channel(ctx, argument, "channels", disnake.abc.GuildChannel)
@staticmethod
def _resolve_channel(ctx: AnyContext, argument: str, attribute: str, type: Type[CT]) -> CT:
bot: disnake.Client = ctx.bot
match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{17,19})>$", argument)
result: Optional[disnake.abc.GuildChannel] = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
iterable: Iterable[CT] = getattr(guild, attribute)
result = _utils_get(iterable, name=argument)
else:
result = disnake.utils.find(
lambda c: isinstance(c, type) and c.name == argument, bot.get_all_channels()
)
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, lambda g: g.get_channel(channel_id))
if not isinstance(result, type):
raise ChannelNotFound(argument)
return result
@staticmethod
def _resolve_thread(ctx: AnyContext, argument: str, attribute: str, type: Type[TT]) -> TT:
match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{17,19})>$", argument)
result: Optional[disnake.Thread] = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
iterable: Iterable[TT] = getattr(guild, attribute)
result = _utils_get(iterable, name=argument)
else:
thread_id = int(match.group(1))
if guild:
result = guild.get_thread(thread_id)
if not isinstance(result, type):
raise ThreadNotFound(argument)
return result
class TextChannelConverter(IDConverter[disnake.TextChannel]):
"""Converts to a :class:`~disnake.TextChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.TextChannel:
return GuildChannelConverter._resolve_channel(
ctx, argument, "text_channels", disnake.TextChannel
)
class VoiceChannelConverter(IDConverter[disnake.VoiceChannel]):
"""Converts to a :class:`~disnake.VoiceChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.VoiceChannel:
return GuildChannelConverter._resolve_channel(
ctx, argument, "voice_channels", disnake.VoiceChannel
)
class StageChannelConverter(IDConverter[disnake.StageChannel]):
"""Converts to a :class:`~disnake.StageChannel`.
.. versionadded:: 1.7
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.StageChannel:
return GuildChannelConverter._resolve_channel(
ctx, argument, "stage_channels", disnake.StageChannel
)
class CategoryChannelConverter(IDConverter[disnake.CategoryChannel]):
"""Converts to a :class:`~disnake.CategoryChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.CategoryChannel:
return GuildChannelConverter._resolve_channel(
ctx, argument, "categories", disnake.CategoryChannel
)
class ForumChannelConverter(IDConverter[disnake.ForumChannel]):
"""Converts to a :class:`~disnake.ForumChannel`.
.. versionadded:: 2.5
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.ForumChannel:
return GuildChannelConverter._resolve_channel(
ctx, argument, "forum_channels", disnake.ForumChannel
)
class ThreadConverter(IDConverter[disnake.Thread]):
"""Coverts to a :class:`~disnake.Thread`.
All lookups are via the local guild.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name.
.. versionadded:: 2.0
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Thread:
return GuildChannelConverter._resolve_thread(ctx, argument, "threads", disnake.Thread)
class ColourConverter(Converter[disnake.Colour]):
"""Converts to a :class:`~disnake.Colour`.
.. versionchanged:: 1.5
Add an alias named ColorConverter
The following formats are accepted:
- ``0x<hex>``
- ``#<hex>``
- ``0x#<hex>``
- ``rgb(<number>, <number>, <number>)``
- Any of the ``classmethod`` in :class:`~disnake.Colour`
- The ``_`` in the name can be optionally replaced with spaces.
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).
.. versionchanged:: 1.5
Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument`
.. versionchanged:: 1.7
Added support for ``rgb`` function and 3-digit hex shortcuts
"""
RGB_REGEX = re.compile(
r"rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)"
)
def parse_hex_number(self, argument: str) -> disnake.Color:
arg = "".join(i * 2 for i in argument) if len(argument) == 3 else argument
try:
value = int(arg, base=16)
if not (0 <= value <= 0xFFFFFF):
raise BadColourArgument(argument)
except ValueError:
raise BadColourArgument(argument) from None
else:
return disnake.Color(value=value)
def parse_rgb_number(self, argument: str, number: str) -> int:
if number[-1] == "%":
value = int(number[:-1])
if not (0 <= value <= 100):
raise BadColourArgument(argument)
return round(255 * (value / 100))
value = int(number)
if not (0 <= value <= 255):
raise BadColourArgument(argument)
return value
def parse_rgb(self, argument: str, *, regex: re.Pattern[str] = RGB_REGEX) -> disnake.Color:
match = regex.match(argument)
if match is None:
raise BadColourArgument(argument)
red = self.parse_rgb_number(argument, match.group("r"))
green = self.parse_rgb_number(argument, match.group("g"))
blue = self.parse_rgb_number(argument, match.group("b"))
return disnake.Color.from_rgb(red, green, blue)
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Color:
if argument[0] == "#":
return self.parse_hex_number(argument[1:])
if argument[0:2] == "0x":
rest = argument[2:]
# Legacy backwards compatible syntax
if rest.startswith("#"):
return self.parse_hex_number(rest[1:])
return self.parse_hex_number(rest)
arg = argument.lower()
if arg[0:3] == "rgb":
return self.parse_rgb(arg)
arg = arg.replace(" ", "_")
method = getattr(disnake.Colour, arg, None)
if arg.startswith("from_") or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
ColorConverter = ColourConverter
class RoleConverter(IDConverter[disnake.Role]):
"""Converts to a :class:`~disnake.Role`.
All lookups are via the local guild. If in a DM context, the converter raises
:exc:`.NoPrivateMessage` exception.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
.. versionchanged:: 1.5
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Role:
guild = ctx.guild
if not guild:
raise NoPrivateMessage
match = self._get_id_match(argument) or re.match(r"<@&([0-9]{17,19})>$", argument)
if match:
result = guild.get_role(int(match.group(1)))
else:
result = _utils_get(guild._roles.values(), name=argument)
if result is None:
raise RoleNotFound(argument)
return result
class GameConverter(Converter[disnake.Game]):
"""Converts to :class:`~disnake.Game`."""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Game:
return disnake.Game(name=argument)
class InviteConverter(Converter[disnake.Invite]):
"""Converts to a :class:`~disnake.Invite`.
This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
.. versionchanged:: 1.5
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Invite:
try:
return await ctx.bot.fetch_invite(argument)
except Exception as exc:
raise BadInviteArgument(argument) from exc
class GuildConverter(IDConverter[disnake.Guild]):
"""Converts to a :class:`~disnake.Guild`.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
.. versionadded:: 1.7
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Guild:
match = self._get_id_match(argument)
bot: disnake.Client = ctx.bot
result: Optional[disnake.Guild] = None
if match is not None:
guild_id = int(match.group(1))
result = bot.get_guild(guild_id)
if result is None:
result = _utils_get(bot.guilds, name=argument)
if result is None:
raise GuildNotFound(argument)
return result
class EmojiConverter(IDConverter[disnake.Emoji]):
"""Converts to a :class:`~disnake.Emoji`.
All lookups are done for the local guild first, if available. If that lookup
fails, then it checks the client's global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by extracting ID from the emoji.
3. Lookup by name
.. versionchanged:: 1.5
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Emoji:
match = self._get_id_match(argument) or re.match(
r"<a?:[a-zA-Z0-9\_]{1,32}:([0-9]{17,19})>$", argument
)
result: Optional[disnake.Emoji] = None
bot = ctx.bot
guild = ctx.guild
if match is None:
# Try to get the emoji by name. Try local guild first.
if guild:
result = _utils_get(guild.emojis, name=argument)
if result is None:
result = _utils_get(bot.emojis, name=argument)
else:
# Try to look up emoji by id.
result = bot.get_emoji(int(match.group(1)))
if result is None:
raise EmojiNotFound(argument)
return result
class PartialEmojiConverter(Converter[disnake.PartialEmoji]):
"""Converts to a :class:`~disnake.PartialEmoji`.
This is done by extracting the animated flag, name and ID from the emoji.
.. versionchanged:: 1.5
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.PartialEmoji:
match = re.match(r"<(a?):([a-zA-Z0-9\_]{1,32}):([0-9]{17,19})>$", argument)
if match:
emoji_animated = bool(match.group(1))
emoji_name: str = match.group(2)
emoji_id = int(match.group(3))
return disnake.PartialEmoji.with_state(
ctx.bot._connection, animated=emoji_animated, name=emoji_name, id=emoji_id
)
raise PartialEmojiConversionFailure(argument)
class GuildStickerConverter(IDConverter[disnake.GuildSticker]):
"""Converts to a :class:`~disnake.GuildSticker`.
All lookups are done for the local guild first, if available. If that lookup
fails, then it checks the client's global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID
2. Lookup by name
.. versionadded:: 2.0
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.GuildSticker:
match = self._get_id_match(argument)
result = None
bot: disnake.Client = ctx.bot
guild = ctx.guild
if match is None:
# Try to get the sticker by name. Try local guild first.
if guild:
result = _utils_get(guild.stickers, name=argument)
if result is None:
result = _utils_get(bot.stickers, name=argument)
else:
# Try to look up sticker by id.
result = bot.get_sticker(int(match.group(1)))
if result is None:
raise GuildStickerNotFound(argument)
return result
class PermissionsConverter(Converter[disnake.Permissions]):
"""Converts to a :class:`~disnake.Permissions`.
Accepts an integer or a string of space-separated permission names (or just a single one) as input.
.. versionadded:: 2.3
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.Permissions:
# try the permission bit value
try:
value = int(argument)
except ValueError:
pass
else:
return disnake.Permissions(value)
argument = argument.replace("server", "guild")
# try multiple attributes, then a single one
perms: List[disnake.Permissions] = []
for name in argument.split():
attr = getattr(disnake.Permissions, name, None)
if attr is None:
break
if callable(attr):
perms.append(attr())
else:
perms.append(disnake.Permissions(**{name: True}))
else:
return functools.reduce(lambda a, b: disnake.Permissions(a.value | b.value), perms)
name = argument.replace(" ", "_")
attr = getattr(disnake.Permissions, name, None)
if attr is None:
raise BadArgument(f"Invalid Permissions: {name!r}")
if callable(attr):
return attr()
else:
return disnake.Permissions(**{name: True})
class GuildScheduledEventConverter(IDConverter[disnake.GuildScheduledEvent]):
"""Converts to a :class:`~disnake.GuildScheduledEvent`.
The lookup strategy is as follows (in order):
1. Lookup by ID (in current guild)
2. Lookup as event URL
3. Lookup by name (in current guild; there is no disambiguation for scheduled events with multiple matching names)
.. versionadded:: 2.5
"""
async def convert(self, ctx: AnyContext, argument: str) -> disnake.GuildScheduledEvent:
event_regex = re.compile(
r"https?://(?:(?:ptb|canary|www)\.)?discord(?:app)?\.com/events/"
r"([0-9]{17,19})/([0-9]{17,19})/?$"
)
bot: disnake.Client = ctx.bot
result: Optional[disnake.GuildScheduledEvent] = None
guild = ctx.guild
# 1.
if guild and (match := self._get_id_match(argument)):
result = guild.get_scheduled_event(int(match.group(1)))
# 2.
if not result and (match := event_regex.match(argument)):
event_guild = bot.get_guild(int(match.group(1)))
if event_guild:
result = event_guild.get_scheduled_event(int(match.group(2)))
# 3.
if not result and guild:
result = _utils_get(guild.scheduled_events, name=argument)
if not result:
raise GuildScheduledEventNotFound(argument)
return result
class clean_content(Converter[str]):
"""Converts the argument to mention scrubbed version of
said content.
This behaves similarly to :attr:`~disnake.Message.clean_content`.
Attributes
----------
fix_channel_mentions: :class:`bool`
Whether to clean channel mentions.
use_nicknames: :class:`bool`
Whether to use :attr:`nicknames <.Member.nick>` and
:attr:`global names <.Member.global_name>` when transforming mentions.
escape_markdown: :class:`bool`
Whether to also escape special markdown characters.
remove_markdown: :class:`bool`
Whether to also remove special markdown characters. This option is not supported with ``escape_markdown``
.. versionadded:: 1.7
"""
def __init__(
self,
*,
fix_channel_mentions: bool = False,
use_nicknames: bool = True,
escape_markdown: bool = False,
remove_markdown: bool = False,
) -> None:
self.fix_channel_mentions = fix_channel_mentions
self.use_nicknames = use_nicknames
self.escape_markdown = escape_markdown
self.remove_markdown = remove_markdown
async def convert(self, ctx: AnyContext, argument: str) -> str:
msg = ctx.message if isinstance(ctx, Context) else None
bot: disnake.Client = ctx.bot
def resolve_user(id: int) -> str:
m = (
(msg and _utils_get(msg.mentions, id=id))
or (ctx.guild and ctx.guild.get_member(id))
or bot.get_user(id)
)
return f"@{m.display_name if self.use_nicknames else m.name}" if m else "@deleted-user"
def resolve_role(id: int) -> str:
if ctx.guild is None:
return "@deleted-role"
r = (msg and _utils_get(msg.role_mentions, id=id)) or ctx.guild.get_role(id)
return f"@{r.name}" if r else "@deleted-role"
def resolve_channel(id: int) -> str:
if ctx.guild and self.fix_channel_mentions:
c = ctx.guild.get_channel(id)
return f"#{c.name}" if c else "#deleted-channel"
return f"<#{id}>"
transforms: Dict[str, Callable[[int], str]] = {
"@": resolve_user,
"@!": resolve_user,
"#": resolve_channel,
"@&": resolve_role,
}
def repl(match: re.Match) -> str:
type = match[1]
id = int(match[2])
return transforms[type](id)
result = re.sub(r"<(@[!&]?|#)([0-9]{17,19})>", repl, argument)
if self.escape_markdown:
result = disnake.utils.escape_markdown(result)
elif self.remove_markdown:
result = disnake.utils.remove_markdown(result)
# Completely ensure no mentions escape:
return disnake.utils.escape_mentions(result)
class Greedy(List[T]):
"""A special converter that greedily consumes arguments until it can't.
As a consequence of this behaviour, most input errors are silently discarded,
since it is used as an indicator of when to stop parsing.
When a parser error is met the greedy converter stops converting, undoes the
internal string parsing routine, and continues parsing regularly.
For example, in the following code:
.. code-block:: python3
@commands.command()
async def test(ctx, numbers: Greedy[int], reason: str):
await ctx.send("numbers: {0}, reason: {1}".format(numbers, reason))
An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with
``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``.
For more information, check :ref:`ext_commands_special_converters`.
"""
__slots__ = ("converter",)
def __init__(self, *, converter: T) -> None:
self.converter = converter
def __repr__(self) -> str:
converter = getattr(self.converter, "__name__", repr(self.converter))
return f"Greedy[{converter}]"
def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]:
if not isinstance(params, tuple):
params = (params,)
if len(params) != 1:
raise TypeError("Greedy[...] only takes a single argument")
converter = params[0]
origin = getattr(converter, "__origin__", None)
args = getattr(converter, "__args__", ())
if not (callable(converter) or isinstance(converter, Converter) or origin is not None):
raise TypeError("Greedy[...] expects a type or a Converter instance.")
if converter in (str, type(None)) or origin is Greedy:
raise TypeError(f"Greedy[{converter.__name__}] is invalid.") # type: ignore
if origin is Union and type(None) in args:
raise TypeError(f"Greedy[{converter!r}] is invalid.")
return cls(converter=converter)
def _convert_to_bool(argument: str) -> bool:
lowered = argument.lower()
if lowered in ("yes", "y", "true", "t", "1", "enable", "on"):
return True
elif lowered in ("no", "n", "false", "f", "0", "disable", "off"):
return False
else:
raise BadBoolArgument(lowered)
def get_converter(param: inspect.Parameter) -> Any:
converter = param.annotation
if converter is param.empty:
if param.default is not param.empty:
converter = str if param.default is None else type(param.default)
else:
converter = str
return converter
_GenericAlias = type(List[T])
def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool:
return isinstance(tp, type) and issubclass(tp, Generic) or isinstance(tp, _GenericAlias)
CONVERTER_MAPPING: Dict[Type[Any], Type[Converter]] = {
disnake.Object: ObjectConverter,
disnake.Member: MemberConverter,
disnake.User: UserConverter,
disnake.Message: MessageConverter,
disnake.PartialMessage: PartialMessageConverter,
disnake.TextChannel: TextChannelConverter,
disnake.Invite: InviteConverter,
disnake.Guild: GuildConverter,
disnake.Role: RoleConverter,
disnake.Game: GameConverter,
disnake.Colour: ColourConverter,
disnake.VoiceChannel: VoiceChannelConverter,
disnake.StageChannel: StageChannelConverter,
disnake.Emoji: EmojiConverter,
disnake.PartialEmoji: PartialEmojiConverter,
disnake.CategoryChannel: CategoryChannelConverter,
disnake.ForumChannel: ForumChannelConverter,
disnake.Thread: ThreadConverter,
disnake.abc.GuildChannel: GuildChannelConverter,
disnake.GuildSticker: GuildStickerConverter,
disnake.Permissions: PermissionsConverter,
disnake.GuildScheduledEvent: GuildScheduledEventConverter,
}
async def _actual_conversion(
ctx: Context,
converter: Union[Type[T], Type[Converter[T]], Converter[T], Callable[[str], T]],
argument: str,
param: inspect.Parameter,
) -> T:
if converter is bool:
return _convert_to_bool(argument) # type: ignore
if isinstance(converter, type):
module = converter.__module__
if module.startswith("disnake.") and not module.endswith("converter"):
converter = CONVERTER_MAPPING.get(converter, converter)
try:
if isinstance(converter, type) and issubclass(converter, Converter):
if inspect.ismethod(converter.convert):
return await converter.convert(ctx, argument)
else:
return await converter().convert(ctx, argument)
elif isinstance(converter, Converter):
return await converter.convert(ctx, argument) # type: ignore
except CommandError:
raise
except Exception as exc:
raise ConversionError(converter, exc) from exc
try:
return converter(argument)
except CommandError:
raise
except Exception as exc:
try:
name = converter.__name__
except AttributeError:
name = converter.__class__.__name__
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
async def run_converters(ctx: Context, converter, argument: str, param: inspect.Parameter):
"""|coro|
Runs converters for a given converter, argument, and parameter.
This function does the same work that the library does under the hood.
.. versionadded:: 2.0
Parameters
----------
ctx: :class:`Context`
The invocation context to run the converters under.
converter: Any
The converter to run, this corresponds to the annotation in the function.
argument: :class:`str`
The argument to convert to.
param: :class:`inspect.Parameter`
The parameter being converted. This is mainly for error reporting.
Raises
------
CommandError
The converter failed to convert.
Returns
-------
Any
The resulting conversion.
"""
origin = getattr(converter, "__origin__", None)
if origin is Union:
errors = []
_NoneType = type(None)
union_args = converter.__args__
for conv in union_args:
# if we got to this part in the code, then the previous conversions have failed
# so we should just undo the view, return the default, and allow parsing to continue
# with the other parameters
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
ctx.view.undo()
return None if param.default is param.empty else param.default
try:
value = await run_converters(ctx, conv, argument, param)
except CommandError as exc:
errors.append(exc)
else:
return value
# if we're here, then we failed all the converters
raise BadUnionArgument(param, union_args, errors)
if origin is Literal:
errors = []
conversions = {}
literal_args = converter.__args__
for literal in literal_args:
literal_type = type(literal)
try:
value = conversions[literal_type]
except KeyError:
try:
value = await _actual_conversion(ctx, literal_type, argument, param)
except CommandError as exc:
errors.append(exc)
conversions[literal_type] = object()
continue
else:
conversions[literal_type] = value
if value == literal:
return value
# if we're here, then we failed to match all the literals
raise BadLiteralArgument(param, literal_args, errors)
# This must be the last if-clause in the chain of origin checking
# Nearly every type is a generic type within the typing library
# So care must be taken to make sure a more specialised origin handle
# isn't overwritten by the widest if clause
if origin is not None and is_generic_type(converter):
converter = origin
return await _actual_conversion(ctx, converter, argument, param)