diff --git a/discord/audit_logs.py b/discord/audit_logs.py index b50f496af1..ee8a4c47e9 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -238,6 +238,7 @@ class AuditLogChanges: "location_type", _enum_transformer(enums.ScheduledEventLocationType), ), + "command_id": ("command_id", _transform_snowflake), } def __init__( diff --git a/discord/bot.py b/discord/bot.py index e9ec44f777..48e2f55e2e 100644 --- a/discord/bot.py +++ b/discord/bot.py @@ -60,7 +60,7 @@ command, ) from .enums import InteractionType -from .errors import CheckFailure, DiscordException, Forbidden, HTTPException +from .errors import CheckFailure, DiscordException from .interactions import Interaction from .shard import AutoShardedClient from .types import interactions @@ -256,7 +256,8 @@ def _check_command(cmd: ApplicationCommand, match: Dict) -> bool: else: as_dict = cmd.to_dict() to_check = { - "default_permission": None, + "dm_permission": None, + "default_member_permissions": None, "name": None, "description": None, "name_localizations": None, @@ -572,7 +573,6 @@ async def sync_commands( register_guild_commands: bool = True, check_guilds: Optional[List[int]] = [], delete_exiting: bool = True, - _register_permissions: bool = True, # TODO: Remove for perms v2 ) -> None: """|coro| @@ -645,12 +645,6 @@ async def sync_commands( guild_commands, guild_id=guild_id, method=method, force=force, delete_existing=delete_exiting ) - # TODO: 2.1: Remove this and favor permissions v2 - # Global Command Permissions - - if not _register_permissions: - return - global_permissions: List = [] for i in registered_commands: @@ -664,12 +658,7 @@ async def sync_commands( cmd.id = i["id"] self._application_commands[cmd.id] = cmd - # Permissions (Roles will be converted to IDs just before Upsert for Global Commands) - global_permissions.append({"id": i["id"], "permissions": cmd.permissions}) - for guild_id, commands in registered_guild_commands.items(): - guild_permissions: List = [] - for i in commands: cmd = find( lambda cmd: cmd.name == i["name"] @@ -684,105 +673,6 @@ async def sync_commands( cmd.id = i["id"] self._application_commands[cmd.id] = cmd - # Permissions - permissions = [ - perm.to_dict() - for perm in cmd.permissions - if perm.guild_id is None - or (perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in cmd.guild_ids) - ] - guild_permissions.append({"id": i["id"], "permissions": permissions}) - - for global_command in global_permissions: - permissions = [ - perm.to_dict() - for perm in global_command["permissions"] - if perm.guild_id is None - or (perm.guild_id == guild_id and cmd.guild_ids is not None and perm.guild_id in cmd.guild_ids) - ] - guild_permissions.append({"id": global_command["id"], "permissions": permissions}) - - # Collect & Upsert Permissions for Each Guild - # Command Permissions for this Guild - guild_cmd_perms: List = [] - - # Loop through Commands Permissions available for this Guild - for item in guild_permissions: - new_cmd_perm = {"id": item["id"], "permissions": []} - - # Replace Role / Owner Names with IDs - for permission in item["permissions"]: - if isinstance(permission["id"], str): - # Replace Role Names - if permission["type"] == 1: - role = get( - self._bot.get_guild(guild_id).roles, - name=permission["id"], - ) - - # If not missing - if role is not None: - new_cmd_perm["permissions"].append( - { - "id": role.id, - "type": 1, - "permission": permission["permission"], - } - ) - else: - raise RuntimeError( - "No Role ID found in Guild ({guild_id}) for Role ({role})".format( - guild_id=guild_id, role=permission["id"] - ) - ) - # Add owner IDs - elif permission["type"] == 2 and permission["id"] == "owner": - app = await self.application_info() # type: ignore - if app.team: - for m in app.team.members: - new_cmd_perm["permissions"].append( - { - "id": m.id, - "type": 2, - "permission": permission["permission"], - } - ) - else: - new_cmd_perm["permissions"].append( - { - "id": app.owner.id, - "type": 2, - "permission": permission["permission"], - } - ) - # Add the rest - else: - new_cmd_perm["permissions"].append(permission) - - # Make sure we don't have over 10 overwrites - if len(new_cmd_perm["permissions"]) > 10: - raise RuntimeError( - "Command '{name}' has more than 10 permission overrides in guild ({guild_id}).".format( - name=self._application_commands[new_cmd_perm["id"]].name, - guild_id=guild_id, - ) - ) - # Append to guild_cmd_perms - guild_cmd_perms.append(new_cmd_perm) - - # Upsert - try: - await self._bot.http.bulk_upsert_command_permissions(self._bot.user.id, guild_id, guild_cmd_perms) - except Forbidden: - raise RuntimeError( - f"Failed to add command permissions to guild {guild_id}", - file=sys.stderr, - ) - except HTTPException: - _log.warning( - "Command Permissions V2 not yet implemented, permissions will not be set for your commands." - ) - async def process_application_commands(self, interaction: Interaction, auto_sync: bool = None) -> None: """|coro| diff --git a/discord/channel.py b/discord/channel.py index 160341e427..1ad3b5a218 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -25,10 +25,7 @@ from __future__ import annotations -import asyncio import datetime -import time -from abc import abstractmethod from typing import ( TYPE_CHECKING, Any, @@ -1140,7 +1137,11 @@ class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): .. versionadded:: 2.0 """ - __slots__ = () + __slots__ = "nsfw" + + def _update(self, guild: Guild, data: VoiceChannelPayload): + super()._update(guild, data) + self.nsfw: bool = data.get("nsfw", False) def __repr__(self) -> str: attrs = [ @@ -1159,6 +1160,10 @@ def __repr__(self) -> str: async def _get_channel(self): return self + def is_nsfw(self) -> bool: + """:class:`bool`: Checks if the channel is NSFW.""" + return self.nsfw + @property def last_message(self) -> Optional[Message]: """Fetches the last message from this channel in cache. diff --git a/discord/commands/core.py b/discord/commands/core.py index 3dd2185206..7e5aa728e3 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -32,10 +32,10 @@ import re import types from collections import OrderedDict +from enum import Enum from typing import ( TYPE_CHECKING, Any, - Awaitable, Callable, Coroutine, Dict, @@ -46,17 +46,15 @@ Type, TypeVar, Union, - overload, ) from ..channel import _guild_channel_factory -from ..enums import ChannelType, MessageType, SlashCommandOptionType, try_enum +from ..enums import MessageType, SlashCommandOptionType, try_enum from ..errors import ( ApplicationCommandError, ApplicationCommandInvokeError, CheckFailure, ClientException, - NotFound, ValidationError, ) from ..member import Member @@ -64,10 +62,9 @@ from ..object import Object from ..role import Role from ..user import User -from ..utils import async_all, find, get_or_fetch, utcnow +from ..utils import async_all, find, utcnow from .context import ApplicationContext, AutocompleteContext from .options import Option, OptionChoice -from .permissions import CommandPermission __all__ = ( "_BaseCommand", @@ -87,6 +84,7 @@ if TYPE_CHECKING: from typing_extensions import Concatenate, ParamSpec + from .. import Permissions from ..cog import Cog T = TypeVar("T") @@ -100,12 +98,16 @@ def wrap_callback(coro): + from ..ext.commands.errors import CommandError + @functools.wraps(coro) async def wrapped(*args, **kwargs): try: ret = await coro(*args, **kwargs) except ApplicationCommandError: raise + except CommandError: + raise except asyncio.CancelledError: return except Exception as exc: @@ -116,12 +118,16 @@ async def wrapped(*args, **kwargs): def hooked_wrapped_callback(command, ctx, coro): + from ..ext.commands.errors import CommandError + @functools.wraps(coro) async def wrapped(arg): try: ret = await coro(arg) except ApplicationCommandError: raise + except CommandError: + raise except asyncio.CancelledError: return except Exception as exc: @@ -201,6 +207,12 @@ def __init__(self, func: Callable, **kwargs) -> None: self.guild_ids: Optional[List[int]] = kwargs.get("guild_ids", None) self.parent = kwargs.get("parent") + # Permissions + self.default_member_permissions: Optional["Permissions"] = getattr( + func, "__default_member_permissions__", kwargs.get("default_member_permissions", None) + ) + self.guild_only: Optional[bool] = getattr(func, "__guild_only__", kwargs.get("guild_only", None)) + def __repr__(self) -> str: return f"" @@ -573,16 +585,11 @@ class SlashCommand(ApplicationCommand): parent: Optional[:class:`SlashCommandGroup`] The parent group that this command belongs to. ``None`` if there isn't one. - default_permission: :class:`bool` - Whether the command is enabled by default when it is added to a guild. - permissions: List[:class:`CommandPermission`] - The permissions for this command. - - .. note:: - - If this is not empty then default_permissions will be set to False. - - cog: Optional[:class:`.Cog`] + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] The cog that this command belongs to. ``None`` if there isn't one. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] A list of predicates that verifies if the command could be executed @@ -647,42 +654,41 @@ def __init__(self, func: Callable, *args, **kwargs) -> None: self._before_invoke = None self._after_invoke = None - # Permissions - self.default_permission = kwargs.get("default_permission", True) - self.permissions: List[CommandPermission] = getattr(func, "__app_cmd_perms__", []) + kwargs.get( - "permissions", [] + def _check_required_params(self, params): + params = iter(params.items()) + required_params = ( + ["self", "context"] + if self.attached_to_group + or self.cog + or len(self.callback.__qualname__.split(".")) > 1 + else ["context"] ) - if self.permissions and self.default_permission: - self.default_permission = False + for p in required_params: + try: + next(params) + except StopIteration: + raise ClientException(f'Callback for {self.name} command is missing "{p}" parameter.') - def _parse_options(self, params) -> List[Option]: - if list(params.items())[0][0] == "self": - temp = list(params.items()) - temp.pop(0) - params = dict(temp) - params = iter(params.items()) + return params - # next we have the 'ctx' as the next parameter - try: - next(params) - except StopIteration: - raise ClientException(f'Callback for {self.name} command is missing "ctx" parameter.') + def _parse_options(self, params, *, check_params: bool = True) -> List[Option]: + if check_params: + params = self._check_required_params(params) final_options = [] for p_name, p_obj in params: - option = p_obj.annotation if option == inspect.Parameter.empty: option = str if self._is_typing_union(option): if self._is_typing_optional(option): - option = Option(option.__args__[0], "No description provided", required=False) + option = Option(option.__args__[0], required=False) else: - option = Option(option.__args__, "No description provided") + option = Option(option.__args__) if not isinstance(option, Option): - option = Option(option, "No description provided") + option = Option(option) if option.default is None: if p_obj.default == inspect.Parameter.empty: @@ -690,10 +696,10 @@ def _parse_options(self, params) -> List[Option]: else: option.default = p_obj.default option.required = False - if option.name is None: option.name = p_name - option._parameter_name = p_name + if option.name != p_name or option._parameter_name is None: + option._parameter_name = p_name _validate_names(option) _validate_descriptions(option) @@ -703,25 +709,15 @@ def _parse_options(self, params) -> List[Option]: return final_options def _match_option_param_names(self, params, options): - if list(params.items())[0][0] == "self": - temp = list(params.items()) - temp.pop(0) - params = dict(temp) - params = iter(params.items()) - - # next we have the 'ctx' as the next parameter - try: - next(params) - except StopIteration: - raise ClientException(f'Callback for {self.name} command is missing "ctx" parameter.') + params = self._check_required_params(params) - check_annotations = [ + check_annotations: List[Callable[[Option, Type], bool]] = [ lambda o, a: o.input_type == SlashCommandOptionType.string and o.converter is not None, # pass on converters lambda o, a: isinstance(o.input_type, SlashCommandOptionType), # pass on slash cmd option type enums lambda o, a: isinstance(o._raw_type, tuple) and a == Union[o._raw_type], # type: ignore # union types lambda o, a: self._is_typing_optional(a) and not o.required and o._raw_type in a.__args__, # optional - lambda o, a: inspect.isclass(a) and issubclass(a, o._raw_type), # 'normal' types + lambda o, a: isinstance(a, type) and issubclass(a, o._raw_type), # 'normal' types ] for o in options: _validate_names(o) @@ -729,18 +725,17 @@ def _match_option_param_names(self, params, options): try: p_name, p_obj = next(params) except StopIteration: # not enough params for all the options - raise ClientException(f"Too many arguments passed to the options kwarg.") + raise ClientException("Too many arguments passed to the options kwarg.") p_obj = p_obj.annotation - if not any(c(o, p_obj) for c in check_annotations): + if not any(check(o, p_obj) for check in check_annotations): raise TypeError(f"Parameter {p_name} does not match input type of {o.name}.") o._parameter_name = p_name left_out_params = OrderedDict() - left_out_params[""] = "" # bypass first iter (ctx) for k, v in params: left_out_params[k] = v - options.extend(self._parse_options(left_out_params)) + options.extend(self._parse_options(left_out_params, check_params=False)) return options @@ -761,7 +756,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "options": [o.to_dict() for o in self.options], - "default_permission": self.default_permission, } if self.name_localizations is not None: as_dict["name_localizations"] = self.name_localizations @@ -770,6 +764,12 @@ def to_dict(self) -> Dict: if self.is_subcommand: as_dict["type"] = SlashCommandOptionType.sub_command.value + if self.guild_only is not None: + as_dict["guild_only"] = self.guild_only + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = self.default_member_permissions.value + return as_dict async def _invoke(self, ctx: ApplicationContext) -> None: @@ -825,6 +825,15 @@ async def _invoke(self, ctx: ApplicationContext) -> None: elif op.input_type == SlashCommandOptionType.string and (converter := op.converter) is not None: arg = await converter.convert(converter, ctx, arg) + elif issubclass(op._raw_type, Enum): + if isinstance(arg, str) and arg.isdigit(): + try: + arg = op._raw_type(int(arg)) + except ValueError: + arg = op._raw_type(arg) + else: + arg = op._raw_type(arg) + kwargs[op._parameter_name] = arg for o in self.options: @@ -917,6 +926,10 @@ class SlashCommandGroup(ApplicationCommand): parent: Optional[:class:`SlashCommandGroup`] The parent group that this group belongs to. ``None`` if there isn't one. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. subcommands: List[Union[:class:`SlashCommand`, :class:`SlashCommandGroup`]] The list of all subcommands under this group. cog: Optional[:class:`.Cog`] @@ -979,10 +992,9 @@ def __init__( self.id = None # Permissions - self.default_permission = kwargs.get("default_permission", True) - self.permissions: List[CommandPermission] = kwargs.get("permissions", []) - if self.permissions and self.default_permission: - self.default_permission = False + self.default_member_permissions: Optional["Permissions"] = kwargs.get("default_member_permissions", None) + self.guild_only: Optional[bool] = kwargs.get("guild_only", None) + self.name_localizations: Optional[Dict[str, str]] = kwargs.get("name_localizations", None) self.description_localizations: Optional[Dict[str, str]] = kwargs.get("description_localizations", None) @@ -995,7 +1007,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "options": [c.to_dict() for c in self.subcommands], - "default_permission": self.default_permission, } if self.name_localizations is not None: as_dict["name_localizations"] = self.name_localizations @@ -1005,6 +1016,12 @@ def to_dict(self) -> Dict: if self.parent is not None: as_dict["type"] = self.input_type.value + if self.guild_only is not None: + as_dict["guild_only"] = self.guild_only + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = self.default_member_permissions.value + return as_dict def command(self, **kwargs) -> Callable[[Callable], SlashCommand]: @@ -1162,7 +1179,7 @@ def _update_copy(self, kwargs: Dict[str, Any]): return self.copy() def _set_cog(self, cog): - self.cog = cog + super()._set_cog(cog) for subcommand in self.subcommands: subcommand._set_cog(cog) @@ -1183,15 +1200,11 @@ class ContextMenuCommand(ApplicationCommand): The coroutine that is executed when the command is called. guild_ids: Optional[List[:class:`int`]] The ids of the guilds where this command will be registered. - default_permission: :class:`bool` - Whether the command is enabled by default when it is added to a guild. - permissions: List[:class:`.CommandPermission`] - The permissions for this command. - - .. note:: - If this is not empty then default_permissions will be set to ``False``. - - cog: Optional[:class:`.Cog`] + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] The cog that this command belongs to. ``None`` if there isn't one. checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] A list of predicates that verifies if the command could be executed @@ -1236,13 +1249,6 @@ def __init__(self, func: Callable, *args, **kwargs) -> None: self.validate_parameters() - self.default_permission = kwargs.get("default_permission", True) - self.permissions: List[CommandPermission] = getattr(func, "__app_cmd_perms__", []) + kwargs.get( - "permissions", [] - ) - if self.permissions and self.default_permission: - self.default_permission = False - # Context Menu commands can't have parents self.parent = None @@ -1283,9 +1289,14 @@ def to_dict(self) -> Dict[str, Union[str, int]]: "name": self.name, "description": self.description, "type": self.type, - "default_permission": self.default_permission, } + if self.guild_only is not None: + as_dict["guild_only"] = self.guild_only + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = self.default_member_permissions.value + if self.name_localizations is not None: as_dict["name_localizations"] = self.name_localizations @@ -1616,45 +1627,45 @@ def command(**kwargs): # Validation def validate_chat_input_name(name: Any, locale: Optional[str] = None): - # Must meet the regex ^[\w-]{1,32}$ + # Must meet the regex ^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$ if locale is not None and locale not in valid_locales: raise ValidationError( - f"Locale '{locale}' is not a valid locale, " - f"see {docs}/reference#locales for list of supported locales." + f"Locale '{locale}' is not a valid locale, " f"see {docs}/reference#locales for list of supported locales." ) error = None - if not isinstance(name, str): - error = TypeError(f"Command names and options must be of type str. Received \"{name}\"") - elif not re.match(r"^[\w-]{1,32}$", name): + if not isinstance(name, str) or not re.match(r"^[\w-]{1,32}$", name): + error = TypeError(f'Command names and options must be of type str. Received "{name}"') + elif not re.match(r"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$", name): error = ValidationError( - r"Command names and options must follow the regex \"^[\w-]{1,32}$\". For more information, see " + r"Command names and options must follow the regex \"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$\". For more information, see " f"{docs}/interactions/application-commands#application-command-object-application-command-naming. " - f"Received \"{name}\"" + f'Received "{name}"' ) elif not 1 <= len(name) <= 32: - error = ValidationError(f"Command names and options must be 1-32 characters long. Received \"{name}\"") - elif not name.lower() == name: # Can't use islower() as it fails if none of the chars can be lower. See #512. - error = ValidationError(f"Command names and options must be lowercase. Received \"{name}\"") + error = ValidationError(f'Command names and options must be 1-32 characters long. Received "{name}"') + elif name.lower() != name: # Can't use islower() as it fails if none of the chars can be lower. See #512. + error = ValidationError(f'Command names and options must be lowercase. Received "{name}"') if error: if locale: - error.args = (error.args[0]+f" in locale {locale}",) + error.args = (f"{error.args[0]} in locale {locale}",) raise error def validate_chat_input_description(description: Any, locale: Optional[str] = None): if locale is not None and locale not in valid_locales: raise ValidationError( - f"Locale '{locale}' is not a valid locale, " - f"see {docs}/reference#locales for list of supported locales." + f"Locale '{locale}' is not a valid locale, " f"see {docs}/reference#locales for list of supported locales." ) error = None if not isinstance(description, str): - error = TypeError(f"Command and option description must be of type str. Received \"{description}\"") + error = TypeError(f'Command and option description must be of type str. Received "{description}"') elif not 1 <= len(description) <= 100: - error = ValidationError(f"Command and option description must be 1-100 characters long. Received \"{description}\"") + error = ValidationError( + f'Command and option description must be 1-100 characters long. Received "{description}"' + ) if error: if locale: - error.args = (error.args[0]+f" in locale {locale}",) + error.args = (f"{error.args[0]} in locale {locale}",) raise error diff --git a/discord/commands/options.py b/discord/commands/options.py index 1cd0260e57..13156325fc 100644 --- a/discord/commands/options.py +++ b/discord/commands/options.py @@ -22,9 +22,11 @@ DEALINGS IN THE SOFTWARE. """ +import inspect from typing import Any, Dict, List, Literal, Optional, Union +from enum import Enum -from ..enums import ChannelType, SlashCommandOptionType +from ..enums import ChannelType, SlashCommandOptionType, Enum as DiscordEnum __all__ = ( "ThreadOption", @@ -80,12 +82,12 @@ async def hello( ---------- input_type: :class:`Any` The type of input that is expected for this option. - description: :class:`str` - The description of this option. - Must be 100 characters or fewer. 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. 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). @@ -115,18 +117,27 @@ async def hello( See `here `_ for a list of valid locales. """ - def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> None: + def __init__(self, input_type: Any = str, /, description: Optional[str] = None, **kwargs) -> None: self.name: Optional[str] = kwargs.pop("name", None) if self.name is not None: self.name = str(self.name) + self._parameter_name = self.name # default self.description = description or "No description provided" self.converter = None self._raw_type = input_type self.channel_types: List[ChannelType] = kwargs.pop("channel_types", []) + enum_choices = [] if not isinstance(input_type, SlashCommandOptionType): if hasattr(input_type, "convert"): self.converter = input_type input_type = SlashCommandOptionType.string + elif issubclass(input_type, (Enum, DiscordEnum)): + enum_choices = [OptionChoice(e.name, e.value) for e in input_type] + if len(enum_choices) != len([elem for elem in enum_choices if elem.value.__class__ == enum_choices[0].value.__class__]): + enum_choices = [OptionChoice(e.name, str(e.value)) for e in input_type] + input_type = SlashCommandOptionType.string + else: + input_type = SlashCommandOptionType.from_datatype(enum_choices[0].value.__class__) else: try: _type = SlashCommandOptionType.from_datatype(input_type) @@ -140,7 +151,10 @@ def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> Non else: if _type == SlashCommandOptionType.channel: if not isinstance(input_type, tuple): - input_type = (input_type,) + if hasattr(input_type, "__args__"): # Union + input_type = input_type.__args__ + else: + input_type = (input_type,) for i in input_type: if i.__name__ == "GuildChannel": continue @@ -154,10 +168,17 @@ def __init__(self, input_type: Any, /, description: str = None, **kwargs) -> Non self.input_type = input_type self.required: bool = kwargs.pop("required", True) if "default" not in kwargs else False self.default = kwargs.pop("default", None) - self.choices: List[OptionChoice] = [ + self.choices: List[OptionChoice] = enum_choices or [ o if isinstance(o, OptionChoice) else OptionChoice(o) for o in kwargs.pop("choices", list()) ] + if description is not None: + self.description = description + elif issubclass(self._raw_type, Enum) and (doc := inspect.getdoc(self._raw_type)) is not None: + self.description = doc + else: + self.description = "No description provided" + if self.input_type == SlashCommandOptionType.integer: minmax_types = (int, type(None)) elif self.input_type == SlashCommandOptionType.number: diff --git a/discord/commands/permissions.py b/discord/commands/permissions.py index c8a6c3480d..71d9132c5c 100644 --- a/discord/commands/permissions.py +++ b/discord/commands/permissions.py @@ -23,229 +23,85 @@ DEALINGS IN THE SOFTWARE. """ -from typing import Callable, Dict, Union +from typing import Callable + +from ..permissions import Permissions +from .core import ApplicationCommand __all__ = ( - "CommandPermission", - "has_role", - "has_any_role", - "is_user", - "is_owner", - "permission", + "default_permissions", + "guild_only", ) -class CommandPermission: - """The class used in the application command decorators - to hash permission data into a dictionary using the - :meth:`~to_dict` method to be sent to the discord API later on. - - .. versionadded:: 2.0 - - Attributes - ----------- - id: Union[:class:`int`, :class:`str`] - A string or integer that represents or helps get - the id of the user or role that the permission is tied to. - type: :class:`int` - An integer representing the type of the permission. - permission: :class:`bool` - A boolean representing the permission's value. - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. - """ - - def __init__( - self, - id: Union[int, str], - type: int, - permission: bool = True, - guild_id: int = None, - ): - self.id = id - self.type = type - self.permission = permission - self.guild_id = guild_id - - def to_dict(self) -> Dict[str, Union[int, bool]]: - return {"id": self.id, "type": self.type, "permission": self.permission} - - -def permission( - role_id: int = None, - user_id: int = None, - permission: bool = True, - guild_id: int = None, -): - """The method used to specify application command permissions - for specific users or roles using their id. - - This method is meant to be used as a decorator. - - .. versionadded:: 2.0 - - Parameters - ----------- - role_id: :class:`int` - An integer which represents the id of the role that the - permission may be tied to. - user_id: :class:`int` - An integer which represents the id of the user that the - permission may be tied to. - permission: :class:`bool` - A boolean representing the permission's value. - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. - """ - - def decorator(func: Callable): - if not role_id is None: - app_cmd_perm = CommandPermission(role_id, 1, permission, guild_id) - elif not user_id is None: - app_cmd_perm = CommandPermission(user_id, 2, permission, guild_id) - else: - raise ValueError("role_id or user_id must be specified!") - - # Create __app_cmd_perms__ - if not hasattr(func, "__app_cmd_perms__"): - func.__app_cmd_perms__ = [] - - # Append - func.__app_cmd_perms__.append(app_cmd_perm) - - return func - - return decorator - - -def has_role(item: Union[int, str], guild_id: int = None): - """The method used to specify application command role restrictions. - - This method is meant to be used as a decorator. - - .. versionadded:: 2.0 - - Parameters - ----------- - item: Union[:class:`int`, :class:`str`] - An integer or string that represent the id or name of the role - that the permission is tied to. - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. - """ - - def decorator(func: Callable): - # Create __app_cmd_perms__ - if not hasattr(func, "__app_cmd_perms__"): - func.__app_cmd_perms__ = [] - - # Permissions (Will Convert ID later in register_commands if needed) - app_cmd_perm = CommandPermission(item, 1, True, guild_id) # {"id": item, "type": 1, "permission": True} - - # Append - func.__app_cmd_perms__.append(app_cmd_perm) - - return func +def default_permissions(**perms: bool) -> Callable: + """A decorator that limits the usage of a slash command to members with certain + permissions. - return decorator + The permissions passed in must be exactly like the properties shown under + :class:`.discord.Permissions`. - -def has_any_role(*items: Union[int, str], guild_id: int = None): - """The method used to specify multiple application command role restrictions, - The application command runs if the invoker has **any** of the specified roles. - - This method is meant to be used as a decorator. - - .. versionadded:: 2.0 + .. 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 - ----------- - *items: Union[:class:`int`, :class:`str`] - The integers or strings that represent the ids or names of the roles - that the permission is tied to. - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. - """ - - def decorator(func: Callable): - # Create __app_cmd_perms__ - if not hasattr(func, "__app_cmd_perms__"): - func.__app_cmd_perms__ = [] + ------------ + perms + An argument list of permissions to check for. - # Permissions (Will Convert ID later in register_commands if needed) - for item in items: - app_cmd_perm = CommandPermission(item, 1, True, guild_id) # {"id": item, "type": 1, "permission": True} + Example + --------- - # Append - func.__app_cmd_perms__.append(app_cmd_perm) + .. code-block:: python3 - return func + from discord import default_permissions - return decorator + @bot.slash_command() + @default_permissions(manage_messages=True) + async def test(ctx): + await ctx.respond('You can manage messages.') - -def is_user(user: int, guild_id: int = None): - """The method used to specify application command user restrictions. - - This method is meant to be used as a decorator. - - .. versionadded:: 2.0 - - Parameters - ----------- - user: :class:`int` - An integer that represent the id of the user that the permission is tied to. - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. """ - def decorator(func: Callable): - # Create __app_cmd_perms__ - if not hasattr(func, "__app_cmd_perms__"): - func.__app_cmd_perms__ = [] + invalid = set(perms) - set(Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") - # Permissions (Will Convert ID later in register_commands if needed) - app_cmd_perm = CommandPermission(user, 2, True, guild_id) # {"id": user, "type": 2, "permission": True} + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + command.default_member_permissions = Permissions(**perms) + else: + command.__default_member_permissions__ = Permissions(**perms) + return command - # Append - func.__app_cmd_perms__.append(app_cmd_perm) + return inner - return func - return decorator +def guild_only() -> Callable: + """A decorator that limits the usage of a slash command to guild contexts. + The command won't be able to be used in private message channels. + Example + --------- -def is_owner(guild_id: int = None): - """The method used to limit application commands exclusively - to the owner of the bot. + .. code-block:: python3 - This method is meant to be used as a decorator. + from discord import guild_only - .. versionadded:: 2.0 + @bot.slash_command() + @guild_only() + async def test(ctx): + await ctx.respond('You\'re in a guild.') - Parameters - ----------- - guild_id: :class:`int` - The integer which represents the id of the guild that the - permission may be tied to. """ - def decorator(func: Callable): - # Create __app_cmd_perms__ - if not hasattr(func, "__app_cmd_perms__"): - func.__app_cmd_perms__ = [] - - # Permissions (Will Convert ID later in register_commands if needed) - app_cmd_perm = CommandPermission("owner", 2, True, guild_id) # {"id": "owner", "type": 2, "permission": True} - - # Append - func.__app_cmd_perms__.append(app_cmd_perm) - - return func + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + command.guild_only = True + else: + command.__guild_only__ = True + return command - return decorator + return inner diff --git a/discord/enums.py b/discord/enums.py index 4d9805e02e..9920d70a86 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -380,6 +380,7 @@ class AuditLogAction(Enum): thread_create = 110 thread_update = 111 thread_delete = 112 + application_command_permission_update = 121 @property def category(self) -> Optional[AuditLogActionCategory]: @@ -431,6 +432,7 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.thread_create: AuditLogActionCategory.create, AuditLogAction.thread_update: AuditLogActionCategory.update, AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.application_command_permission_update: AuditLogActionCategory.update, } return lookup[self] @@ -467,6 +469,8 @@ def target_type(self) -> Optional[str]: return "scheduled_event" elif v < 113: return "thread" + elif v < 121: + return "application_command_permission" class UserFlags(Enum): @@ -695,8 +699,12 @@ def from_datatype(cls, datatype): if issubclass(datatype, float): return cls.number - # TODO: Improve the error message - raise TypeError(f"Invalid class {datatype} used as an input type for an Option") + from .commands.context import ApplicationContext + + if not issubclass(datatype, ApplicationContext): # TODO: prevent ctx being passed here in cog commands + raise TypeError( + f"Invalid class {datatype} used as an input type for an Option" + ) # TODO: Improve the error message class EmbeddedActivity(Enum): diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index f0af1ff9da..f9aa6f7548 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -41,9 +41,8 @@ __all__ = ("BridgeCommand", "bridge_command", "BridgeExtCommand", "BridgeSlashCommand") -from ..commands.converter import _convert_to_bool - from ...utils import get +from ..commands.converter import _convert_to_bool class BridgeSlashCommand(SlashCommand): diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index b7c3a4ef5f..121ebdf438 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -25,31 +25,15 @@ from __future__ import annotations -import asyncio import collections import collections.abc -import importlib.util -import inspect import sys import traceback -import types -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - List, - Mapping, - Optional, - Type, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, TypeVar, Union import discord from . import errors -from .cog import Cog from .context import Context from .core import GroupMixin from .help import DefaultHelpCommand, HelpCommand diff --git a/discord/ext/commands/help.py b/discord/ext/commands/help.py index 27d219ccfa..200abd2452 100644 --- a/discord/ext/commands/help.py +++ b/discord/ext/commands/help.py @@ -25,10 +25,9 @@ import copy import functools -import inspect import itertools import re -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING import discord.utils diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index fcfdba6560..82fbc6f3a8 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -230,7 +230,7 @@ class PageGroup: label: :class:`str` The label shown on the corresponding PaginatorMenu dropdown option. Also used as the SelectOption value. - description: :class:`str` + description: Optional[:class:`str`] The description shown on the corresponding PaginatorMenu dropdown option. emoji: Union[:class:`str`, :class:`discord.Emoji`, :class:`discord.PartialEmoji`] The emoji shown on the corresponding PaginatorMenu dropdown option. @@ -264,7 +264,7 @@ def __init__( self, pages: Union[List[str], List[Page], List[Union[List[discord.Embed], discord.Embed]]], label: str, - description: str, + description: Optional[str] = None, emoji: Union[str, discord.Emoji, discord.PartialEmoji] = None, show_disabled: Optional[bool] = None, show_indicator: Optional[bool] = None, @@ -279,7 +279,7 @@ def __init__( trigger_on_display: Optional[bool] = None, ): self.label = label - self.description = description + self.description: Optional[str] = description self.emoji: Union[str, discord.Emoji, discord.PartialEmoji] = emoji self.pages: Union[List[str], List[Union[List[discord.Embed], discord.Embed]]] = pages self.show_disabled = show_disabled diff --git a/discord/guild.py b/discord/guild.py index 271c3a049f..41c6bac1f4 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1866,7 +1866,7 @@ async def active_threads(self) -> List[Thread]: return threads # TODO: Remove Optional typing here when async iterators are refactored - def fetch_members(self, *, limit: int = 1000, after: Optional[SnowflakeTime] = None) -> MemberIterator: + def fetch_members(self, *, limit: Optional[int] = 1000, after: Optional[SnowflakeTime] = None) -> MemberIterator: """Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this, :meth:`Intents.members` must be enabled. diff --git a/discord/http.py b/discord/http.py index ae6e667456..08def4cf1e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2288,19 +2288,34 @@ def bulk_upsert_guild_commands( ) return self.request(r, json=payload) - def bulk_upsert_command_permissions( + # Application commands (permissions) + + def get_command_permissions( self, application_id: Snowflake, guild_id: Snowflake, - payload: List[interactions.EditApplicationCommand], - ) -> Response[List[interactions.ApplicationCommand]]: + command_id: Snowflake, + ) -> Response[interactions.GuildApplicationCommandPermissions]: r = Route( - "PUT", + "GET", + "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions", + application_id=application_id, + guild_id=guild_id, + ) + return self.request(r) + + def get_guild_command_permissions( + self, + application_id: Snowflake, + guild_id: Snowflake, + ) -> Response[List[interactions.GuildApplicationCommandPermissions]]: + r = Route( + "GET", "/applications/{application_id}/guilds/{guild_id}/commands/permissions", application_id=application_id, guild_id=guild_id, ) - return self.request(r, json=payload) + return self.request(r) # Interaction responses diff --git a/discord/interactions.py b/discord/interactions.py index c964e5ca4f..0ab2f6c7c6 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -28,7 +28,7 @@ from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, Coroutine +from typing import TYPE_CHECKING, Any, Coroutine, Dict, List, Optional, Tuple, Union from . import utils from .channel import ChannelType, PartialMessageable diff --git a/discord/state.py b/discord/state.py index f115f0193f..c1571d9255 100644 --- a/discord/state.py +++ b/discord/state.py @@ -613,6 +613,10 @@ def parse_ready(self, data) -> None: def parse_resumed(self, data) -> None: self.dispatch("resumed") + def parse_application_command_permissions_update(self, data) -> None: + # unsure what the implementation would be like + pass + def parse_message_create(self, data) -> None: channel, _ = self._get_guild_channel(data) # channel would be the correct type here @@ -1572,7 +1576,6 @@ def parse_voice_state_update(self, data) -> None: coro = voice.on_voice_state_update(data) asyncio.create_task(logging_coroutine(coro, info="Voice Protocol voice state update handler")) - def parse_voice_server_update(self, data) -> None: try: key_id = int(data["guild_id"]) diff --git a/discord/threads.py b/discord/threads.py index 03cb327bac..e58668e2af 100644 --- a/discord/threads.py +++ b/discord/threads.py @@ -25,8 +25,6 @@ from __future__ import annotations -import asyncio -import time from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Union from .abc import Messageable, _purge_messages_helper diff --git a/discord/types/interactions.py b/discord/types/interactions.py index 8ce26f4a76..56cb9c1f5e 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -85,7 +85,7 @@ class ApplicationCommandOptionChoice(_ApplicationCommandOptionChoiceOptional): value: Union[str, int] -ApplicationCommandPermissionType = Literal[1, 2] +ApplicationCommandPermissionType = Literal[1, 2, 3] class ApplicationCommandPermissions(TypedDict): diff --git a/discord/ui/button.py b/discord/ui/button.py index ec4cfa589e..6f12a4d7da 100644 --- a/discord/ui/button.py +++ b/discord/ui/button.py @@ -96,9 +96,9 @@ def __init__( row: Optional[int] = None, ): super().__init__() - if label and len(label) > 80: + if label and len(str(label)) > 80: raise ValueError("label must be 80 characters or fewer") - if custom_id is not None and len(custom_id) > 100: + if custom_id is not None and len(str(custom_id)) > 100: raise ValueError("custom_id must be 100 characters or fewer") if custom_id is not None and url is not None: raise TypeError("cannot mix both url and custom_id with Button") @@ -184,7 +184,7 @@ def label(self) -> Optional[str]: @label.setter def label(self, value: Optional[str]): - if value and len(value) > 80: + if value and len(str(value)) > 80: raise ValueError("label must be 80 characters or fewer") self._underlying.label = str(value) if value is not None else value diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 0fe04679da..9cbae77dd3 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -63,15 +63,15 @@ def __init__( row: Optional[int] = None, ): super().__init__() - if len(label) > 45: + if len(str(label)) > 45: raise ValueError("label must be 45 characters or fewer") if min_length and (min_length < 0 or min_length > 4000): raise ValueError("min_length must be between 0 and 4000") if max_length and (max_length < 0 or max_length > 4000): raise ValueError("max_length must be between 1 and 4000") - if value and len(value) > 4000: + if value and len(str(value)) > 4000: raise ValueError("value must be 4000 characters or fewer") - if placeholder and len(placeholder) > 100: + if placeholder and len(str(placeholder)) > 100: raise ValueError("placeholder must be 100 characters or fewer") if not isinstance(custom_id, str) and custom_id is not None: raise TypeError(f"expected custom_id to be str, not {custom_id.__class__.__name__}") @@ -125,10 +125,10 @@ def label(self) -> str: @label.setter def label(self, value: str): - if len(value) > 45: - raise ValueError("label must be 45 characters or fewer") if not isinstance(value, str): raise TypeError(f"label should be str not {value.__class__.__name__}") + if len(value) > 45: + raise ValueError("label must be 45 characters or fewer") self._underlying.label = value @property @@ -193,7 +193,7 @@ def value(self) -> Optional[str]: def value(self, value: Optional[str]): if value and not isinstance(value, str): raise TypeError(f"value must be None or str not {value.__class__.__name__}") # type: ignore - if value and len(value) > 4000: + if value and len(str(value)) > 4000: raise ValueError("value must be 4000 characters or fewer") self._underlying.value = value diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 4e4f4201f9..0cb7d64cff 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -41,7 +41,7 @@ def __init__(self, title: str, custom_id: Optional[str] = None) -> None: if not isinstance(custom_id, str) and custom_id is not None: raise TypeError(f"expected custom_id to be str, not {custom_id.__class__.__name__}") self._custom_id: Optional[str] = custom_id or os.urandom(16).hex() - if len(title) > 45: + if len(str(title)) > 45: raise ValueError("title must be 45 characters or fewer") self._title = title self.children: List[InputText] = [] diff --git a/discord/ui/select.py b/discord/ui/select.py index 95b100fadd..a861108c38 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -149,10 +149,10 @@ def placeholder(self) -> Optional[str]: @placeholder.setter def placeholder(self, value: Optional[str]): - if value and len(value) > 150: - raise ValueError("placeholder must be 150 characters or fewer") if value is not None and not isinstance(value, str): raise TypeError("placeholder must be None or str") + if value and len(value) > 150: + raise ValueError("placeholder must be 150 characters or fewer") self._underlying.placeholder = value diff --git a/discord/ui/view.py b/discord/ui/view.py index 70e7c675dc..6b3fec5a5a 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -470,7 +470,7 @@ def disable_all_items(self, *, exclusions: Optional[List[Item]] = None) -> None: Disables all items in the view. Parameters - ----------- + ----------- exclusions: Optional[List[:class:`ui.Item`]] A list of items in `self.children` to not disable from the view. """ @@ -478,6 +478,19 @@ def disable_all_items(self, *, exclusions: Optional[List[Item]] = None) -> None: if exclusions is None or child not in exclusions: child.disabled = True + def enable_all_items(self, *, exclusions: Optional[List[Item]] = None) -> None: + """ + Enables all items in the view. + + Parameters + ----------- + exclusions: Optional[List[:class:`ui.Item`]] + A list of items in `self.children` to not enable from the view. + """ + for child in self.children: + if exclusions is None or child not in exclusions: + child.disabled = False + class ViewStore: def __init__(self, state: ConnectionState): diff --git a/docs/api.rst b/docs/api.rst index 7be1558187..7c6648a8b3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -96,6 +96,18 @@ AutoShardedBot Application Commands --------------------- + +Command Permission Decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +.. autofunction:: discord.commands.default_permissions + :decorator: + +.. autofunction:: discord.commands.guild_only + :decorator: + + ApplicationCommand ~~~~~~~~~~~~~~~~~~~ @@ -104,10 +116,10 @@ ApplicationCommand .. autoclass:: ApplicationCommand :members: -.. autofunction:: discord.commands.core.application_command +.. autofunction:: discord.commands.application_command :decorator: -.. autofunction:: discord.commands.core.command +.. autofunction:: discord.commands.command :decorator: SlashCommand @@ -118,7 +130,7 @@ SlashCommand .. autoclass:: SlashCommand :members: -.. autofunction:: discord.commands.core.slash_command +.. autofunction:: discord.commands.slash_command :decorator: SlashCommandGroup @@ -137,7 +149,7 @@ Option .. autoclass:: Option :members: -.. autofunction:: discord.commands.core.Option +.. autofunction:: discord.commands.Option :decorator: OptionChoice @@ -156,7 +168,7 @@ UserCommand .. autoclass:: UserCommand :members: -.. autofunction:: discord.commands.core.user_command +.. autofunction:: discord.commands.user_command :decorator: MessageCommand @@ -167,7 +179,7 @@ MessageCommand .. autoclass:: MessageCommand :members: -.. autofunction:: discord.commands.core.message_command +.. autofunction:: discord.commands.message_command :decorator: ApplicationContext @@ -186,29 +198,6 @@ AutocompleteContext .. autoclass:: AutocompleteContext :members: -CommandPermission -~~~~~~~~~~~~~~~~~ - -.. attributetable:: CommandPermission - -.. autoclass:: CommandPermission - :members: - -.. autofunction:: discord.commands.permissions.permission - :decorator: - -.. autofunction:: discord.commands.permissions.has_role - :decorator: - -.. autofunction:: discord.commands.permissions.has_any_role - :decorator: - -.. autofunction:: discord.commands.permissions.is_user - :decorator: - -.. autofunction:: discord.commands.permissions.is_owner - :decorator: - Cogs ----- @@ -2865,6 +2854,21 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.invitable` .. versionadded:: 2.0 + + .. attribute:: application_command_permission_update + + An application command's permissions were updated. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + an :class:`Object` with the ID of the command that + had it's permissions edited. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.command_id` + + .. versionadded:: 2.0 + .. class:: AuditLogActionCategory @@ -3927,6 +3931,12 @@ AuditLogDiff Non-moderators can now add other non-moderators to this thread. :type: :class:`bool` + + .. attribute:: command_id + + This command's permissions were updated. + + :type: :class:`int` .. this is currently missing the following keys: reason and application_id I'm not sure how to about porting these diff --git a/examples/app_commands/slash_autocomplete.py b/examples/app_commands/slash_autocomplete.py index 84327766ce..2703a2ada0 100644 --- a/examples/app_commands/slash_autocomplete.py +++ b/examples/app_commands/slash_autocomplete.py @@ -127,9 +127,9 @@ async def get_animals(ctx: discord.AutocompleteContext): @option("color", description="Pick a color!", autocomplete=get_colors) @option("animal", description="Pick an animal!", autocomplete=get_animals) async def autocomplete_example( - ctx: discord.ApplicationContext, - color: str, - animal: str, + ctx: discord.ApplicationContext, + color: str, + animal: str, ): """Demonstrates using ctx.options to create options that are dependent on the values of other options. For the `color` option, a callback is passed, where additional logic can be added to determine which values are returned. @@ -151,9 +151,9 @@ async def autocomplete_example( # Demonstrates passing a static iterable discord.utils.basic_autocomplete ) async def autocomplete_basic_example( - ctx: discord.ApplicationContext, - color: str, - animal: str, + ctx: discord.ApplicationContext, + color: str, + animal: str, ): """This demonstrates using the discord.utils.basic_autocomplete helper function. For the `color` option, a callback is passed, where additional logic can be added to determine which values are returned. diff --git a/examples/app_commands/slash_options.py b/examples/app_commands/slash_options.py index 56f6bf923a..469fefadaa 100644 --- a/examples/app_commands/slash_options.py +++ b/examples/app_commands/slash_options.py @@ -25,10 +25,10 @@ # age: Option(int, "Enter your age") = 18 ) async def hello( - ctx: discord.ApplicationContext, - name: str, - gender: str, - age: str, + ctx: discord.ApplicationContext, + name: str, + gender: str, + age: str, ): await ctx.respond(f"Hello {name}! Your gender is {gender} and you are {age} years old.") @@ -38,11 +38,11 @@ async def hello( "channel", [discord.TextChannel, discord.VoiceChannel], # you can specify allowed channel types by passing a list of them like this - description="Select a channel" + description="Select a channel", ) async def channel( - ctx: discord.ApplicationContext, - channel: Union[discord.TextChannel, discord.VoiceChannel], + ctx: discord.ApplicationContext, + channel: Union[discord.TextChannel, discord.VoiceChannel], ): await ctx.respond(f"Hi! You selected {channel.mention} channel.") @@ -50,8 +50,8 @@ async def channel( @bot.slash_command(name="attach_file") @option("attachment", discord.Attachment, description="A file to attach to the message", required=False) async def say( - ctx: discord.ApplicationContext, - attachment: discord.Attachment, + ctx: discord.ApplicationContext, + attachment: discord.Attachment, ): """This demonstrates how to attach a file with a slash command.""" file = await attachment.to_file() diff --git a/examples/audio_recording.py b/examples/audio_recording.py index 5de421a1a8..54e65e1eb2 100644 --- a/examples/audio_recording.py +++ b/examples/audio_recording.py @@ -8,20 +8,20 @@ @bot.command() -@option("encoding", choices=[ - "mp3", - "wav", - "pcm", - "ogg", - "mka", - "mkv", - "mp4", - "m4a", -]) -async def start( - ctx: ApplicationContext, - encoding: str -): +@option( + "encoding", + choices=[ + "mp3", + "wav", + "pcm", + "ogg", + "mka", + "mkv", + "mp4", + "m4a", + ], +) +async def start(ctx: ApplicationContext, encoding: str): """ Record your voice! """ diff --git a/requirements-dev.txt b/requirements-dev.txt index 350c9dcff4..7a3e85b834 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -r requirements.txt -pylint~=2.13.7 +pylint~=2.13.8 pytest~=7.1.2 pytest-asyncio~=0.18.3 # pytest-order~=1.0.1