On branch DiscordProfile

Initial commit
This commit is contained in:
EG
2026-07-01 15:15:07 +03:00
commit d4bf750c9e
3125 changed files with 601334 additions and 0 deletions
@@ -0,0 +1,29 @@
"""
The MIT License (MIT)
Copyright (c) 2015-2021 Rapptz
Copyright (c) 2021-present Pycord Development
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from .context import *
from .core import *
from .options import *
from .permissions import *
@@ -0,0 +1,476 @@
"""
The MIT License (MIT)
Copyright (c) 2015-2021 Rapptz
Copyright (c) 2021-present Pycord Development
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, TypeVar, overload
import discord.abc
from discord.interactions import Interaction, InteractionMessage, InteractionResponse
from discord.webhook.async_ import Webhook
if TYPE_CHECKING:
from typing import Awaitable, Callable
from typing_extensions import ParamSpec
import discord
from .. import AllowedMentions, Bot
from ..client import ClientUser
from ..cog import Cog
from ..embeds import Embed
from ..file import File
from ..guild import Guild
from ..interactions import InteractionChannel
from ..member import Member
from ..message import Message
from ..permissions import Permissions
from ..poll import Poll
from ..state import ConnectionState
from ..ui import BaseView
from ..user import User
from ..voice import VoiceClient
from ..webhook import WebhookMessage
from .core import ApplicationCommand, Option
T = TypeVar("T")
CogT = TypeVar("CogT", bound="Cog")
if TYPE_CHECKING:
P = ParamSpec("P")
else:
P = TypeVar("P")
__all__ = ("ApplicationContext", "AutocompleteContext")
class ApplicationContext(discord.abc.Messageable):
"""Represents a Discord application command interaction context.
This class is not created manually and is instead passed to application
commands as the first parameter.
.. versionadded:: 2.0
Attributes
----------
bot: :class:`.Bot`
The bot that the command belongs to.
interaction: :class:`.Interaction`
The interaction object that invoked the command.
"""
def __init__(self, bot: Bot, interaction: Interaction):
self.bot = bot
self.interaction = interaction
# below attributes will be set after initialization
self.focused: Option = None # type: ignore
self.value: str = None # type: ignore
self.options: dict = None # type: ignore
self._state: ConnectionState = self.interaction._state
async def _get_channel(self) -> InteractionChannel | None:
return self.interaction.channel
async def invoke(
self,
command: ApplicationCommand[CogT, P, T],
/,
*args: P.args,
**kwargs: P.kwargs,
) -> T:
r"""|coro|
Calls a command with the arguments given.
This is useful if you want to just call the callback that a
:class:`.ApplicationCommand` holds internally.
.. note::
This does not handle converters, checks, cooldowns, pre-invoke,
or after-invoke hooks in any matter. It calls the internal callback
directly as-if it was a regular function.
You must take care in passing the proper arguments when
using this function.
Parameters
-----------
command: :class:`.ApplicationCommand`
The command that is going to be called.
\*args
The arguments to use.
\*\*kwargs
The keyword arguments to use.
Raises
-------
TypeError
The command argument to invoke is missing.
"""
return await command(self, *args, **kwargs)
@property
def command(self) -> ApplicationCommand | None:
"""The command that this context belongs to."""
return self.interaction.command
@command.setter
def command(self, value: ApplicationCommand | None) -> None:
self.interaction.command = value
@property
def channel(self) -> InteractionChannel | None:
"""Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]:
Returns the channel associated with this context's command. Shorthand for :attr:`.Interaction.channel`.
"""
return self.interaction.channel
@property
def channel_id(self) -> int | None:
"""Returns the ID of the channel associated with this context's command.
Shorthand for :attr:`.Interaction.channel_id`.
"""
return self.interaction.channel_id
@property
def guild(self) -> Guild | None:
"""Returns the guild associated with this context's command.
Shorthand for :attr:`.Interaction.guild`.
"""
return self.interaction.guild
@property
def guild_id(self) -> int | None:
"""Returns the ID of the guild associated with this context's command.
Shorthand for :attr:`.Interaction.guild_id`.
"""
return self.interaction.guild_id
@property
def locale(self) -> str | None:
"""Returns the locale of the guild associated with this context's command.
Shorthand for :attr:`.Interaction.locale`.
"""
return self.interaction.locale
@property
def guild_locale(self) -> str | None:
"""Returns the locale of the guild associated with this context's command.
Shorthand for :attr:`.Interaction.guild_locale`.
"""
return self.interaction.guild_locale
@property
def app_permissions(self) -> Permissions:
return self.interaction.app_permissions
@property
def me(self) -> Member | ClientUser | None:
"""Union[:class:`.Member`, :class:`.ClientUser`]:
Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message
message contexts, or when :meth:`Intents.guilds` is absent.
"""
return (
self.interaction.guild.me
if self.interaction.guild is not None
else self.bot.user
)
@property
def message(self) -> Message | None:
"""Returns the message sent with this context's command.
Shorthand for :attr:`.Interaction.message`, if applicable.
"""
return self.interaction.message
@property
def user(self) -> Member | User:
"""Returns the user that sent this context's command.
Shorthand for :attr:`.Interaction.user`.
"""
return self.interaction.user # type: ignore # command user will never be None
author: Member | User = user
@property
def voice_client(self) -> VoiceClient | None:
"""Returns the voice client associated with this context's command.
Shorthand for :attr:`Interaction.guild.voice_client<~discord.Guild.voice_client>`, if applicable.
"""
if self.interaction.guild is None:
return None
return self.interaction.guild.voice_client
@property
def response(self) -> InteractionResponse:
"""Returns the response object associated with this context's command.
Shorthand for :attr:`.Interaction.response`.
"""
return self.interaction.response
@property
def selected_options(self) -> list[dict[str, Any]] | None:
"""The options and values that were selected by the user when sending the command.
Returns
-------
Optional[List[Dict[:class:`str`, Any]]]
A dictionary containing the options and values that were selected by the user when the command
was processed, if applicable. Returns ``None`` if the command has not yet been invoked,
or if there are no options defined for that command.
"""
return self.interaction.data.get("options", None)
@property
def unselected_options(self) -> list[Option] | None:
"""The options that were not provided by the user when sending the command.
Returns
-------
Optional[List[:class:`.Option`]]
A list of Option objects (if any) that were not selected by the user when the command was processed.
Returns ``None`` if there are no options defined for that command.
"""
if self.command.options is not None: # type: ignore
if self.selected_options:
return [
option
for option in self.command.options # type: ignore
if option.to_dict()["name"]
not in [opt["name"] for opt in self.selected_options]
]
else:
return self.command.options # type: ignore
return None
@property
def attachment_size_limit(self) -> int:
"""Returns the attachment size limit associated with this context's interaction.
Shorthand for :attr:`.Interaction.attachment_size_limit`.
"""
return self.interaction.attachment_size_limit
@property
@discord.utils.copy_doc(InteractionResponse.send_modal)
def send_modal(self) -> Callable[..., Awaitable[Interaction]]:
return self.interaction.response.send_modal
@overload
async def respond(
self,
content: Any | None = None,
embed: Embed | None = None,
view: BaseView | None = None,
tts: bool = False,
ephemeral: bool = False,
allowed_mentions: AllowedMentions | None = None,
file: File | None = None,
files: list[File] | None = None,
poll: Poll | None = None,
delete_after: float | None = None,
silent: bool = False,
suppress_embeds: bool = False,
) -> Interaction | WebhookMessage: ...
@overload
async def respond(
self,
content: Any | None = None,
embeds: list[Embed] | None = None,
view: BaseView | None = None,
tts: bool = False,
ephemeral: bool = False,
allowed_mentions: AllowedMentions | None = None,
file: File | None = None,
files: list[File] | None = None,
poll: Poll | None = None,
delete_after: float | None = None,
silent: bool = False,
suppress_embeds: bool = False,
) -> Interaction | WebhookMessage: ...
@discord.utils.copy_doc(Interaction.respond)
async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage:
return await self.interaction.respond(*args, **kwargs)
@property
@discord.utils.copy_doc(InteractionResponse.send_message)
def send_response(self) -> Callable[..., Awaitable[Interaction]]:
if not self.interaction.response.is_done():
return self.interaction.response.send_message
else:
raise RuntimeError(
"Interaction was already issued a response. Try using"
f" {type(self).__name__}.send_followup() instead."
)
@property
@discord.utils.copy_doc(Webhook.send)
def send_followup(self) -> Callable[..., Awaitable[WebhookMessage]]:
if self.interaction.response.is_done():
return self.followup.send
else:
raise RuntimeError(
"Interaction was not yet issued a response. Try using"
f" {type(self).__name__}.respond() first."
)
@property
@discord.utils.copy_doc(InteractionResponse.defer)
def defer(self) -> Callable[..., Awaitable[None]]:
return self.interaction.response.defer
@property
def followup(self) -> Webhook:
"""Returns the followup webhook for followup interactions."""
return self.interaction.followup
async def delete(self, *, delay: float | None = None) -> None:
"""|coro|
Deletes the original interaction response message.
This is a higher level interface to :meth:`Interaction.delete_original_response`.
Parameters
----------
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
Raises
------
HTTPException
Deleting the message failed.
Forbidden
You do not have proper permissions to delete the message.
"""
if not self.interaction.response.is_done():
await self.defer()
return await self.interaction.delete_original_response(delay=delay)
@property
@discord.utils.copy_doc(Interaction.edit_original_response)
def edit(self) -> Callable[..., Awaitable[InteractionMessage]]:
return self.interaction.edit_original_response
@property
def cog(self) -> Cog | None:
"""Returns the cog associated with this context's command.
``None`` if it does not exist.
"""
if self.command is None:
return None
return self.command.cog
def is_guild_authorised(self) -> bool:
""":class:`bool`: Checks if the invoked command is guild-installed.
This is a shortcut for :meth:`Interaction.is_guild_authorised`.
There is an alias for this called :meth:`.is_guild_authorized`.
.. versionadded:: 2.7
"""
return self.interaction.is_guild_authorised()
def is_user_authorised(self) -> bool:
""":class:`bool`: Checks if the invoked command is user-installed.
This is a shortcut for :meth:`Interaction.is_user_authorised`.
There is an alias for this called :meth:`.is_user_authorized`.
.. versionadded:: 2.7
"""
return self.interaction.is_user_authorised()
def is_guild_authorized(self) -> bool:
""":class:`bool`: An alias for :meth:`.is_guild_authorised`.
.. versionadded:: 2.7
"""
return self.is_guild_authorised()
def is_user_authorized(self) -> bool:
""":class:`bool`: An alias for :meth:`.is_user_authorised`.
.. versionadded:: 2.7
"""
return self.is_user_authorised()
class AutocompleteContext:
"""Represents context for a slash command's option autocomplete.
This class is not created manually and is instead passed to an :class:`.Option`'s autocomplete callback.
.. versionadded:: 2.0
Attributes
----------
bot: :class:`.Bot`
The bot that the command belongs to.
interaction: :class:`.Interaction`
The interaction object that invoked the autocomplete.
focused: :class:`.Option`
The option the user is currently typing.
value: :class:`.str`
The content of the focused option.
options: Dict[:class:`str`, Any]
A name to value mapping of the options that the user has selected before this option.
"""
__slots__ = ("bot", "interaction", "focused", "value", "options")
def __init__(self, bot: Bot, interaction: Interaction):
self.bot = bot
self.interaction = interaction
self.focused: Option = None # type: ignore
self.value: str = None # type: ignore
self.options: dict = None # type: ignore
@property
def cog(self) -> Cog | None:
"""Returns the cog associated with this context's command.
``None`` if it does not exist.
"""
if self.command is None:
return None
return self.command.cog
@property
def command(self) -> ApplicationCommand | None:
"""The command that this context belongs to."""
return self.interaction.command
@command.setter
def command(self, value: ApplicationCommand | None) -> None:
self.interaction.command = value
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,557 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Pycord Development
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import inspect
import logging
import sys
import types
from collections.abc import Awaitable, Callable, Iterable
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Literal,
Optional,
Type,
TypeVar,
Union,
get_args,
)
if sys.version_info >= (3, 12):
from typing import TypeAliasType
else:
from typing_extensions import TypeAliasType
from ..abc import GuildChannel, Mentionable
from ..channel import (
CategoryChannel,
DMChannel,
ForumChannel,
MediaChannel,
StageChannel,
TextChannel,
Thread,
VoiceChannel,
)
from ..commands import ApplicationContext, AutocompleteContext
from ..enums import ChannelType
from ..enums import Enum as DiscordEnum
from ..enums import SlashCommandOptionType
from ..utils import MISSING, basic_autocomplete
if TYPE_CHECKING:
from ..cog import Cog
from ..ext.commands import Converter
from ..member import Member
from ..message import Attachment
from ..role import Role
from ..user import User
InputType = Union[
Type[str],
Type[bool],
Type[int],
Type[float],
Type[GuildChannel],
Type[Thread],
Type[Member],
Type[User],
Type[Attachment],
Type[Role],
Type[Mentionable],
SlashCommandOptionType,
Converter,
Type[Converter],
Type[Enum],
Type[DiscordEnum],
]
AutocompleteReturnType = Union[
Iterable["OptionChoice"], Iterable[str], Iterable[int], Iterable[float]
]
T = TypeVar("T", bound=AutocompleteReturnType)
MaybeAwaitable = Union[T, Awaitable[T]]
AutocompleteFunction = Union[
Callable[[AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
Callable[[Cog, AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
Callable[
[AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
MaybeAwaitable[AutocompleteReturnType],
],
Callable[
[Cog, AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
MaybeAwaitable[AutocompleteReturnType],
],
]
__all__ = (
"ThreadOption",
"Option",
"OptionChoice",
"option",
)
CHANNEL_TYPE_MAP = {
TextChannel: ChannelType.text,
VoiceChannel: ChannelType.voice,
StageChannel: ChannelType.stage_voice,
CategoryChannel: ChannelType.category,
Thread: ChannelType.public_thread,
ForumChannel: ChannelType.forum,
MediaChannel: ChannelType.media,
DMChannel: ChannelType.private,
}
_log = logging.getLogger(__name__)
class ThreadOption:
"""Represents a class that can be passed as the ``input_type`` for an :class:`Option` class.
.. versionadded:: 2.0
Parameters
----------
thread_type: Literal["public", "private", "news"]
The thread type to expect for this options input.
"""
def __init__(self, thread_type: Literal["public", "private", "news"]):
type_map = {
"public": ChannelType.public_thread,
"private": ChannelType.private_thread,
"news": ChannelType.news_thread,
}
self._type = type_map[thread_type]
class Option:
"""Represents a selectable option for a slash command.
Attributes
----------
input_type: Union[Type[:class:`str`], Type[:class:`bool`], Type[:class:`int`], Type[:class:`float`], Type[:class:`.abc.GuildChannel`], Type[:class:`Thread`], Type[:class:`Member`], Type[:class:`User`], Type[:class:`Attachment`], Type[:class:`Role`], Type[:class:`.abc.Mentionable`], :class:`SlashCommandOptionType`, Type[:class:`.ext.commands.Converter`], Type[:class:`enums.Enum`], Type[:class:`Enum`]]
The type of input that is expected for this option. This can be a :class:`SlashCommandOptionType`,
an associated class, a channel type, a :class:`Converter`, a converter class or an :class:`enum.Enum`.
If a :class:`enum.Enum` is used and it has up to 25 values, :attr:`choices` will be automatically filled. If the :class:`enum.Enum` has more than 25 values, :attr:`autocomplete` will be implemented with :func:`discord.utils.basic_autocomplete` instead.
name: :class:`str`
The name of this option visible in the UI.
Inherits from the variable name if not provided as a parameter.
description: Optional[:class:`str`]
The description of this option.
Must be 100 characters or fewer. If :attr:`input_type` is a :class:`enum.Enum` and :attr:`description` is not specified, :attr:`input_type`'s docstring will be used.
choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]]
The list of available choices for this option.
Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair).
If provided, the input from the user must match one of the choices in the list.
required: Optional[:class:`bool`]
Whether this option is required.
default: Optional[:class:`Any`]
The default value for this option. If provided, ``required`` will be considered ``False``.
min_value: Optional[:class:`int`]
The minimum value that can be entered.
Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`.
max_value: Optional[:class:`int`]
The maximum value that can be entered.
Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`.
min_length: Optional[:class:`int`]
The minimum length of the string that can be entered. Must be between 0 and 6000 (inclusive).
Only applies to Options with an :attr:`input_type` of :class:`str`.
max_length: Optional[:class:`int`]
The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive).
Only applies to Options with an :attr:`input_type` of :class:`str`.
channel_types: list[:class:`discord.ChannelType`] | None
A list of channel types that can be selected in this option.
Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`.
If this argument is used, :attr:`input_type` will be ignored.
name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this option. The values of this should be ``"locale": "name"``.
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this option. The values of this should be ``"locale": "description"``.
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
Examples
--------
Basic usage: ::
@bot.slash_command(guild_ids=[...])
async def hello(
ctx: discord.ApplicationContext,
name: Option(str, "Enter your name"),
age: Option(int, "Enter your age", min_value=1, max_value=99, default=18)
# passing the default value makes an argument optional
# you also can create optional argument using:
# age: Option(int, "Enter your age") = 18
):
await ctx.respond(f"Hello! Your name is {name} and you are {age} years old.")
.. versionadded:: 2.0
"""
input_type: SlashCommandOptionType
converter: Converter | type[Converter] | None = None
def __init__(
self, input_type: InputType = str, /, description: str | None = None, **kwargs
) -> None:
self.name: str | None = kwargs.pop("name", None)
if self.name is not None:
self.name = str(self.name)
self._parameter_name = self.name # default
input_type = self._parse_type_alias(input_type)
input_type = self._strip_none_type(input_type)
self._raw_type: InputType | tuple = input_type
enum_choices = []
input_type_is_class = isinstance(input_type, type)
if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)):
if description is None and input_type.__doc__ is not None:
description = inspect.cleandoc(input_type.__doc__)
if description and len(description) > 100:
description = description[:97] + "..."
_log.warning(
"Option %s's description was truncated due to Enum %s's docstring exceeding 100 characters.",
self.name,
input_type,
)
enum_choices = [OptionChoice(e.name, e.value) for e in input_type]
value_class = enum_choices[0].value.__class__
if value_class in SlashCommandOptionType.__members__ and all(
isinstance(elem.value, value_class) for elem in enum_choices
):
input_type = SlashCommandOptionType.from_datatype(
enum_choices[0].value.__class__
)
else:
enum_choices = [OptionChoice(e.name, str(e.value)) for e in input_type]
input_type = SlashCommandOptionType.string
self.description = description or "No description provided"
self.channel_types: list[ChannelType] = kwargs.pop("channel_types", [])
if self.channel_types:
self.input_type = SlashCommandOptionType.channel
elif isinstance(input_type, SlashCommandOptionType):
self.input_type = input_type
else:
from ..ext.commands import Converter
if isinstance(input_type, tuple) and any(
issubclass(op, ApplicationContext) for op in input_type
):
input_type = next(
op for op in input_type if issubclass(op, ApplicationContext)
)
if (
isinstance(input_type, Converter)
or input_type_is_class
and issubclass(input_type, Converter)
):
self.converter = input_type
self._raw_type = str
self.input_type = SlashCommandOptionType.string
else:
try:
self.input_type = SlashCommandOptionType.from_datatype(input_type)
except TypeError as exc:
from ..ext.commands.converter import CONVERTER_MAPPING
if input_type not in CONVERTER_MAPPING:
raise exc
self.converter = CONVERTER_MAPPING[input_type]
self._raw_type = str
self.input_type = SlashCommandOptionType.string
else:
if self.input_type == SlashCommandOptionType.channel:
if not isinstance(self._raw_type, tuple):
if hasattr(input_type, "__args__"):
self._raw_type = input_type.__args__ # type: ignore # Union.__args__
else:
self._raw_type = (input_type,)
if not self.channel_types:
self.channel_types = [
CHANNEL_TYPE_MAP[t]
for t in self._raw_type
if t is not GuildChannel
]
self.required: bool = (
kwargs.pop("required", True) if "default" not in kwargs else False
)
self.default = kwargs.pop("default", None)
self._autocomplete: AutocompleteFunction | None = None
self._autocomplete_is_instance_method: bool = False
self.autocomplete = kwargs.pop("autocomplete", None)
if len(enum_choices) > 25:
self.choices: list[OptionChoice] = []
for e in enum_choices:
e.value = str(e.value)
self.autocomplete = basic_autocomplete(enum_choices)
self.input_type = SlashCommandOptionType.string
else:
self.choices: list[OptionChoice] = enum_choices or [
o if isinstance(o, OptionChoice) else OptionChoice(o)
for o in kwargs.pop("choices", [])
]
if self.input_type == SlashCommandOptionType.integer:
minmax_types = (int, type(None))
minmax_typehint = Optional[int]
elif self.input_type == SlashCommandOptionType.number:
minmax_types = (int, float, type(None))
minmax_typehint = Optional[Union[int, float]]
else:
minmax_types = (type(None),)
minmax_typehint = type(None)
if self.input_type == SlashCommandOptionType.string:
minmax_length_types = (int, type(None))
minmax_length_typehint = Optional[int]
else:
minmax_length_types = (type(None),)
minmax_length_typehint = type(None)
self.min_value: int | float | None = kwargs.pop("min_value", None)
self.max_value: int | float | None = kwargs.pop("max_value", None)
self.min_length: int | None = kwargs.pop("min_length", None)
self.max_length: int | None = kwargs.pop("max_length", None)
if (
self.input_type != SlashCommandOptionType.integer
and self.input_type != SlashCommandOptionType.number
and (self.min_value or self.max_value)
):
raise AttributeError(
"Option does not take min_value or max_value if not of type "
"SlashCommandOptionType.integer or SlashCommandOptionType.number"
)
if self.input_type != SlashCommandOptionType.string and (
self.min_length or self.max_length
):
raise AttributeError(
"Option does not take min_length or max_length if not of type str"
)
if self.min_value is not None and not isinstance(self.min_value, minmax_types):
raise TypeError(
f"Expected {minmax_typehint} for min_value, got"
f' "{type(self.min_value).__name__}"'
)
if self.max_value is not None and not isinstance(self.max_value, minmax_types):
raise TypeError(
f"Expected {minmax_typehint} for max_value, got"
f' "{type(self.max_value).__name__}"'
)
if self.min_length is not None:
if not isinstance(self.min_length, minmax_length_types):
raise TypeError(
f"Expected {minmax_length_typehint} for min_length,"
f' got "{type(self.min_length).__name__}"'
)
if self.min_length < 0 or self.min_length > 6000:
raise AttributeError(
"min_length must be between 0 and 6000 (inclusive)"
)
if self.max_length is not None:
if not isinstance(self.max_length, minmax_length_types):
raise TypeError(
f"Expected {minmax_length_typehint} for max_length,"
f' got "{type(self.max_length).__name__}"'
)
if self.max_length < 1 or self.max_length > 6000:
raise AttributeError("max_length must between 1 and 6000 (inclusive)")
self.name_localizations = kwargs.pop("name_localizations", MISSING)
self.description_localizations = kwargs.pop(
"description_localizations", MISSING
)
if input_type is None:
raise TypeError("input_type cannot be NoneType.")
@staticmethod
def _parse_type_alias(input_type: InputType) -> InputType:
if isinstance(input_type, TypeAliasType):
return input_type.__value__
return input_type
@staticmethod
def _strip_none_type(input_type):
if isinstance(input_type, SlashCommandOptionType):
return input_type
if input_type is type(None):
raise TypeError("Option type cannot be only NoneType")
args = ()
if isinstance(input_type, types.UnionType):
args = get_args(input_type)
elif getattr(input_type, "__origin__", None) is Union:
args = get_args(input_type)
elif isinstance(input_type, tuple):
args = input_type
if args:
filtered = tuple(t for t in args if t is not type(None))
if not filtered:
raise TypeError("Option type cannot be only NoneType")
if len(filtered) == 1:
return filtered[0]
return filtered
return input_type
def to_dict(self) -> dict:
as_dict = {
"name": self.name,
"description": self.description,
"type": self.input_type.value,
"required": self.required,
"choices": [c.to_dict() for c in self.choices],
"autocomplete": bool(self.autocomplete),
}
if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
if self.description_localizations is not MISSING:
as_dict["description_localizations"] = self.description_localizations
if self.channel_types:
as_dict["channel_types"] = [t.value for t in self.channel_types]
if self.min_value is not None:
as_dict["min_value"] = self.min_value
if self.max_value is not None:
as_dict["max_value"] = self.max_value
if self.min_length is not None:
as_dict["min_length"] = self.min_length
if self.max_length is not None:
as_dict["max_length"] = self.max_length
return as_dict
def __repr__(self):
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
@property
def autocomplete(self) -> AutocompleteFunction | None:
"""
The autocomplete handler for the option. Accepts a callable (sync or async)
that takes a single required argument of :class:`AutocompleteContext` or two arguments
of :class:`discord.Cog` (being the command's cog) and :class:`AutocompleteContext`.
The callable must return an iterable of :class:`str` or :class:`OptionChoice`.
Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable.
Returns
-------
Optional[AutocompleteFunction]
.. versionchanged:: 2.7
.. note::
Does not validate the input value against the autocomplete results.
"""
return self._autocomplete
@autocomplete.setter
def autocomplete(self, value: AutocompleteFunction | None) -> None:
self._autocomplete = value
# this is done here so it does not have to be computed every time the autocomplete is invoked
if self._autocomplete is not None:
self._autocomplete_is_instance_method = (
sum(
1
for param in inspect.signature(
self._autocomplete
).parameters.values()
if param.default == param.empty
and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
)
== 2
)
class OptionChoice:
"""
Represents a name:value pairing for a selected :class:`.Option`.
.. versionadded:: 2.0
Attributes
----------
name: :class:`str`
The name of the choice. Shown in the UI when selecting an option.
value: Optional[Union[:class:`str`, :class:`int`, :class:`float`]]
The value of the choice. If not provided, will use the value of ``name``.
name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this choice. The values of this should be ``"locale": "name"``.
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
"""
def __init__(
self,
name: str,
value: str | int | float | None = None,
name_localizations: dict[str, str] = MISSING,
):
self.name = str(name)
self.value = value if value is not None else name
self.name_localizations = name_localizations
def to_dict(self) -> dict[str, str | int | float]:
as_dict = {"name": self.name, "value": self.value}
if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
return as_dict
def option(name, input_type=None, **kwargs):
"""A decorator that can be used instead of typehinting :class:`.Option`.
.. versionadded:: 2.0
Attributes
----------
parameter_name: :class:`str`
The name of the target function parameter this option is mapped to.
This allows you to have a separate UI ``name`` and parameter name.
"""
def decorator(func):
resolved_name = kwargs.pop("parameter_name", None) or name
itype = (
kwargs.pop("type", None)
or input_type
or func.__annotations__.get(resolved_name, str)
)
func.__annotations__[resolved_name] = Option(itype, name=name, **kwargs)
return func
return decorator
@@ -0,0 +1,139 @@
"""
The MIT License (MIT)
Copyright (c) 2015-2021 Rapptz
Copyright (c) 2021-present Pycord Development
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from typing import Callable
from ..enums import InteractionContextType
from ..permissions import Permissions
from .core import ApplicationCommand
__all__ = ("default_permissions", "guild_only", "is_nsfw")
def default_permissions(**perms: bool) -> Callable:
"""A decorator that limits the usage of an application command to members with certain
permissions.
The permissions passed in must be exactly like the properties shown under
:class:`.discord.Permissions`.
.. note::
These permissions can be updated by server administrators per-guild. As such, these are only "defaults", as the
name suggests. If you want to make sure that a user **always** has the specified permissions regardless, you
should use an internal check such as :func:`~.ext.commands.has_permissions`.
Parameters
----------
**perms: Dict[:class:`str`, :class:`bool`]
An argument list of permissions to check for.
Example
-------
.. code-block:: python3
from discord import default_permissions
@bot.slash_command()
@default_permissions(manage_messages=True)
async def test(ctx):
await ctx.respond('You can manage messages.')
"""
invalid = set(perms) - set(Permissions.VALID_FLAGS)
if invalid:
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def inner(command: Callable):
if isinstance(command, ApplicationCommand):
if command.parent is not None:
raise RuntimeError(
"Permission restrictions can only be set on top-level commands"
)
command.default_member_permissions = Permissions(**perms)
else:
command.__default_member_permissions__ = Permissions(**perms)
return command
return inner
def guild_only() -> Callable:
"""A decorator that limits the usage of an application command to guild contexts.
The command won't be able to be used in private message channels.
Example
-------
.. code-block:: python3
from discord import guild_only
@bot.slash_command()
@guild_only()
async def test(ctx):
await ctx.respond("You're in a guild.")
"""
def inner(command: Callable):
if isinstance(command, ApplicationCommand):
command.contexts = {InteractionContextType.guild}
else:
command.__contexts__ = {InteractionContextType.guild}
return command
return inner
def is_nsfw() -> Callable:
"""A decorator that limits the usage of an application command to 18+ channels and users.
In guilds, the command will only be able to be used in channels marked as NSFW.
In DMs, users must have opted into age-restricted commands via privacy settings.
Note that apps intending to be listed in the App Directory cannot have NSFW commands.
Example
-------
.. code-block:: python3
from discord import is_nsfw
@bot.slash_command()
@is_nsfw()
async def test(ctx):
await ctx.respond("This command is age restricted.")
"""
def inner(command: Callable):
if isinstance(command, ApplicationCommand):
command.nsfw = True
else:
command.__nsfw__ = True
return command
return inner