# SPDX-License-Identifier: MIT from __future__ import annotations import math import re from abc import ABC from typing import TYPE_CHECKING, ClassVar, Dict, List, Mapping, Optional, Tuple, Union from .enums import ( ApplicationCommandPermissionType, ApplicationCommandType, ChannelType, Locale, OptionType, enum_if_int, try_enum, try_enum_to_int, ) from .i18n import Localized from .permissions import Permissions from .utils import MISSING, _get_as_snowflake, _maybe_cast if TYPE_CHECKING: from typing_extensions import Self from .i18n import LocalizationProtocol, LocalizationValue, LocalizedOptional, LocalizedRequired from .state import ConnectionState from .types.interactions import ( ApplicationCommand as ApplicationCommandPayload, ApplicationCommandOption as ApplicationCommandOptionPayload, ApplicationCommandOptionChoice as ApplicationCommandOptionChoicePayload, ApplicationCommandOptionChoiceValue, ApplicationCommandPermissions as ApplicationCommandPermissionsPayload, EditApplicationCommand as EditApplicationCommandPayload, GuildApplicationCommandPermissions as GuildApplicationCommandPermissionsPayload, ) Choices = Union[ List["OptionChoice"], List[ApplicationCommandOptionChoiceValue], Dict[str, ApplicationCommandOptionChoiceValue], List[Localized[str]], ] APIApplicationCommand = Union["APIUserCommand", "APIMessageCommand", "APISlashCommand"] __all__ = ( "application_command_factory", "ApplicationCommand", "SlashCommand", "APISlashCommand", "UserCommand", "APIUserCommand", "MessageCommand", "APIMessageCommand", "OptionChoice", "Option", "ApplicationCommandPermissions", "GuildApplicationCommandPermissions", ) def application_command_factory(data: ApplicationCommandPayload) -> APIApplicationCommand: cmd_type = try_enum(ApplicationCommandType, data.get("type", 1)) if cmd_type is ApplicationCommandType.chat_input: return APISlashCommand.from_dict(data) if cmd_type is ApplicationCommandType.user: return APIUserCommand.from_dict(data) if cmd_type is ApplicationCommandType.message: return APIMessageCommand.from_dict(data) raise TypeError(f"Application command of type {cmd_type} is not valid") def _validate_name(name: str) -> None: # used for slash command names and option names # see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming if not isinstance(name, str): raise TypeError( f"Slash command name and option names must be an instance of class 'str', received '{name.__class__}'" ) if name != name.lower() or not re.fullmatch(r"[\w-]{1,32}", name): raise ValueError( f"Slash command or option name '{name}' should be lowercase, " "between 1 and 32 characters long, and only consist of " "these symbols: a-z, 0-9, -, _, and other languages'/scripts' symbols" ) class OptionChoice: """Represents an option choice. Parameters ---------- name: Union[:class:`str`, :class:`.Localized`] The name of the option choice (visible to users). .. versionchanged:: 2.5 Added support for localizations. value: Union[:class:`str`, :class:`int`] The value of the option choice. """ def __init__( self, name: LocalizedRequired, value: ApplicationCommandOptionChoiceValue, ) -> None: name_loc = Localized._cast(name, True) self.name: str = name_loc.string self.name_localizations: LocalizationValue = name_loc.localizations self.value: ApplicationCommandOptionChoiceValue = value def __repr__(self) -> str: return f"" def __eq__(self, other) -> bool: return ( self.name == other.name and self.value == other.value and self.name_localizations == other.name_localizations ) def to_dict(self, *, locale: Optional[Locale] = None) -> ApplicationCommandOptionChoicePayload: localizations = self.name_localizations.data name: Optional[str] = None # if `locale` provided, get localized name from dict if locale is not None and localizations: name = localizations.get(str(locale)) # fall back to default name if no locale or no localized name if name is None: name = self.name payload: ApplicationCommandOptionChoicePayload = { "name": name, "value": self.value, } # if no `locale` provided, include all localizations in payload if locale is None and localizations: payload["name_localizations"] = localizations return payload @classmethod def from_dict(cls, data: ApplicationCommandOptionChoicePayload): return OptionChoice( name=Localized(data["name"], data=data.get("name_localizations")), value=data["value"], ) def localize(self, store: LocalizationProtocol) -> None: self.name_localizations._link(store) class Option: """Represents a slash command option. Parameters ---------- name: Union[:class:`str`, :class:`.Localized`] The option's name. .. versionchanged:: 2.5 Added support for localizations. description: Optional[Union[:class:`str`, :class:`.Localized`]] The option's description. .. versionchanged:: 2.5 Added support for localizations. type: :class:`OptionType` The option type, e.g. :class:`OptionType.user`. required: :class:`bool` Whether this option is required. choices: Union[List[:class:`OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]] The list of option choices. options: List[:class:`Option`] The list of sub options. Normally you don't have to specify it directly, instead consider using ``@main_cmd.sub_command`` or ``@main_cmd.sub_command_group`` decorators. channel_types: List[:class:`ChannelType`] The list of channel types that your option supports, if the type is :class:`OptionType.channel`. By default, it supports all channel types. autocomplete: :class:`bool` Whether this option can be autocompleted. min_value: Union[:class:`int`, :class:`float`] The minimum value permitted. max_value: Union[:class:`int`, :class:`float`] The maximum value permitted. min_length: :class:`int` The minimum length for this option if this is a string option. .. versionadded:: 2.6 max_length: :class:`int` The maximum length for this option if this is a string option. .. versionadded:: 2.6 """ __slots__ = ( "name", "description", "type", "required", "choices", "options", "channel_types", "autocomplete", "min_value", "max_value", "name_localizations", "description_localizations", "min_length", "max_length", ) def __init__( self, name: LocalizedRequired, description: LocalizedOptional = None, type: Optional[Union[OptionType, int]] = None, required: bool = False, choices: Optional[Choices] = None, options: Optional[list] = None, channel_types: Optional[List[ChannelType]] = None, autocomplete: bool = False, min_value: Optional[float] = None, max_value: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, ) -> None: name_loc = Localized._cast(name, True) _validate_name(name_loc.string) self.name: str = name_loc.string self.name_localizations: LocalizationValue = name_loc.localizations desc_loc = Localized._cast(description, False) self.description: str = desc_loc.string or "-" self.description_localizations: LocalizationValue = desc_loc.localizations self.type: OptionType = enum_if_int(OptionType, type) or OptionType.string self.required: bool = required self.options: List[Option] = options or [] if min_value and self.type is OptionType.integer: min_value = math.ceil(min_value) if max_value and self.type is OptionType.integer: max_value = math.floor(max_value) self.min_value: Optional[float] = min_value self.max_value: Optional[float] = max_value self.min_length: Optional[int] = min_length self.max_length: Optional[int] = max_length if channel_types is not None and not all(isinstance(t, ChannelType) for t in channel_types): raise TypeError("channel_types must be a list of `ChannelType`s") self.channel_types: List[ChannelType] = channel_types or [] self.choices: List[OptionChoice] = [] if choices is not None: if autocomplete: raise TypeError("can not specify both choices and autocomplete args") if isinstance(choices, Mapping): self.choices = [OptionChoice(name, value) for name, value in choices.items()] else: for c in choices: if isinstance(c, Localized): c = OptionChoice(c, c.string) elif not isinstance(c, OptionChoice): c = OptionChoice(str(c), c) self.choices.append(c) self.autocomplete: bool = autocomplete def __repr__(self) -> str: return ( f"