From 4f6a371e794b42f4d6d2b327ccdeaa868297eab9 Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sat, 28 Dec 2024 16:39:42 +0100 Subject: [PATCH 1/7] fix(typing): improve view type inference of ui decorators (#1190) --- changelog/1190.feature.rst | 1 + disnake/ui/button.py | 32 +++++++++------------ disnake/ui/item.py | 31 +++++++-------------- disnake/ui/select/base.py | 20 +++++-------- disnake/ui/select/channel.py | 22 +++++++-------- disnake/ui/select/mentionable.py | 24 ++++++++-------- disnake/ui/select/role.py | 22 +++++++-------- disnake/ui/select/string.py | 23 ++++++++------- disnake/ui/select/user.py | 22 +++++++-------- disnake/ui/view.py | 8 +++--- pyproject.toml | 3 +- tests/ui/test_decorators.py | 48 +++++++++++++------------------- 12 files changed, 113 insertions(+), 143 deletions(-) create mode 100644 changelog/1190.feature.rst diff --git a/changelog/1190.feature.rst b/changelog/1190.feature.rst new file mode 100644 index 0000000000..6fd323a472 --- /dev/null +++ b/changelog/1190.feature.rst @@ -0,0 +1 @@ +The ``cls`` parameter of UI component decorators (such as :func:`ui.button`) now accepts any matching callable, in addition to item subclasses. diff --git a/disnake/ui/button.py b/disnake/ui/button.py index 9995013ebb..bfcccb663f 100644 --- a/disnake/ui/button.py +++ b/disnake/ui/button.py @@ -10,10 +10,8 @@ Callable, Optional, Tuple, - Type, TypeVar, Union, - get_origin, overload, ) @@ -21,7 +19,7 @@ from ..enums import ButtonStyle, ComponentType from ..partial_emoji import PartialEmoji, _EmojiTag from ..utils import MISSING -from .item import DecoratedItem, Item, ItemShape +from .item import DecoratedItem, Item __all__ = ( "Button", @@ -263,20 +261,20 @@ def button( style: ButtonStyle = ButtonStyle.secondary, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[Button[V_co]]], DecoratedItem[Button[V_co]]]: +) -> Callable[[ItemCallbackType[V_co, Button[V_co]]], DecoratedItem[Button[V_co]]]: ... @overload def button( - cls: Type[ItemShape[B_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[B_co]], DecoratedItem[B_co]]: + cls: Callable[P, B_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, B_co]], DecoratedItem[B_co]]: ... def button( - cls: Type[ItemShape[B_co, ...]] = Button[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[B_co]], DecoratedItem[B_co]]: + cls: Callable[..., B_co] = Button[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, B_co]], DecoratedItem[B_co]]: """A decorator that attaches a button to a component. The function being decorated should have three parameters, ``self`` representing @@ -293,13 +291,12 @@ def button( Parameters ---------- - cls: Type[:class:`Button`] - The button subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`Button`] + A callable (may be a :class:`Button` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. .. versionadded:: 2.6 - label: Optional[:class:`str`] The label of the button, if any. custom_id: Optional[:class:`str`] @@ -319,13 +316,10 @@ def button( For example, row=1 will show up before row=2. Defaults to ``None``, which is automatic ordering. The row number must be between 0 and 4 (i.e. zero indexed). """ - if (origin := get_origin(cls)) is not None: - cls = origin - - if not isinstance(cls, type) or not issubclass(cls, Button): - raise TypeError(f"cls argument must be a subclass of Button, got {cls!r}") + if not callable(cls): + raise TypeError("cls argument must be callable") - def decorator(func: ItemCallbackType[B_co]) -> DecoratedItem[B_co]: + def decorator(func: ItemCallbackType[V_co, B_co]) -> DecoratedItem[B_co]: if not asyncio.iscoroutinefunction(func): raise TypeError("button function must be a coroutine function") diff --git a/disnake/ui/item.py b/disnake/ui/item.py index c4d29c6417..284e839378 100644 --- a/disnake/ui/item.py +++ b/disnake/ui/item.py @@ -12,17 +12,18 @@ Optional, Protocol, Tuple, + Type, TypeVar, overload, ) __all__ = ("Item", "WrappedComponent") -ItemT = TypeVar("ItemT", bound="Item") +I = TypeVar("I", bound="Item[Any]") V_co = TypeVar("V_co", bound="Optional[View]", covariant=True) if TYPE_CHECKING: - from typing_extensions import ParamSpec, Self + from typing_extensions import Self from ..client import Client from ..components import NestedComponent @@ -31,7 +32,7 @@ from ..types.components import Component as ComponentPayload from .view import View - ItemCallbackType = Callable[[Any, ItemT, MessageInteraction], Coroutine[Any, Any, Any]] + ItemCallbackType = Callable[[V_co, I, MessageInteraction], Coroutine[Any, Any, Any]] else: ParamSpec = TypeVar @@ -160,29 +161,17 @@ async def callback(self, interaction: MessageInteraction[ClientT], /) -> None: pass -I_co = TypeVar("I_co", bound=Item, covariant=True) +SelfViewT = TypeVar("SelfViewT", bound="Optional[View]") -# while the decorators don't actually return a descriptor that matches this protocol, +# While the decorators don't actually return a descriptor that matches this protocol, # this protocol ensures that type checkers don't complain about statements like `self.button.disabled = True`, -# which work as `View.__init__` replaces the handler with the item -class DecoratedItem(Protocol[I_co]): +# which work as `View.__init__` replaces the handler with the item. +class DecoratedItem(Protocol[I]): @overload - def __get__(self, obj: None, objtype: Any) -> ItemCallbackType: + def __get__(self, obj: None, objtype: Type[SelfViewT]) -> ItemCallbackType[SelfViewT, I]: ... @overload - def __get__(self, obj: Any, objtype: Any) -> I_co: - ... - - -T_co = TypeVar("T_co", covariant=True) -P = ParamSpec("P") - - -class ItemShape(Protocol[T_co, P]): - def __new__(cls) -> T_co: - ... - - def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: + def __get__(self, obj: Any, objtype: Any) -> I: ... diff --git a/disnake/ui/select/base.py b/disnake/ui/select/base.py index 912a24ba1f..10cae4f4c9 100644 --- a/disnake/ui/select/base.py +++ b/disnake/ui/select/base.py @@ -7,7 +7,6 @@ from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, - Any, Callable, ClassVar, Generic, @@ -19,14 +18,13 @@ Type, TypeVar, Union, - get_origin, ) from ...components import AnySelectMenu, SelectDefaultValue from ...enums import ComponentType, SelectDefaultValueType from ...object import Object from ...utils import MISSING, humanize_list -from ..item import DecoratedItem, Item, ItemShape +from ..item import DecoratedItem, Item __all__ = ("BaseSelect",) @@ -239,24 +237,20 @@ def _transform_default_values( def _create_decorator( - cls: Type[ItemShape[S_co, P]], - # only for input validation - base_cls: Type[BaseSelect[Any, Any, Any]], + # FIXME(3.0): rename `cls` parameter to more closely represent any callable argument type + cls: Callable[P, S_co], /, *args: P.args, **kwargs: P.kwargs, -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: if args: # the `*args` def above is just to satisfy the typechecker raise RuntimeError("expected no *args") - if (origin := get_origin(cls)) is not None: - cls = origin + if not callable(cls): + raise TypeError("cls argument must be callable") - if not isinstance(cls, type) or not issubclass(cls, base_cls): - raise TypeError(f"cls argument must be a subclass of {base_cls.__name__}, got {cls!r}") - - def decorator(func: ItemCallbackType[S_co]) -> DecoratedItem[S_co]: + def decorator(func: ItemCallbackType[V_co, S_co]) -> DecoratedItem[S_co]: if not asyncio.iscoroutinefunction(func): raise TypeError("select function must be a coroutine function") diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py index f004308482..f27c7a2107 100644 --- a/disnake/ui/select/channel.py +++ b/disnake/ui/select/channel.py @@ -30,7 +30,7 @@ from typing_extensions import Self from ...abc import AnyChannel - from ..item import DecoratedItem, ItemCallbackType, ItemShape + from ..item import DecoratedItem, ItemCallbackType __all__ = ( @@ -197,20 +197,20 @@ def channel_select( channel_types: Optional[List[ChannelType]] = None, default_values: Optional[Sequence[SelectDefaultValueInputType[AnyChannel]]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]]: +) -> Callable[[ItemCallbackType[V_co, ChannelSelect[V_co]]], DecoratedItem[ChannelSelect[V_co]]]: ... @overload def channel_select( - cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ... def channel_select( - cls: Type[ItemShape[S_co, ...]] = ChannelSelect[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[..., S_co] = ChannelSelect[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: """A decorator that attaches a channel select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -224,10 +224,10 @@ def channel_select( Parameters ---------- - cls: Type[:class:`ChannelSelect`] - The select subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`ChannelSelect`] + A callable (may be a :class:`ChannelSelect` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -256,4 +256,4 @@ def channel_select( .. versionadded:: 2.10 """ - return _create_decorator(cls, ChannelSelect, **kwargs) + return _create_decorator(cls, **kwargs) diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py index e98dfb29c9..1cc0be5b8a 100644 --- a/disnake/ui/select/mentionable.py +++ b/disnake/ui/select/mentionable.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ..item import DecoratedItem, ItemCallbackType, ItemShape + from ..item import DecoratedItem, ItemCallbackType __all__ = ( @@ -174,20 +174,22 @@ def mentionable_select( Sequence[SelectDefaultValueMultiInputType[Union[User, Member, Role]]] ] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[MentionableSelect[V_co]]], DecoratedItem[MentionableSelect[V_co]]]: +) -> Callable[ + [ItemCallbackType[V_co, MentionableSelect[V_co]]], DecoratedItem[MentionableSelect[V_co]] +]: ... @overload def mentionable_select( - cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ... def mentionable_select( - cls: Type[ItemShape[S_co, ...]] = MentionableSelect[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[..., S_co] = MentionableSelect[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: """A decorator that attaches a mentionable (user/member/role) select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -201,10 +203,10 @@ def mentionable_select( Parameters ---------- - cls: Type[:class:`MentionableSelect`] - The select subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`MentionableSelect`] + A callable (may be a :class:`MentionableSelect` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -232,4 +234,4 @@ def mentionable_select( .. versionadded:: 2.10 """ - return _create_decorator(cls, MentionableSelect, **kwargs) + return _create_decorator(cls, **kwargs) diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py index 4cb886168f..439749a136 100644 --- a/disnake/ui/select/role.py +++ b/disnake/ui/select/role.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ..item import DecoratedItem, ItemCallbackType, ItemShape + from ..item import DecoratedItem, ItemCallbackType __all__ = ( @@ -161,20 +161,20 @@ def role_select( disabled: bool = False, default_values: Optional[Sequence[SelectDefaultValueInputType[Role]]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[RoleSelect[V_co]]], DecoratedItem[RoleSelect[V_co]]]: +) -> Callable[[ItemCallbackType[V_co, RoleSelect[V_co]]], DecoratedItem[RoleSelect[V_co]]]: ... @overload def role_select( - cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ... def role_select( - cls: Type[ItemShape[S_co, ...]] = RoleSelect[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[..., S_co] = RoleSelect[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: """A decorator that attaches a role select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -188,10 +188,10 @@ def role_select( Parameters ---------- - cls: Type[:class:`RoleSelect`] - The select subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`RoleSelect`] + A callable (may be a :class:`RoleSelect` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -217,4 +217,4 @@ def role_select( .. versionadded:: 2.10 """ - return _create_decorator(cls, RoleSelect, **kwargs) + return _create_decorator(cls, **kwargs) diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py index 3b12d80388..b336dfa388 100644 --- a/disnake/ui/select/string.py +++ b/disnake/ui/select/string.py @@ -29,7 +29,7 @@ from ...emoji import Emoji from ...partial_emoji import PartialEmoji - from ..item import DecoratedItem, ItemCallbackType, ItemShape + from ..item import DecoratedItem, ItemCallbackType __all__ = ( @@ -265,20 +265,20 @@ def string_select( options: SelectOptionInput = ..., disabled: bool = False, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[StringSelect[V_co]]], DecoratedItem[StringSelect[V_co]]]: +) -> Callable[[ItemCallbackType[V_co, StringSelect[V_co]]], DecoratedItem[StringSelect[V_co]]]: ... @overload def string_select( - cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ... def string_select( - cls: Type[ItemShape[S_co, ...]] = StringSelect[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[..., S_co] = StringSelect[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: """A decorator that attaches a string select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -293,13 +293,12 @@ def string_select( Parameters ---------- - cls: Type[:class:`StringSelect`] - The select subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`StringSelect`] + A callable (may be a :class:`StringSelect` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. .. versionadded:: 2.6 - placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -329,7 +328,7 @@ def string_select( disabled: :class:`bool` Whether the select is disabled. Defaults to ``False``. """ - return _create_decorator(cls, StringSelect, **kwargs) + return _create_decorator(cls, **kwargs) select = string_select # backwards compatibility diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py index 9ab9b803ce..2dd20d40f6 100644 --- a/disnake/ui/select/user.py +++ b/disnake/ui/select/user.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from typing_extensions import Self - from ..item import DecoratedItem, ItemCallbackType, ItemShape + from ..item import DecoratedItem, ItemCallbackType __all__ = ( @@ -163,20 +163,20 @@ def user_select( disabled: bool = False, default_values: Optional[Sequence[SelectDefaultValueInputType[Union[User, Member]]]] = None, row: Optional[int] = None, -) -> Callable[[ItemCallbackType[UserSelect[V_co]]], DecoratedItem[UserSelect[V_co]]]: +) -> Callable[[ItemCallbackType[V_co, UserSelect[V_co]]], DecoratedItem[UserSelect[V_co]]]: ... @overload def user_select( - cls: Type[ItemShape[S_co, P]], *_: P.args, **kwargs: P.kwargs -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[P, S_co], *_: P.args, **kwargs: P.kwargs +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: ... def user_select( - cls: Type[ItemShape[S_co, ...]] = UserSelect[Any], **kwargs: Any -) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]: + cls: Callable[..., S_co] = UserSelect[Any], **kwargs: Any +) -> Callable[[ItemCallbackType[V_co, S_co]], DecoratedItem[S_co]]: """A decorator that attaches a user select menu to a component. The function being decorated should have three parameters, ``self`` representing @@ -190,10 +190,10 @@ def user_select( Parameters ---------- - cls: Type[:class:`UserSelect`] - The select subclass to create an instance of. If provided, the following parameters - described below do not apply. Instead, this decorator will accept the same keywords - as the passed cls does. + cls: Callable[..., :class:`UserSelect`] + A callable (may be a :class:`UserSelect` subclass) to create a new instance of this component. + If provided, the other parameters described below do not apply. + Instead, this decorator will accept the same keywords as the passed callable/class does. placeholder: Optional[:class:`str`] The placeholder text that is shown if nothing is selected, if any. custom_id: :class:`str` @@ -219,4 +219,4 @@ def user_select( .. versionadded:: 2.10 """ - return _create_decorator(cls, UserSelect, **kwargs) + return _create_decorator(cls, **kwargs) diff --git a/disnake/ui/view.py b/disnake/ui/view.py index 71c2965074..ffaa90fa3c 100644 --- a/disnake/ui/view.py +++ b/disnake/ui/view.py @@ -153,10 +153,10 @@ class View: """ __discord_ui_view__: ClassVar[bool] = True - __view_children_items__: ClassVar[List[ItemCallbackType[Item]]] = [] + __view_children_items__: ClassVar[List[ItemCallbackType[Self, Item[Self]]]] = [] def __init_subclass__(cls) -> None: - children: List[ItemCallbackType[Item]] = [] + children: List[ItemCallbackType[Self, Item[Self]]] = [] for base in reversed(cls.__mro__): for member in base.__dict__.values(): if hasattr(member, "__discord_ui_model_type__"): @@ -169,9 +169,9 @@ def __init_subclass__(cls) -> None: def __init__(self, *, timeout: Optional[float] = 180.0) -> None: self.timeout = timeout - self.children: List[Item] = [] + self.children: List[Item[Self]] = [] for func in self.__view_children_items__: - item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) + item: Item[Self] = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__) item.callback = partial(func, self, item) item._view = self setattr(self, func.__name__, item) diff --git a/pyproject.toml b/pyproject.toml index ed9467bfec..777507fb7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,6 +213,8 @@ ignore = [ "S311", # insecure RNG usage, we don't use these for security-related things "PLE0237", # pyright seems to catch this already + "E741", # ambiguous variable names + # temporary disables, to fix later "D205", # blank line required between summary and description "D401", # first line of docstring should be in imperative mood @@ -248,7 +250,6 @@ ignore = [ "T201", # print found, printing is okay in examples ] "examples/basic_voice.py" = ["S104"] # possible binding to all interfaces -"examples/views/tic_tac_toe.py" = ["E741"] # ambigious variable name: `O` [tool.ruff.lint.isort] combine-as-imports = true diff --git a/tests/ui/test_decorators.py b/tests/ui/test_decorators.py index e9c3680873..7dbe6aa488 100644 --- a/tests/ui/test_decorators.py +++ b/tests/ui/test_decorators.py @@ -9,12 +9,15 @@ from disnake import ui from disnake.ui.button import V_co -T = TypeVar("T", bound=ui.Item) +V = TypeVar("V", bound=ui.View) +I = TypeVar("I", bound=ui.Item) @contextlib.contextmanager -def create_callback(item_type: Type[T]) -> Iterator["ui.item.ItemCallbackType[T]"]: - async def callback(self, item, inter) -> None: +def create_callback( + view_type: Type[V], item_type: Type[I] +) -> Iterator["ui.item.ItemCallbackType[V, I]"]: + async def callback(self: V, item: I, inter) -> None: pytest.fail("callback should not be invoked") yield callback @@ -28,33 +31,36 @@ def __init__(self, *, param: float = 42.0) -> None: pass +class _CustomView(ui.View): + ... + + class TestDecorator: def test_default(self) -> None: - with create_callback(ui.Button[ui.View]) as func: + with create_callback(_CustomView, ui.Button[ui.View]) as func: res = ui.button(custom_id="123")(func) - assert_type(res, ui.item.DecoratedItem[ui.Button[ui.View]]) + assert_type(res, ui.item.DecoratedItem[ui.Button[_CustomView]]) - assert func.__discord_ui_model_type__ is ui.Button + assert func.__discord_ui_model_type__ is ui.Button[Any] assert func.__discord_ui_model_kwargs__ == {"custom_id": "123"} - with create_callback(ui.StringSelect[ui.View]) as func: + with create_callback(_CustomView, ui.StringSelect[ui.View]) as func: res = ui.string_select(custom_id="123")(func) - assert_type(res, ui.item.DecoratedItem[ui.StringSelect[ui.View]]) + assert_type(res, ui.item.DecoratedItem[ui.StringSelect[_CustomView]]) - assert func.__discord_ui_model_type__ is ui.StringSelect + assert func.__discord_ui_model_type__ is ui.StringSelect[Any] assert func.__discord_ui_model_kwargs__ == {"custom_id": "123"} # from here on out we're mostly only testing the button decorator, # as @ui.string_select etc. works identically @pytest.mark.parametrize("cls", [_CustomButton, _CustomButton[Any]]) - def test_cls(self, cls: Type[_CustomButton]) -> None: - with create_callback(cls) as func: + def test_cls(self, cls: Type[_CustomButton[ui.View]]) -> None: + with create_callback(_CustomView, cls) as func: res = ui.button(cls=cls, param=1337)(func) assert_type(res, ui.item.DecoratedItem[cls]) - # should strip to origin type - assert func.__discord_ui_model_type__ is _CustomButton + assert func.__discord_ui_model_type__ is cls assert func.__discord_ui_model_kwargs__ == {"param": 1337} # typing-only check @@ -63,19 +69,3 @@ def _test_typing_cls(self) -> None: cls=_CustomButton, this_should_not_work="h", # type: ignore ) - - @pytest.mark.parametrize( - ("decorator", "invalid_cls"), - [ - (ui.button, ui.StringSelect), - (ui.string_select, ui.Button), - (ui.user_select, ui.Button), - (ui.role_select, ui.Button), - (ui.mentionable_select, ui.Button), - (ui.channel_select, ui.Button), - ], - ) - def test_cls_invalid(self, decorator, invalid_cls) -> None: - for cls in [123, int, invalid_cls]: - with pytest.raises(TypeError, match=r"cls argument must be"): - decorator(cls=cls) From df5e3915c905c014f8343f5913231b25ea0fc06d Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sat, 28 Dec 2024 17:55:06 +0100 Subject: [PATCH 2/7] fix(modal): fix timeout edge cases with `custom_id` reuse and long-running callbacks (#914) --- changelog/914.bugfix.rst | 1 + disnake/ui/modal.py | 104 ++++++++++++++++++++++++--------- examples/interactions/modal.py | 2 +- test_bot/cogs/modals.py | 2 +- 4 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 changelog/914.bugfix.rst diff --git a/changelog/914.bugfix.rst b/changelog/914.bugfix.rst new file mode 100644 index 0000000000..6dd6dcc4bf --- /dev/null +++ b/changelog/914.bugfix.rst @@ -0,0 +1 @@ +Fix :class:`ui.Modal` timeout issues with long-running callbacks, and multiple modals with the same user and ``custom_id``. diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py index adf21ffa9c..7f0192c3b8 100644 --- a/disnake/ui/modal.py +++ b/disnake/ui/modal.py @@ -6,7 +6,8 @@ import os import sys import traceback -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, TypeVar, Union +from functools import partial +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union from ..enums import TextInputStyle from ..utils import MISSING @@ -38,14 +39,32 @@ class Modal: components: |components_type| The components to display in the modal. Up to 5 action rows. custom_id: :class:`str` - The custom ID of the modal. + The custom ID of the modal. This is usually not required. + If not given, then a unique one is generated for you. + + .. note:: + :class:`Modal`\\s are identified based on the user ID that triggered the + modal, and this ``custom_id``. + This can result in collisions when a user opens a modal with the same ``custom_id`` on + two separate devices, for example. + + To avoid such issues, consider not specifying a ``custom_id`` to use an automatically generated one, + or include a unique value in the custom ID (e.g. the original interaction ID). + timeout: :class:`float` The time to wait until the modal is removed from cache, if no interaction is made. Modals without timeouts are not supported, since there's no event for when a modal is closed. Defaults to 600 seconds. """ - __slots__ = ("title", "custom_id", "components", "timeout") + __slots__ = ( + "title", + "custom_id", + "components", + "timeout", + "__remove_callback", + "__timeout_handle", + ) def __init__( self, @@ -67,6 +86,11 @@ def __init__( self.components: List[ActionRow] = rows self.timeout: float = timeout + # function for the modal to remove itself from the store, if any + self.__remove_callback: Optional[Callable[[Modal], None]] = None + # timer handle for the scheduled timeout + self.__timeout_handle: Optional[asyncio.TimerHandle] = None + def __repr__(self) -> str: return ( f" None: except Exception as e: await self.on_error(e, interaction) finally: - # if the interaction was responded to (no matter if in the callback or error handler), - # the modal closed for the user and therefore can be removed from the store - if interaction.response._response_type is not None: - interaction._state._modal_store.remove_modal( - interaction.author.id, interaction.custom_id - ) + if interaction.response._response_type is None: + # If the interaction was not successfully responded to, the modal didn't close for the user. + # Since the timeout was already stopped at this point, restart it. + self._start_listening(self.__remove_callback) + else: + # Otherwise, the modal closed for the user; remove it from the store. + self._stop_listening() + + def _start_listening(self, remove_callback: Optional[Callable[[Modal], None]]) -> None: + self.__remove_callback = remove_callback + + loop = asyncio.get_running_loop() + if self.__timeout_handle is not None: + # shouldn't get here, but handled just in case + self.__timeout_handle.cancel() + + # start timeout + self.__timeout_handle = loop.call_later(self.timeout, self._dispatch_timeout) + + def _stop_listening(self) -> None: + # cancel timeout + if self.__timeout_handle is not None: + self.__timeout_handle.cancel() + self.__timeout_handle = None + + # remove modal from store + if self.__remove_callback is not None: + self.__remove_callback(self) + self.__remove_callback = None + + def _dispatch_timeout(self) -> None: + self._stop_listening() + asyncio.create_task(self.on_timeout(), name=f"disnake-ui-modal-timeout-{self.custom_id}") def dispatch(self, interaction: ModalInteraction) -> None: + # stop the timeout, but don't remove the modal from the store yet in case the + # response fails and the modal stays open + if self.__timeout_handle is not None: + self.__timeout_handle.cancel() + asyncio.create_task( self._scheduled_task(interaction), name=f"disnake-ui-modal-dispatch-{self.custom_id}" ) @@ -232,28 +288,22 @@ def __init__(self, state: ConnectionState) -> None: self._modals: Dict[Tuple[int, str], Modal] = {} def add_modal(self, user_id: int, modal: Modal) -> None: - loop = asyncio.get_running_loop() - self._modals[(user_id, modal.custom_id)] = modal - loop.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout)) + key = (user_id, modal.custom_id) - def remove_modal(self, user_id: int, modal_custom_id: str) -> Modal: - return self._modals.pop((user_id, modal_custom_id)) + # if another modal with the same user+custom_id already exists, + # stop its timeout to avoid overlaps/collisions + if (existing := self._modals.get(key)) is not None: + existing._stop_listening() - async def handle_timeout(self, user_id: int, modal_custom_id: str, timeout: float) -> None: - # Waits for the timeout and then removes the modal from cache, this is done just in case - # the user closed the modal, as there isn't an event for that. + # start timeout, store modal + remove_callback = partial(self.remove_modal, user_id) + modal._start_listening(remove_callback) + self._modals[key] = modal - await asyncio.sleep(timeout) - try: - modal = self.remove_modal(user_id, modal_custom_id) - except KeyError: - # The modal has already been removed. - pass - else: - await modal.on_timeout() + def remove_modal(self, user_id: int, modal: Modal) -> None: + self._modals.pop((user_id, modal.custom_id), None) def dispatch(self, interaction: ModalInteraction) -> None: key = (interaction.author.id, interaction.custom_id) - modal = self._modals.get(key) - if modal is not None: + if (modal := self._modals.get(key)) is not None: modal.dispatch(interaction) diff --git a/examples/interactions/modal.py b/examples/interactions/modal.py index f271c82f4c..311b1d7d46 100644 --- a/examples/interactions/modal.py +++ b/examples/interactions/modal.py @@ -43,7 +43,7 @@ def __init__(self) -> None: max_length=1024, ), ] - super().__init__(title="Create Tag", custom_id="create_tag", components=components) + super().__init__(title="Create Tag", components=components) async def callback(self, inter: disnake.ModalInteraction) -> None: tag_name = inter.text_values["name"] diff --git a/test_bot/cogs/modals.py b/test_bot/cogs/modals.py index c5d514a25c..e988c88284 100644 --- a/test_bot/cogs/modals.py +++ b/test_bot/cogs/modals.py @@ -22,7 +22,7 @@ def __init__(self) -> None: style=TextInputStyle.paragraph, ), ] - super().__init__(title="Create Tag", custom_id="create_tag", components=components) + super().__init__(title="Create Tag", components=components) async def callback(self, inter: disnake.ModalInteraction[commands.Bot]) -> None: embed = disnake.Embed(title="Tag Creation") From 14d5b7898657663a10e8f834e13249751980910f Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:05:38 +0100 Subject: [PATCH 3/7] fix(commands): some caveats related to guild cache in user app interactions (#1262) --- disnake/ext/commands/cooldowns.py | 4 +++- disnake/ext/commands/core.py | 10 ++++++++-- disnake/interactions/application_command.py | 7 +++++++ disnake/interactions/base.py | 7 +++++++ disnake/interactions/message.py | 7 +++++++ disnake/interactions/modal.py | 7 +++++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/disnake/ext/commands/cooldowns.py b/disnake/ext/commands/cooldowns.py index cda127398a..aa364948f2 100644 --- a/disnake/ext/commands/cooldowns.py +++ b/disnake/ext/commands/cooldowns.py @@ -49,7 +49,9 @@ def get_key(self, msg: Message) -> Any: elif self is BucketType.role: # if author is not a Member we are in a private-channel context; returning its id # yields the same result as for a guild with only the @everyone role - return (msg.author.top_role if isinstance(msg.author, Member) else msg.channel).id + return ( + msg.author.top_role if msg.guild and isinstance(msg.author, Member) else msg.channel + ).id def __call__(self, msg: Message) -> Any: return self.get_key(msg) diff --git a/disnake/ext/commands/core.py b/disnake/ext/commands/core.py index eb7d190b0e..f69c367b5f 100644 --- a/disnake/ext/commands/core.py +++ b/disnake/ext/commands/core.py @@ -2426,11 +2426,14 @@ def dm_only() -> Callable[[T], T]: This check raises a special exception, :exc:`.PrivateMessageOnly` that is inherited from :exc:`.CheckFailure`. + .. note:: + For application commands, consider setting the allowed :ref:`contexts ` instead. + .. versionadded:: 1.1 """ def predicate(ctx: AnyContext) -> bool: - if ctx.guild is not None: + if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is not None: raise PrivateMessageOnly return True @@ -2444,10 +2447,13 @@ def guild_only() -> Callable[[T], T]: This check raises a special exception, :exc:`.NoPrivateMessage` that is inherited from :exc:`.CheckFailure`. + + .. note:: + For application commands, consider setting the allowed :ref:`contexts ` instead. """ def predicate(ctx: AnyContext) -> bool: - if ctx.guild is None: + if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is None: raise NoPrivateMessage return True diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 2148d7df9c..4fb2ee70a3 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -77,6 +77,13 @@ class ApplicationCommandInteraction(Interaction[ClientT]): author: Union[:class:`User`, :class:`Member`] The user or member that sent the interaction. + + .. note:: + In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable, + such as with user-installed applications in guilds, some attributes of :class:`Member`\\s + that depend on the guild/role cache will not work due to an API limitation. + This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`, + and :attr:`~Member.guild_permissions`. locale: :class:`Locale` The selected language of the interaction's author. diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 92b69a2cac..eb28fec4eb 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -145,6 +145,13 @@ class Interaction(Generic[ClientT]): author: Union[:class:`User`, :class:`Member`] The user or member that sent the interaction. + + .. note:: + In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable, + such as with user-installed applications in guilds, some attributes of :class:`Member`\\s + that depend on the guild/role cache will not work due to an API limitation. + This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`, + and :attr:`~Member.guild_permissions`. locale: :class:`Locale` The selected language of the interaction's author. diff --git a/disnake/interactions/message.py b/disnake/interactions/message.py index ee1f401f8c..5205bbfe56 100644 --- a/disnake/interactions/message.py +++ b/disnake/interactions/message.py @@ -66,6 +66,13 @@ class MessageInteraction(Interaction[ClientT]): author: Union[:class:`User`, :class:`Member`] The user or member that sent the interaction. + + .. note:: + In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable, + such as with user-installed applications in guilds, some attributes of :class:`Member`\\s + that depend on the guild/role cache will not work due to an API limitation. + This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`, + and :attr:`~Member.guild_permissions`. locale: :class:`Locale` The selected language of the interaction's author. diff --git a/disnake/interactions/modal.py b/disnake/interactions/modal.py index bc9141e09e..7c661ed3dd 100644 --- a/disnake/interactions/modal.py +++ b/disnake/interactions/modal.py @@ -58,6 +58,13 @@ class ModalInteraction(Interaction[ClientT]): author: Union[:class:`User`, :class:`Member`] The user or member that sent the interaction. + + .. note:: + In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable, + such as with user-installed applications in guilds, some attributes of :class:`Member`\\s + that depend on the guild/role cache will not work due to an API limitation. + This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`, + and :attr:`~Member.guild_permissions`. locale: :class:`Locale` The selected language of the interaction's author. From 42ef060bc1b5de26f86fe2d7983f87c6470f73d4 Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:19:43 +0100 Subject: [PATCH 4/7] feat(commands): support bot-wide defaults for `install_types`/`contexts` (#1261) --- changelog/1173.feature.rst | 1 + changelog/1261.feature.rst | 14 ++++ disnake/app_commands.py | 37 ++++++++++- disnake/ext/commands/base_core.py | 5 ++ disnake/ext/commands/bot.py | 67 ++++++++++++++++++-- disnake/ext/commands/interaction_bot_base.py | 8 +++ docs/ext/commands/slash_commands.rst | 9 +++ tests/ext/commands/test_base_core.py | 44 +++++++++++++ 8 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 changelog/1261.feature.rst diff --git a/changelog/1173.feature.rst b/changelog/1173.feature.rst index 02268dbb8b..77c75f5c8b 100644 --- a/changelog/1173.feature.rst +++ b/changelog/1173.feature.rst @@ -11,3 +11,4 @@ Add support for user-installed commands. See :ref:`app_command_contexts` for fur - |commands| Add ``install_types`` and ``contexts`` parameters to application command decorators. - |commands| Add :func:`~ext.commands.install_types` and :func:`~ext.commands.contexts` decorators. - |commands| Using the :class:`GuildCommandInteraction` annotation now sets :attr:`~ApplicationCommand.install_types` and :attr:`~ApplicationCommand.contexts`, instead of :attr:`~ApplicationCommand.dm_permission`. +- |commands| Add ``default_install_types`` and ``default_contexts`` parameters to :class:`~ext.commands.Bot`. diff --git a/changelog/1261.feature.rst b/changelog/1261.feature.rst new file mode 100644 index 0000000000..77c75f5c8b --- /dev/null +++ b/changelog/1261.feature.rst @@ -0,0 +1,14 @@ +Add support for user-installed commands. See :ref:`app_command_contexts` for further details. +- Add :attr:`ApplicationCommand.install_types` and :attr:`ApplicationCommand.contexts` fields, + with respective :class:`ApplicationInstallTypes` and :class:`InteractionContextTypes` flag types. +- :class:`Interaction` changes: + - Add :attr:`Interaction.context` field, reflecting the context in which the interaction occurred. + - Add :attr:`Interaction.authorizing_integration_owners` field and :class:`AuthorizingIntegrationOwners` class, containing details about the application installation. + - :attr:`Interaction.app_permissions` is now always provided by Discord. +- Add :attr:`Message.interaction_metadata` and :class:`InteractionMetadata` type, containing metadata for the interaction associated with a message. +- Add ``integration_type`` parameter to :func:`utils.oauth_url`. +- Add :attr:`AppInfo.guild_install_type_config` and :attr:`AppInfo.user_install_type_config` fields. +- |commands| Add ``install_types`` and ``contexts`` parameters to application command decorators. +- |commands| Add :func:`~ext.commands.install_types` and :func:`~ext.commands.contexts` decorators. +- |commands| Using the :class:`GuildCommandInteraction` annotation now sets :attr:`~ApplicationCommand.install_types` and :attr:`~ApplicationCommand.contexts`, instead of :attr:`~ApplicationCommand.dm_permission`. +- |commands| Add ``default_install_types`` and ``default_contexts`` parameters to :class:`~ext.commands.Bot`. diff --git a/disnake/app_commands.py b/disnake/app_commands.py index cd6eb76ece..38d83a0ca7 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -547,6 +547,13 @@ def __init__( self.install_types: Optional[ApplicationInstallTypes] = install_types self.contexts: Optional[InteractionContextTypes] = contexts + # TODO(3.0): refactor + # These are for ext.commands defaults. It's quite ugly to do it this way, + # but since __eq__ and to_dict functionality is encapsulated here and can't be moved trivially, + # it'll do until the presumably soon-ish refactor of the entire commands framework. + self._default_install_types: Optional[ApplicationInstallTypes] = None + self._default_contexts: Optional[InteractionContextTypes] = None + self._always_synced: bool = False # reset `default_permission` if set before @@ -614,6 +621,9 @@ def __str__(self) -> str: return self.name def __eq__(self, other) -> bool: + if not isinstance(other, ApplicationCommand): + return False + if not ( self.type == other.type and self.name == other.name @@ -634,8 +644,10 @@ def __eq__(self, other) -> bool: # `contexts` takes priority over `dm_permission`; # ignore `dm_permission` if `contexts` is set, # since the API returns both even when only `contexts` was provided - if self.contexts is not None or other.contexts is not None: - if self.contexts != other.contexts: + self_contexts = self._contexts_with_default + other_contexts = other._contexts_with_default + if self_contexts is not None or other_contexts is not None: + if self_contexts != other_contexts: return False else: # this is a bit awkward; `None` is equivalent to `True` in this case @@ -648,6 +660,9 @@ def __eq__(self, other) -> bool: def _install_types_with_default(self) -> Optional[ApplicationInstallTypes]: # if this is an api-provided command object, keep things as-is if self.install_types is None and not isinstance(self, _APIApplicationCommandMixin): + if self._default_install_types is not None: + return self._default_install_types + # The purpose of this default is to avoid re-syncing after the updating to the new version, # at least as long as the user hasn't enabled user installs in the dev portal # (i.e. if they haven't, the api defaults to this value as well). @@ -658,6 +673,20 @@ def _install_types_with_default(self) -> Optional[ApplicationInstallTypes]: return self.install_types + @property + def _contexts_with_default(self) -> Optional[InteractionContextTypes]: + # (basically the same logic as `_install_types_with_default`, but without a fallback) + if ( + self.contexts is None + and not isinstance(self, _APIApplicationCommandMixin) + and self._default_contexts is not None + # only use default if legacy `dm_permission` wasn't set + and self._dm_permission is None + ): + return self._default_contexts + + return self.contexts + def to_dict(self) -> EditApplicationCommandPayload: data: EditApplicationCommandPayload = { "type": try_enum_to_int(self.type), @@ -678,7 +707,9 @@ def to_dict(self) -> EditApplicationCommandPayload: ) data["integration_types"] = install_types - contexts = self.contexts.values if self.contexts is not None else None + contexts = ( + self._contexts_with_default.values if self._contexts_with_default is not None else None + ) data["contexts"] = contexts # don't set `dm_permission` if `contexts` is set diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py index ff9b8afd76..c513c977db 100644 --- a/disnake/ext/commands/base_core.py +++ b/disnake/ext/commands/base_core.py @@ -36,6 +36,7 @@ from ._types import AppCheck, Coro, Error, Hook from .cog import Cog + from .interaction_bot_base import InteractionBotBase ApplicationCommandInteractionT = TypeVar( "ApplicationCommandInteractionT", bound=ApplicationCommandInteraction, covariant=True @@ -268,6 +269,10 @@ def _apply_guild_only(self) -> None: self.body.contexts = InteractionContextTypes(guild=True) self.body.install_types = ApplicationInstallTypes(guild=True) + def _apply_defaults(self, bot: InteractionBotBase) -> None: + self.body._default_install_types = bot._default_install_types + self.body._default_contexts = bot._default_contexts + @property def dm_permission(self) -> bool: """:class:`bool`: Whether this command can be used in DMs.""" diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py index 825f96e6ae..1e3e2d864a 100644 --- a/disnake/ext/commands/bot.py +++ b/disnake/ext/commands/bot.py @@ -18,7 +18,12 @@ from disnake.activity import BaseActivity from disnake.client import GatewayParams from disnake.enums import Status - from disnake.flags import Intents, MemberCacheFlags + from disnake.flags import ( + ApplicationInstallTypes, + Intents, + InteractionContextTypes, + MemberCacheFlags, + ) from disnake.i18n import LocalizationProtocol from disnake.mentions import AllowedMentions from disnake.message import Message @@ -117,6 +122,28 @@ class Bot(BotBase, InteractionBotBase, disnake.Client): .. versionadded:: 2.5 + default_install_types: Optional[:class:`.ApplicationInstallTypes`] + The default installation types where application commands will be available. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.install_types` decorator, + the ``install_types`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + default_contexts: Optional[:class:`.InteractionContextTypes`] + The default contexts where application commands will be usable. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.contexts` decorator, + the ``contexts`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + Attributes ---------- command_prefix @@ -233,10 +260,12 @@ def __init__( reload: bool = False, case_insensitive: bool = False, command_sync_flags: CommandSyncFlags = ..., - test_guilds: Optional[Sequence[int]] = None, sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, shard_id: Optional[int] = None, @@ -285,10 +314,12 @@ def __init__( reload: bool = False, case_insensitive: bool = False, command_sync_flags: CommandSyncFlags = ..., - test_guilds: Optional[Sequence[int]] = None, sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, shard_ids: Optional[List[int]] = None, # instead of shard_id @@ -391,6 +422,28 @@ class InteractionBot(InteractionBotBase, disnake.Client): .. versionadded:: 2.5 + default_install_types: Optional[:class:`.ApplicationInstallTypes`] + The default installation types where application commands will be available. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.install_types` decorator, + the ``install_types`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + default_contexts: Optional[:class:`.InteractionContextTypes`] + The default contexts where application commands will be usable. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.contexts` decorator, + the ``contexts`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + Attributes ---------- owner_id: Optional[:class:`int`] @@ -434,10 +487,12 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, command_sync_flags: CommandSyncFlags = ..., - test_guilds: Optional[Sequence[int]] = None, sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, shard_id: Optional[int] = None, @@ -479,10 +534,12 @@ def __init__( owner_ids: Optional[Set[int]] = None, reload: bool = False, command_sync_flags: CommandSyncFlags = ..., - test_guilds: Optional[Sequence[int]] = None, sync_commands: bool = ..., sync_commands_debug: bool = ..., sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, asyncio_debug: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, shard_ids: Optional[List[int]] = None, # instead of shard_id diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py index 4e9f8698ea..902c2c1880 100644 --- a/disnake/ext/commands/interaction_bot_base.py +++ b/disnake/ext/commands/interaction_bot_base.py @@ -149,6 +149,8 @@ def __init__( sync_commands_debug: bool = MISSING, sync_commands_on_cog_unload: bool = MISSING, test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, **options: Any, ) -> None: if test_guilds and not all(isinstance(guild_id, int) for guild_id in test_guilds): @@ -200,6 +202,9 @@ def __init__( self._command_sync_flags = command_sync_flags self._sync_queued: asyncio.Lock = asyncio.Lock() + self._default_install_types = default_install_types + self._default_contexts = default_contexts + self._slash_command_checks = [] self._slash_command_check_once = [] self._user_command_checks = [] @@ -286,6 +291,7 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None: if slash_command.name in self.all_slash_commands: raise CommandRegistrationError(slash_command.name) + slash_command._apply_defaults(self) slash_command.body.localize(self.i18n) self.all_slash_commands[slash_command.name] = slash_command @@ -316,6 +322,7 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None: if user_command.name in self.all_user_commands: raise CommandRegistrationError(user_command.name) + user_command._apply_defaults(self) user_command.body.localize(self.i18n) self.all_user_commands[user_command.name] = user_command @@ -348,6 +355,7 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None: if message_command.name in self.all_message_commands: raise CommandRegistrationError(message_command.name) + message_command._apply_defaults(self) message_command.body.localize(self.i18n) self.all_message_commands[message_command.name] = message_command diff --git a/docs/ext/commands/slash_commands.rst b/docs/ext/commands/slash_commands.rst index ad3e681e46..0b878aa30d 100644 --- a/docs/ext/commands/slash_commands.rst +++ b/docs/ext/commands/slash_commands.rst @@ -709,6 +709,14 @@ as an argument directly to the command decorator. To allow all (guild + user) in a :meth:`ApplicationInstallTypes.all` shorthand is also available. By default, commands are set to only be usable in guild-installed contexts. +You can set bot-wide defaults using the ``default_install_types`` parameter on +the :class:`~ext.commands.Bot` constructor: + +.. code-block:: python3 + + bot = commands.Bot( + default_install_types=disnake.ApplicationInstallTypes(user=True), + ) .. note:: To enable installing the bot in user contexts (or disallow guild contexts), you will need to @@ -739,6 +747,7 @@ decorator, to e.g. disallow a command in guilds: In the same way, you can use the ``contexts=`` parameter and :class:`InteractionContextTypes` in the command decorator directly. The default context for commands is :attr:`~InteractionContextTypes.guild` + :attr:`~InteractionContextTypes.bot_dm`. +This can also be adjusted using the ``default_contexts`` parameter on the :class:`~ext.commands.Bot` constructor. This attribute supersedes the old ``dm_permission`` field, which can now be considered equivalent to the :attr:`~InteractionContextTypes.bot_dm` flag. diff --git a/tests/ext/commands/test_base_core.py b/tests/ext/commands/test_base_core.py index c8335dd70a..ac6b5ee93b 100644 --- a/tests/ext/commands/test_base_core.py +++ b/tests/ext/commands/test_base_core.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: MIT +import warnings + import pytest import disnake @@ -118,6 +120,48 @@ async def cmd(self, _) -> None: assert c.cmd.install_types == disnake.ApplicationInstallTypes(guild=True) +class TestDefaultContexts: + @pytest.fixture + def bot(self) -> commands.InteractionBot: + return commands.InteractionBot( + default_contexts=disnake.InteractionContextTypes(bot_dm=True) + ) + + def test_default(self, bot: commands.InteractionBot) -> None: + @bot.slash_command() + async def c(inter) -> None: + ... + + assert c.body.to_dict().get("contexts") == [1] + assert "dm_permission" not in c.body.to_dict() + + def test_decorator_override(self, bot: commands.InteractionBot) -> None: + @commands.contexts(private_channel=True) + @bot.slash_command() + async def c(inter) -> None: + ... + + assert c.body.to_dict().get("contexts") == [2] + + def test_annotation_override(self, bot: commands.InteractionBot) -> None: + @bot.slash_command() + async def c(inter: disnake.GuildCommandInteraction) -> None: + ... + + assert c.body.to_dict().get("contexts") == [0] + + def test_dm_permission(self, bot: commands.InteractionBot) -> None: + with warnings.catch_warnings(record=True): + + @bot.slash_command(dm_permission=False) + async def c(inter) -> None: + ... + + # if dm_permission was set, the `contexts` default shouldn't apply + assert c.body.to_dict().get("contexts") is None + assert c.body.to_dict().get("dm_permission") is False + + def test_localization_copy() -> None: class Cog(commands.Cog): @commands.slash_command() From 6cde3327be9ad576bbc856723734207198ce558b Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:33:16 +0100 Subject: [PATCH 5/7] feat: implement subscriptions and update the premium apps API (#1258) Signed-off-by: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> --- changelog/1113.feature.rst | 4 +- changelog/1186.feature.rst | 4 +- changelog/1249.feature.rst | 4 +- changelog/1257.feature.rst | 5 ++ disnake/__init__.py | 1 + disnake/client.py | 2 +- disnake/entitlement.py | 1 - disnake/enums.py | 24 ++++++- disnake/http.py | 41 ++++++++++++ disnake/iterators.py | 101 ++++++++++++++++++++++++++++ disnake/sku.py | 77 ++++++++++++++++++++- disnake/subscription.py | 123 ++++++++++++++++++++++++++++++++++ disnake/types/subscription.py | 23 +++++++ docs/api/events.rst | 31 ++++++++- docs/api/index.rst | 1 + docs/api/subscriptions.rst | 44 ++++++++++++ 16 files changed, 472 insertions(+), 14 deletions(-) create mode 100644 changelog/1257.feature.rst create mode 100644 disnake/subscription.py create mode 100644 disnake/types/subscription.py create mode 100644 docs/api/subscriptions.rst diff --git a/changelog/1113.feature.rst b/changelog/1113.feature.rst index 079ad452e1..e9c1c6b123 100644 --- a/changelog/1113.feature.rst +++ b/changelog/1113.feature.rst @@ -1,5 +1,5 @@ Support application subscriptions and one-time purchases (see the :ddocs:`official docs ` for more info). -- New types: :class:`SKU`, :class:`Entitlement`. +- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`. - New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type. -- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`. +- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`. - New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`. diff --git a/changelog/1186.feature.rst b/changelog/1186.feature.rst index 079ad452e1..e9c1c6b123 100644 --- a/changelog/1186.feature.rst +++ b/changelog/1186.feature.rst @@ -1,5 +1,5 @@ Support application subscriptions and one-time purchases (see the :ddocs:`official docs ` for more info). -- New types: :class:`SKU`, :class:`Entitlement`. +- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`. - New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type. -- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`. +- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`. - New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`. diff --git a/changelog/1249.feature.rst b/changelog/1249.feature.rst index 079ad452e1..e9c1c6b123 100644 --- a/changelog/1249.feature.rst +++ b/changelog/1249.feature.rst @@ -1,5 +1,5 @@ Support application subscriptions and one-time purchases (see the :ddocs:`official docs ` for more info). -- New types: :class:`SKU`, :class:`Entitlement`. +- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`. - New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type. -- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`. +- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`. - New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`. diff --git a/changelog/1257.feature.rst b/changelog/1257.feature.rst new file mode 100644 index 0000000000..e9c1c6b123 --- /dev/null +++ b/changelog/1257.feature.rst @@ -0,0 +1,5 @@ +Support application subscriptions and one-time purchases (see the :ddocs:`official docs ` for more info). +- New types: :class:`SKU`, :class:`Entitlement`, :class:`Subscription`. +- New :attr:`Interaction.entitlements` attribute, and :meth:`InteractionResponse.require_premium` response type. +- New events: :func:`on_entitlement_create`, :func:`on_entitlement_update`, :func:`on_entitlement_delete`, :func:`on_subscription_create`, :func:`on_subscription_update` and :func:`on_subscription_delete`. +- New :class:`Client` methods: :meth:`~Client.skus`, :meth:`~Client.entitlements`, :meth:`~Client.fetch_entitlement`, :meth:`~Client.create_entitlement`. diff --git a/disnake/__init__.py b/disnake/__init__.py index 705de41175..e2658fed15 100644 --- a/disnake/__init__.py +++ b/disnake/__init__.py @@ -66,6 +66,7 @@ from .soundboard import * from .stage_instance import * from .sticker import * +from .subscription import * from .team import * from .template import * from .threads import * diff --git a/disnake/client.py b/disnake/client.py index 23cc8ae626..c87213d2f2 100644 --- a/disnake/client.py +++ b/disnake/client.py @@ -3180,7 +3180,7 @@ async def skus(self) -> List[SKU]: The list of SKUs. """ data = await self.http.get_skus(self.application_id) - return [SKU(data=d) for d in data] + return [SKU(data=d, state=self._connection) for d in data] def entitlements( self, diff --git a/disnake/entitlement.py b/disnake/entitlement.py index b27fff44db..8dfda2c745 100644 --- a/disnake/entitlement.py +++ b/disnake/entitlement.py @@ -76,7 +76,6 @@ class Entitlement(Hashable): Set to ``None`` when this is a test entitlement. ends_at: Optional[:class:`datetime.datetime`] The time at which the entitlement stops being active. - Set to ``None`` when this is a test entitlement. You can use :meth:`is_active` to check whether this entitlement is still active. """ diff --git a/disnake/enums.py b/disnake/enums.py index 1707d0976b..f6eef0fb7d 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -72,6 +72,7 @@ "OnboardingPromptType", "SKUType", "EntitlementType", + "SubscriptionStatus", "PollLayoutType", "VoiceChannelEffectAnimationType", "MessageReferenceType", @@ -1340,7 +1341,22 @@ class Event(Enum): """ entitlement_delete = "entitlement_delete" """Called when a user's entitlement is deleted. - Represents the :func:`on_entitlement_delete` event. + Represents the :func:`on_entitlement_delete` event.""" + subscription_create = "subscription_create" + """Called when a subscription for a premium app is created. + Represents the :func:`on_subscription_create` event. + + .. versionadded:: 2.10 + """ + subscription_update = "subscription_update" + """Called when a subscription for a premium app is updated. + Represents the :func:`on_subscription_update` event. + + .. versionadded:: 2.10 + """ + subscription_delete = "subscription_delete" + """Called when a subscription for a premium app is deleted. + Represents the :func:`on_subscription_delete` event. .. versionadded:: 2.10 """ @@ -1429,6 +1445,12 @@ class EntitlementType(Enum): application_subscription = 8 +class SubscriptionStatus(Enum): + active = 0 + ending = 1 + inactive = 2 + + class PollLayoutType(Enum): default = 1 diff --git a/disnake/http.py b/disnake/http.py index 8304d780d4..4d3132c2ea 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -75,6 +75,7 @@ sku, soundboard, sticker, + subscription, template, threads, user, @@ -2412,6 +2413,46 @@ def get_entitlement( ) ) + def get_subscriptions( + self, + sku_id: Snowflake, + *, + before: Optional[Snowflake] = None, + after: Optional[Snowflake] = None, + limit: int = 50, + user_id: Optional[Snowflake] = None, + ) -> Response[List[subscription.Subscription]]: + params: Dict[str, Any] = { + "limit": limit, + } + if before is not None: + params["before"] = before + if after is not None: + params["after"] = after + if user_id is not None: + params["user_id"] = user_id + + return self.request( + Route( + "GET", + "/skus/{sku_id}/subscriptions", + sku_id=sku_id, + ), + params=params, + ) + + def get_subscription( + self, sku_id: Snowflake, subscription_id: int + ) -> Response[subscription.Subscription]: + return self.request( + Route( + "GET", + "/skus/{sku_id}/subscriptions/{subscription_id}", + sku_id=sku_id, + subscription_id=subscription_id, + ) + ) + def create_test_entitlement( self, application_id: Snowflake, diff --git a/disnake/iterators.py b/disnake/iterators.py index 86c311e39f..847041b7ac 100644 --- a/disnake/iterators.py +++ b/disnake/iterators.py @@ -27,6 +27,7 @@ from .guild_scheduled_event import GuildScheduledEvent from .integrations import PartialIntegration from .object import Object +from .subscription import Subscription from .threads import Thread from .utils import maybe_coroutine, snowflake_time, time_snowflake @@ -39,6 +40,7 @@ "MemberIterator", "GuildScheduledEventUserIterator", "EntitlementIterator", + "SubscriptionIterator", "PollAnswerIterator", ) @@ -60,6 +62,7 @@ GuildScheduledEventUser as GuildScheduledEventUserPayload, ) from .types.message import Message as MessagePayload + from .types.subscription import Subscription as SubscriptionPayload from .types.threads import Thread as ThreadPayload from .types.user import PartialUser as PartialUserPayload from .user import User @@ -1147,6 +1150,104 @@ async def _after_strategy(self, retrieve: int) -> List[EntitlementPayload]: return data +class SubscriptionIterator(_AsyncIterator["Subscription"]): + def __init__( + self, + sku_id: int, + *, + state: ConnectionState, + user_id: Optional[int] = None, # required, except for oauth queries + limit: Optional[int] = None, + before: Optional[Union[Snowflake, datetime.datetime]] = None, + after: Optional[Union[Snowflake, datetime.datetime]] = None, + ) -> None: + if isinstance(before, datetime.datetime): + before = Object(id=time_snowflake(before, high=False)) + if isinstance(after, datetime.datetime): + after = Object(id=time_snowflake(after, high=True)) + + self.sku_id: int = sku_id + self.user_id: Optional[int] = user_id + self.limit: Optional[int] = limit + self.before: Optional[Snowflake] = before + self.after: Snowflake = after or OLDEST_OBJECT + + self._state: ConnectionState = state + self.request = self._state.http.get_subscriptions + self.subscriptions: asyncio.Queue[Subscription] = asyncio.Queue() + + self._filter: Optional[Callable[[SubscriptionPayload], bool]] = None + if self.before: + self._strategy = self._before_strategy + if self.after != OLDEST_OBJECT: + self._filter = lambda s: int(s["id"]) > self.after.id + else: + self._strategy = self._after_strategy + + async def next(self) -> Subscription: + if self.subscriptions.empty(): + await self._fill() + + try: + return self.subscriptions.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems from None + + def _get_retrieve(self) -> bool: + limit = self.limit + if limit is None or limit > 100: + retrieve = 100 + else: + retrieve = limit + self.retrieve: int = retrieve + return retrieve > 0 + + async def _fill(self) -> None: + if not self._get_retrieve(): + return + + data = await self._strategy(self.retrieve) + if len(data) < 100: + self.limit = 0 # terminate loop + + if self._filter: + data = filter(self._filter, data) + + for subscription in data: + await self.subscriptions.put(Subscription(data=subscription, state=self._state)) + + async def _before_strategy(self, retrieve: int) -> List[SubscriptionPayload]: + before = self.before.id if self.before else None + data = await self.request( + self.sku_id, + before=before, + limit=retrieve, + user_id=self.user_id, + ) + + if len(data): + if self.limit is not None: + self.limit -= retrieve + # since pagination order isn't documented, don't rely on results being sorted one way or the other + self.before = Object(id=min(int(data[0]["id"]), int(data[-1]["id"]))) + return data + + async def _after_strategy(self, retrieve: int) -> List[SubscriptionPayload]: + after = self.after.id + data = await self.request( + self.sku_id, + after=after, + limit=retrieve, + user_id=self.user_id, + ) + + if len(data): + if self.limit is not None: + self.limit -= retrieve + self.after = Object(id=max(int(data[0]["id"]), int(data[-1]["id"]))) + return data + + class PollAnswerIterator(_AsyncIterator[Union["User", "Member"]]): def __init__( self, diff --git a/disnake/sku.py b/disnake/sku.py index 204720d6a3..3b9e4fabb3 100644 --- a/disnake/sku.py +++ b/disnake/sku.py @@ -3,14 +3,18 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from .enums import SKUType, try_enum from .flags import SKUFlags +from .iterators import SubscriptionIterator from .mixins import Hashable +from .subscription import Subscription from .utils import snowflake_time if TYPE_CHECKING: + from .abc import Snowflake, SnowflakeTime + from .state import ConnectionState from .types.sku import SKU as SKUPayload @@ -56,9 +60,10 @@ class SKU(Hashable): The SKU's URL slug, system-generated based on :attr:`name`. """ - __slots__ = ("id", "type", "application_id", "name", "slug", "_flags") + __slots__ = ("_state", "id", "type", "application_id", "name", "slug", "_flags") - def __init__(self, *, data: SKUPayload) -> None: + def __init__(self, *, data: SKUPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state self.id: int = int(data["id"]) self.type: SKUType = try_enum(SKUType, data["type"]) self.application_id: int = int(data["application_id"]) @@ -81,3 +86,69 @@ def created_at(self) -> datetime.datetime: def flags(self) -> SKUFlags: """:class:`SKUFlags`: Returns the SKU's flags.""" return SKUFlags._from_value(self._flags) + + async def subscriptions( + self, + user: Snowflake, + *, + limit: Optional[int] = 50, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + ) -> SubscriptionIterator: + """|coro| + + Retrieves an :class:`.AsyncIterator` that enables receiving subscriptions for the SKU. + + All parameters, except ``user``, are optional. + + Parameters + ---------- + user: :class:`abc.Snowflake` + The user to retrieve subscriptions for. + limit: Optional[:class:`int`] + The number of subscriptions to retrieve. + If ``None``, retrieves every subscription. + Note, however, that this would make it a slow operation. + Defaults to ``50``. + before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieves subscriptions created before this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieve subscriptions created after this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + + Raises + ------ + HTTPException + Retrieving the subscriptions failed. + + Yields + ------ + :class:`.Subscription` + The subscriptions for the given parameters. + """ + return SubscriptionIterator( + self.id, + state=self._state, + user_id=user.id, + limit=limit, + before=before, + after=after, + ) + + async def fetch_subscription(self, subscription_id: int, /) -> Subscription: + """|coro| + + Retrieve a subscription for this SKU given its ID. + + Raises + ------ + NotFound + The subscription does not exist. + HTTPException + Retrieving the subscription failed. + """ + data = await self._state.http.get_subscription(self.id, subscription_id) + return Subscription(data=data, state=self._state) diff --git a/disnake/subscription.py b/disnake/subscription.py new file mode 100644 index 0000000000..bf86e1a35a --- /dev/null +++ b/disnake/subscription.py @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, List, Optional + +from .enums import SubscriptionStatus, try_enum +from .mixins import Hashable +from .utils import parse_time, snowflake_time + +if TYPE_CHECKING: + from .state import ConnectionState + from .types.subscription import Subscription as SubscriptionPayload + from .user import User + +__all__ = ("Subscription",) + + +class Subscription(Hashable): + """Represents a subscription. + + This can only be retrieved using :meth:`SKU.subscriptions` or :meth:`SKU.fetch_subscription`, + or provided by events (e.g. :func:`on_subscription_create`). + + .. warning:: + :class:`Subscription`\\s should not be used to grant perks. Use :class:`Entitlement`\\s as a way of determining whether a user should have access to a specific :class:`SKU`. + + .. note:: + Some subscriptions may have been canceled already; consider using :meth:`is_canceled` to check whether a given subscription was canceled. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two :class:`Subscription`\\s are equal. + + .. describe:: x != y + + Checks if two :class:`Subscription`\\s are not equal. + + .. describe:: hash(x) + + Returns the subscription's hash. + + .. versionadded:: 2.10 + + Attributes + ---------- + id: :class:`int` + The subscription's ID. + user_id: :class:`int` + The ID of the user who is subscribed to the :attr:`sku_ids`. + + See also :attr:`user`. + sku_ids: List[:class:`int`] + The ID of the SKUs the user is subscribed to. + renewal_sku_ids: List[:class:`int`] + The IDs of the SKUs that will be renewed at the start of the new period. + entitlement_ids: List[:class:`int`] + The IDs of the entitlements the user has as part of this subscription. + current_period_start: :class:`datetime.datetime` + The time at which the current period for the given subscription started. + current_period_end: :class:`datetime.datetime` + The time at which the current period for the given subscription will end. + status: :class:`SubscriptionStatus` + The current status of the given subscription. + canceled_at: Optional[:class:`datetime.datetime`] + The time at which the subscription was canceled. + + See also :attr:`is_canceled`. + """ + + __slots__ = ( + "_state", + "id", + "user_id", + "sku_ids", + "entitlement_ids", + "renewal_sku_ids", + "current_period_start", + "current_period_end", + "status", + "canceled_at", + ) + + def __init__(self, *, data: SubscriptionPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + + self.id: int = int(data["id"]) + self.user_id: int = int(data["user_id"]) + self.sku_ids: List[int] = list(map(int, data["sku_ids"])) + self.entitlement_ids: List[int] = list(map(int, data["entitlement_ids"])) + self.renewal_sku_ids: Optional[List[int]] = ( + list(map(int, renewal_sku_ids)) + if (renewal_sku_ids := data.get("renewal_sku_ids")) is not None + else None + ) + self.current_period_start: datetime.datetime = parse_time(data["current_period_start"]) + self.current_period_end: datetime.datetime = parse_time(data["current_period_end"]) + self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data["status"]) + self.canceled_at: Optional[datetime.datetime] = parse_time(data["canceled_at"]) + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the subscription's creation time in UTC.""" + return snowflake_time(self.id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user who is subscribed to the :attr:`sku_ids`. + + Requires the user to be cached. + See also :attr:`user_id`. + """ + return self._state.get_user(self.user_id) + + @property + def is_canceled(self) -> bool: + """:class:`bool`: Whether the subscription was canceled, + based on :attr:`canceled_at`. + """ + return self.canceled_at is not None diff --git a/disnake/types/subscription.py b/disnake/types/subscription.py new file mode 100644 index 0000000000..1cfaaf8684 --- /dev/null +++ b/disnake/types/subscription.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: MIT + +from typing import List, Literal, Optional, TypedDict + +from typing_extensions import NotRequired + +from .snowflake import Snowflake + +SubscriptionStatus = Literal[0, 1, 2] + + +class Subscription(TypedDict): + id: Snowflake + user_id: Snowflake + sku_ids: List[Snowflake] + entitlement_ids: List[Snowflake] + renewal_sku_ids: Optional[List[Snowflake]] + current_period_start: str + current_period_end: str + status: SubscriptionStatus + canceled_at: Optional[str] + # this is always missing unless queried with a private OAuth scope. + country: NotRequired[str] diff --git a/docs/api/events.rst b/docs/api/events.rst index bd4f16652a..99599fa400 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1580,8 +1580,8 @@ This section documents events related to entitlements, which are used for applic Called when an entitlement is updated. - This happens e.g. when a user's subscription gets renewed (in which case the - :attr:`Entitlement.ends_at` attribute reflects the new expiration date). + This happens **only** when a user's subscription ends or is cancelled (in which case the + :attr:`Entitlement.ends_at` attribute reflects the expiration date). .. versionadded:: 2.10 @@ -1601,6 +1601,33 @@ This section documents events related to entitlements, which are used for applic :param entitlement: The entitlement that was deleted. :type entitlement: :class:`Entitlement` +.. function:: on_subscription_create(subscription) + + Called when a subscription is created. + + .. versionadded:: 2.10 + + :param subscription: The subscription that was created. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_update(subscription) + + Called when a subscription is updated. + + .. versionadded:: 2.10 + + :param subscription: The subscription that was updated. + :type subscription: :class:`Subscription` + +.. function:: on_subscription_delete(subscription) + + Called when a subscription is deleted. + + .. versionadded:: 2.10 + + :param subscription: The subscription that was deleted. + :type subscription: :class:`Subscription` + Enumerations ------------ diff --git a/docs/api/index.rst b/docs/api/index.rst index c29ba29e6f..0b04ea5e1d 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -116,6 +116,7 @@ Documents permissions roles skus + subscriptions soundboard stage_instances stickers diff --git a/docs/api/subscriptions.rst b/docs/api/subscriptions.rst new file mode 100644 index 0000000000..338cb9e786 --- /dev/null +++ b/docs/api/subscriptions.rst @@ -0,0 +1,44 @@ +.. SPDX-License-Identifier: MIT + +.. currentmodule:: disnake + +Subscriptions +=============== + +This section documents everything related to Subscription(s), which represents a user making recurring payments for at least one SKU. +See the :ddocs:`official docs ` for more info. + +Discord Models +-------------- + +Subscription +~~~~~~~~~~~~ + +.. attributetable:: Subscription + +.. autoclass:: Subscription() + :members: + +Enumerations +------------ + +SubscriptionStatus +~~~~~~~~~~~~~~~~~~ + +.. class:: SubscriptionStatus + + Represents the status of a subscription. + + .. versionadded:: 2.10 + + .. attribute:: active + + Represents an active Subscription which is scheduled to renew. + + .. attribute:: ending + + Represents an active Subscription which will not renew. + + .. attribute:: inactive + + Represents an inactive Subscription which is not being charged. From 3e3b27b267483eafc11a94c9ea995dc9ab9d66c5 Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:57:32 +0100 Subject: [PATCH 6/7] refactor(docs): move enum documentation to docstrings (#1243) --- changelog/1243.misc.rst | 1 + disnake/enums.py | 908 ++++++++++++++++++++++++++-- disnake/ext/commands/cooldowns.py | 12 + disnake/ext/commands/flags.py | 8 +- docs/api/activities.rst | 61 +- docs/api/app_commands.rst | 77 +-- docs/api/app_info.rst | 76 +-- docs/api/audit_logs.rst | 19 +- docs/api/automod.rst | 67 +- docs/api/channels.rst | 138 +---- docs/api/components.rst | 130 +--- docs/api/entitlements.rst | 39 +- docs/api/events.rst | 2 +- docs/api/guild_scheduled_events.rst | 59 +- docs/api/guilds.rst | 180 +----- docs/api/index.rst | 4 +- docs/api/integrations.rst | 19 +- docs/api/interactions.rst | 78 +-- docs/api/invites.rst | 38 +- docs/api/localization.rst | 139 +---- docs/api/messages.rst | 220 +------ docs/api/skus.rst | 23 +- docs/api/stage_instances.rst | 23 +- docs/api/stickers.rst | 40 +- docs/api/subscriptions.rst | 19 +- docs/api/users.rst | 107 +--- docs/api/voice.rst | 72 +-- docs/api/webhooks.rst | 21 +- docs/api/widgets.rst | 29 +- docs/conf.py | 1 + docs/ext/commands/api/checks.rst | 29 +- docs/extensions/enumattrs.py | 49 ++ 32 files changed, 1046 insertions(+), 1642 deletions(-) create mode 100644 changelog/1243.misc.rst create mode 100644 docs/extensions/enumattrs.py diff --git a/changelog/1243.misc.rst b/changelog/1243.misc.rst new file mode 100644 index 0000000000..b10a18609e --- /dev/null +++ b/changelog/1243.misc.rst @@ -0,0 +1 @@ +Move enum member documentation into docstrings. diff --git a/disnake/enums.py b/disnake/enums.py index f6eef0fb7d..c6c3b8eb57 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -117,6 +117,7 @@ class EnumMeta(type): _enum_member_names_: ClassVar[List[str]] _enum_member_map_: ClassVar[Dict[str, Any]] _enum_value_map_: ClassVar[Dict[Any, Any]] + _enum_value_cls_: ClassVar[Type[_EnumValueBase]] def __new__(cls, name: str, bases, attrs, *, comparable: bool = False): value_mapping = {} @@ -210,80 +211,275 @@ def try_value(cls, value): class ChannelType(Enum): + """Specifies the type of channel.""" + text = 0 + """A text channel.""" private = 1 + """A private text channel. Also called a direct message.""" voice = 2 + """A voice channel.""" group = 3 + """A private group text channel.""" category = 4 + """A category channel.""" news = 5 + """A guild news channel.""" news_thread = 10 + """A news thread. + + .. versionadded:: 2.0 + """ public_thread = 11 + """A public thread. + + .. versionadded:: 2.0 + """ private_thread = 12 + """A private thread. + + .. versionadded:: 2.0 + """ stage_voice = 13 + """A guild stage voice channel. + + .. versionadded:: 1.7 + """ guild_directory = 14 + """A student hub channel. + + .. versionadded:: 2.1 + """ forum = 15 + """A channel of only threads. + + .. versionadded:: 2.5 + """ media = 16 + """A channel of only threads but with a focus on media, similar to forum channels. + + .. versionadded:: 2.10 + """ def __str__(self) -> str: return self.name class MessageType(Enum): + """Specifies the type of :class:`Message`. This is used to denote if a message + is to be interpreted as a system message or a regular message. + """ + default = 0 + """The default message type. This is the same as regular messages.""" recipient_add = 1 + """The system message when a user is added to a group private message or a thread.""" recipient_remove = 2 + """The system message when a user is removed from a group private message or a thread.""" call = 3 + """The system message denoting call state, e.g. missed call, started call, etc.""" channel_name_change = 4 + """The system message denoting that a channel's name has been changed.""" channel_icon_change = 5 + """The system message denoting that a channel's icon has been changed.""" pins_add = 6 + """The system message denoting that a pinned message has been added to a channel.""" new_member = 7 + """The system message denoting that a new member has joined a Guild.""" premium_guild_subscription = 8 + """The system message denoting that a member has "nitro boosted" a guild.""" premium_guild_tier_1 = 9 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 1.""" premium_guild_tier_2 = 10 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 2.""" premium_guild_tier_3 = 11 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 3.""" channel_follow_add = 12 + """The system message denoting that an announcement channel has been followed. + + .. versionadded:: 1.3 + """ guild_stream = 13 + """The system message denoting that a member is streaming in the guild. + + .. versionadded:: 1.7 + """ guild_discovery_disqualified = 14 + """The system message denoting that the guild is no longer eligible for Server Discovery. + + .. versionadded:: 1.7 + """ guild_discovery_requalified = 15 + """The system message denoting that the guild has become eligible again for Server Discovery. + + .. versionadded:: 1.7 + """ guild_discovery_grace_period_initial_warning = 16 + """The system message denoting that the guild has failed to meet the Server + Discovery requirements for one week. + + .. versionadded:: 1.7 + """ guild_discovery_grace_period_final_warning = 17 + """The system message denoting that the guild has failed to meet the Server + Discovery requirements for 3 weeks in a row. + + .. versionadded:: 1.7 + """ thread_created = 18 + """The system message denoting that a thread has been created. This is only + sent if the thread has been created from an older message. The period of time + required for a message to be considered old cannot be relied upon and is up to + Discord. + + .. versionadded:: 2.0 + """ reply = 19 + """The system message denoting that the author is replying to a message. + + .. versionadded:: 2.0 + """ application_command = 20 + """The system message denoting that an application (or "slash") command was executed. + + .. versionadded:: 2.0 + """ thread_starter_message = 21 + """The system message denoting the message in the thread that is the one that started the + thread's conversation topic. + + .. versionadded:: 2.0 + """ guild_invite_reminder = 22 + """The system message sent as a reminder to invite people to the guild. + + .. versionadded:: 2.0 + """ context_menu_command = 23 + """The system message denoting that a context menu command was executed. + + .. versionadded:: 2.3 + """ auto_moderation_action = 24 + """The system message denoting that an auto moderation action was executed. + + .. versionadded:: 2.5 + """ role_subscription_purchase = 25 + """The system message denoting that a role subscription was purchased. + + .. versionadded:: 2.9 + """ interaction_premium_upsell = 26 + """The system message for an application premium subscription upsell. + + .. versionadded:: 2.8 + """ stage_start = 27 + """The system message denoting the stage has been started. + + .. versionadded:: 2.9 + """ stage_end = 28 + """The system message denoting the stage has ended. + + .. versionadded:: 2.9 + """ stage_speaker = 29 + """The system message denoting a user has become a speaker. + + .. versionadded:: 2.9 + """ stage_topic = 31 + """The system message denoting the stage topic has been changed. + + .. versionadded:: 2.9 + """ guild_application_premium_subscription = 32 + """The system message denoting that a guild member has subscribed to an application. + + .. versionadded:: 2.8 + """ guild_incident_alert_mode_enabled = 36 + """The system message denoting that an admin enabled security actions. + + .. versionadded:: 2.10 + """ guild_incident_alert_mode_disabled = 37 + """The system message denoting that an admin disabled security actions. + + .. versionadded:: 2.10 + """ guild_incident_report_raid = 38 + """The system message denoting that an admin reported a raid. + + .. versionadded:: 2.10 + """ guild_incident_report_false_alarm = 39 + """The system message denoting that a raid report was a false alarm. + + .. versionadded:: 2.10 + """ poll_result = 46 + """The system message denoting that a poll expired, announcing the most voted answer. + + .. versionadded:: 2.10 + """ class PartyType(Enum): + """Represents the type of a voice channel activity/application. + + .. deprecated:: 2.9 + """ + poker = 755827207812677713 + """The "Poker Night" activity.""" betrayal = 773336526917861400 + """The "Betrayal.io" activity.""" fishing = 814288819477020702 + """The "Fishington.io" activity.""" chess = 832012774040141894 + """The "Chess In The Park" activity.""" letter_tile = 879863686565621790 + """The "Letter Tile" activity.""" word_snack = 879863976006127627 + """The "Word Snacks" activity.""" doodle_crew = 878067389634314250 + """The "Doodle Crew" activity.""" checkers = 832013003968348200 + """The "Checkers In The Park" activity. + + .. versionadded:: 2.3 + """ spellcast = 852509694341283871 + """The "SpellCast" activity. + + .. versionadded:: 2.3 + """ watch_together = 880218394199220334 + """The "Watch Together" activity, a Youtube application. + + .. versionadded:: 2.3 + """ sketch_heads = 902271654783242291 + """The "Sketch Heads" activity. + + .. versionadded:: 2.4 + """ ocho = 832025144389533716 + """The "Ocho" activity. + + .. versionadded:: 2.4 + """ gartic_phone = 1007373802981822582 + """The "Gartic Phone" activity. + + .. versionadded:: 2.9 + """ +# undocumented/internal class SpeakingState(Enum): none = 0 voice = 1 << 0 @@ -298,63 +494,192 @@ def __int__(self) -> int: class VerificationLevel(Enum, comparable=True): + """Specifies a :class:`Guild`\\'s verification level, which is the criteria in + which a member must meet before being able to send messages to the guild. + + .. collapse:: operations + + .. versionadded:: 2.0 + + .. describe:: x == y + + Checks if two verification levels are equal. + .. describe:: x != y + + Checks if two verification levels are not equal. + .. describe:: x > y + + Checks if a verification level is higher than another. + .. describe:: x < y + + Checks if a verification level is lower than another. + .. describe:: x >= y + + Checks if a verification level is higher or equal to another. + .. describe:: x <= y + + Checks if a verification level is lower or equal to another. + """ + none = 0 + """No criteria set.""" low = 1 + """Member must have a verified email on their Discord account.""" medium = 2 + """Member must have a verified email and be registered on Discord for more than five minutes.""" high = 3 + """Member must have a verified email, be registered on Discord for more + than five minutes, and be a member of the guild itself for more than ten minutes. + """ highest = 4 + """Member must have a verified phone on their Discord account.""" def __str__(self) -> str: return self.name class ContentFilter(Enum, comparable=True): + """Specifies a :class:`Guild`\\'s explicit content filter, which is the machine + learning algorithms that Discord uses to detect if an image contains NSFW content. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two content filter levels are equal. + .. describe:: x != y + + Checks if two content filter levels are not equal. + .. describe:: x > y + + Checks if a content filter level is higher than another. + .. describe:: x < y + + Checks if a content filter level is lower than another. + .. describe:: x >= y + + Checks if a content filter level is higher or equal to another. + .. describe:: x <= y + + Checks if a content filter level is lower or equal to another. + """ + disabled = 0 + """The guild does not have the content filter enabled.""" no_role = 1 + """The guild has the content filter enabled for members without a role.""" all_members = 2 + """The guild has the content filter enabled for every member.""" def __str__(self) -> str: return self.name class Status(Enum): + """Specifies a :class:`Member`\\'s status.""" + online = "online" + """The member is online.""" offline = "offline" + """The member is offline.""" idle = "idle" + """The member is idle.""" dnd = "dnd" + """The member is "Do Not Disturb".""" do_not_disturb = "dnd" + """An alias for :attr:`dnd`.""" invisible = "invisible" + """The member is "invisible". In reality, this is only used in sending + a presence a la :meth:`Client.change_presence`. When you receive a + user's presence this will be :attr:`offline` instead. + """ streaming = "streaming" + """The member is live streaming to Twitch or YouTube. + + .. versionadded:: 2.3 + """ def __str__(self) -> str: return self.value class DefaultAvatar(Enum): + """Represents the default avatar of a Discord :class:`User`.""" + blurple = 0 + """Represents the default avatar with the color blurple. See also :attr:`Colour.blurple`.""" grey = 1 + """Represents the default avatar with the color grey. See also :attr:`Colour.greyple`.""" gray = 1 + """An alias for :attr:`grey`.""" green = 2 + """Represents the default avatar with the color green. See also :attr:`Colour.green`.""" orange = 3 + """Represents the default avatar with the color orange. See also :attr:`Colour.orange`.""" red = 4 + """Represents the default avatar with the color red. See also :attr:`Colour.red`.""" fuchsia = 5 + """Represents the default avatar with the color fuchsia. See also :attr:`Colour.fuchsia`. + + .. versionadded:: 2.9 + """ def __str__(self) -> str: return self.name class NotificationLevel(Enum, comparable=True): + """Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two notification levels are equal. + .. describe:: x != y + + Checks if two notification levels are not equal. + .. describe:: x > y + + Checks if a notification level is higher than another. + .. describe:: x < y + + Checks if a notification level is lower than another. + .. describe:: x >= y + + Checks if a notification level is higher or equal to another. + .. describe:: x <= y + + Checks if a notification level is lower or equal to another. + """ + all_messages = 0 + """Members receive notifications for every message regardless of them being mentioned.""" only_mentions = 1 + """Members receive notifications for messages they are mentioned in.""" class AuditLogActionCategory(Enum): + """Represents the category that the :class:`AuditLogAction` belongs to. + + This can be retrieved via :attr:`AuditLogEntry.category`. + """ + create = 1 + """The action is the creation of something.""" delete = 2 + """The action is the deletion of something.""" update = 3 + """The action is the update of something.""" +# NOTE: these fields are only fully documented in audit_logs.rst, +# as the docstrings alone would be ~1000-1500 additional lines class AuditLogAction(Enum): + """Represents the type of action being done for a :class:`AuditLogEntry`\\, + which is retrievable via :meth:`Guild.audit_logs` or via the :func:`on_audit_log_entry_create` event. + """ + # fmt: off guild_update = 1 channel_create = 10 @@ -532,79 +857,186 @@ def target_type(self) -> Optional[str]: class UserFlags(Enum): + """Represents Discord user flags.""" + staff = 1 << 0 + """The user is a Discord Employee.""" partner = 1 << 1 + """The user is a Discord Partner.""" hypesquad = 1 << 2 + """The user is a HypeSquad Events member.""" bug_hunter = 1 << 3 + """The user is a Bug Hunter.""" mfa_sms = 1 << 4 + """The user has SMS recovery for Multi Factor Authentication enabled.""" premium_promo_dismissed = 1 << 5 + """The user has dismissed the Discord Nitro promotion.""" hypesquad_bravery = 1 << 6 + """The user is a HypeSquad Bravery member.""" hypesquad_brilliance = 1 << 7 + """The user is a HypeSquad Brilliance member.""" hypesquad_balance = 1 << 8 + """The user is a HypeSquad Balance member.""" early_supporter = 1 << 9 + """The user is an Early Supporter.""" team_user = 1 << 10 + """The user is a Team User.""" system = 1 << 12 + """The user is a system user (i.e. represents Discord officially).""" has_unread_urgent_messages = 1 << 13 + """The user has an unread system message.""" bug_hunter_level_2 = 1 << 14 + """The user is a Bug Hunter Level 2.""" verified_bot = 1 << 16 + """The user is a Verified Bot.""" verified_bot_developer = 1 << 17 + """The user is an Early Verified Bot Developer.""" discord_certified_moderator = 1 << 18 + """The user is a Discord Certified Moderator.""" http_interactions_bot = 1 << 19 + """The user is a bot that only uses HTTP interactions. + + .. versionadded:: 2.3 + """ spammer = 1 << 20 + """The user is marked as a spammer. + + .. versionadded:: 2.3 + """ active_developer = 1 << 22 + """The user is an Active Developer. + + .. versionadded:: 2.8 + """ class ActivityType(Enum): + """Specifies the type of :class:`Activity`. This is used to check how to + interpret the activity itself. + """ + unknown = -1 + """An unknown activity type. This should generally not happen.""" playing = 0 + """A "Playing" activity type.""" streaming = 1 + """A "Streaming" activity type.""" listening = 2 + """A "Listening" activity type.""" watching = 3 + """A "Watching" activity type.""" custom = 4 + """A custom activity type.""" competing = 5 + """A competing activity type. + + .. versionadded:: 1.5 + """ def __int__(self) -> int: return self.value class TeamMembershipState(Enum): + """Represents the membership state of a team member retrieved through :func:`Client.application_info`. + + .. versionadded:: 1.3 + """ + invited = 1 + """Represents an invited member.""" accepted = 2 + """Represents a member currently in the team.""" class TeamMemberRole(Enum): + """Represents the role of a team member retrieved through :func:`Client.application_info`. + + .. versionadded:: 2.10 + """ + admin = "admin" + """Admins have the most permissions. An admin can only take destructive actions + on the team or team-owned apps if they are the team owner. + """ developer = "developer" + """Developers can access information about a team and team-owned applications, + and take limited actions on them, like configuring interaction + endpoints or resetting the bot token. + """ read_only = "read_only" + """Read-only members can access information about a team and team-owned applications.""" def __str__(self) -> str: return self.name class WebhookType(Enum): + """Represents the type of webhook that can be received. + + .. versionadded:: 1.3 + """ + incoming = 1 + """Represents a webhook that can post messages to channels with a token.""" channel_follower = 2 + """Represents a webhook that is internally managed by Discord, used for following channels.""" application = 3 + """Represents a webhook that is used for interactions or applications. + + .. versionadded:: 2.0 + """ class ExpireBehaviour(Enum): + """Represents the behaviour the :class:`Integration` should perform + when a user's subscription has finished. + + There is an alias for this called ``ExpireBehavior``. + + .. versionadded:: 1.4 + """ + remove_role = 0 + """This will remove the :attr:`StreamIntegration.role` from the user + when their subscription is finished. + """ kick = 1 + """This will kick the user when their subscription is finished.""" ExpireBehavior = ExpireBehaviour class StickerType(Enum): + """Represents the type of sticker. + + .. versionadded:: 2.0 + """ + standard = 1 + """Represents a standard sticker that all users can use.""" guild = 2 + """Represents a custom sticker created in a guild.""" class StickerFormatType(Enum): + """Represents the type of sticker images. + + .. versionadded:: 1.6 + """ + png = 1 + """Represents a sticker with a png image.""" apng = 2 + """Represents a sticker with an apng image.""" lottie = 3 + """Represents a sticker with a lottie image.""" gif = 4 + """Represents a sticker with a gif image. + + .. versionadded:: 2.8 + """ @property def file_extension(self) -> str: @@ -620,175 +1052,442 @@ def file_extension(self) -> str: class InviteType(Enum): + """Represents the type of an invite. + + .. versionadded:: 2.10 + """ + guild = 0 + """Represents an invite to a guild.""" group_dm = 1 + """Represents an invite to a group channel.""" friend = 2 + """Represents a friend invite.""" class InviteTarget(Enum): + """Represents the invite type for voice channel invites. + + .. versionadded:: 2.0 + """ + unknown = 0 + """The invite doesn't target anyone or anything.""" stream = 1 + """A stream invite that targets a user.""" embedded_application = 2 + """A stream invite that targets an embedded application.""" class InteractionType(Enum): + """Specifies the type of :class:`Interaction`. + + .. versionadded:: 2.0 + """ + ping = 1 + """Represents Discord pinging to see if the interaction response server is alive.""" application_command = 2 + """Represents an application command interaction.""" component = 3 + """Represents a component based interaction, i.e. using the Discord Bot UI Kit.""" application_command_autocomplete = 4 + """Represents an application command autocomplete interaction.""" modal_submit = 5 + """Represents a modal submit interaction.""" class InteractionResponseType(Enum): + """Specifies the response type for the interaction. + + .. versionadded:: 2.0 + """ + pong = 1 + """Pongs the interaction when given a ping. + + See also :meth:`InteractionResponse.pong`. + """ channel_message = 4 + """Responds to the interaction with a message. + + See also :meth:`InteractionResponse.send_message`. + """ deferred_channel_message = 5 + """Responds to the interaction with a message at a later time. + + See also :meth:`InteractionResponse.defer`. + """ deferred_message_update = 6 + """Acknowledges the component interaction with a promise that + the message will update later (though there is no need to actually update the message). + + See also :meth:`InteractionResponse.defer`. + """ message_update = 7 + """Responds to the interaction by editing the message. + + See also :meth:`InteractionResponse.edit_message`. + """ application_command_autocomplete_result = 8 + """Responds to the autocomplete interaction with suggested choices. + + See also :meth:`InteractionResponse.autocomplete`. + """ modal = 9 + """Responds to the interaction by displaying a modal. + + See also :meth:`InteractionResponse.send_modal`. + + .. versionadded:: 2.4 + """ premium_required = 10 + """Responds to the interaction with a message containing an upgrade button. + Only available for applications with monetization enabled. + + See also :meth:`InteractionResponse.require_premium`. + + .. versionadded:: 2.10 + """ class VideoQualityMode(Enum): + """Represents the camera video quality mode for voice channel participants. + + .. versionadded:: 2.0 + """ + auto = 1 + """Represents auto camera video quality.""" full = 2 + """Represents full camera video quality.""" def __int__(self) -> int: return self.value class ComponentType(Enum): + """Represents the type of component. + + .. versionadded:: 2.0 + """ + action_row = 1 + """Represents the group component which holds different components in a row.""" button = 2 + """Represents a button component.""" string_select = 3 + """Represents a string select component. + + .. versionadded:: 2.7 + """ select = 3 # backwards compatibility + """An alias of :attr:`string_select`.""" text_input = 4 + """Represents a text input component.""" user_select = 5 + """Represents a user select component. + + .. versionadded:: 2.7 + """ role_select = 6 + """Represents a role select component. + + .. versionadded:: 2.7 + """ mentionable_select = 7 + """Represents a mentionable (user/member/role) select component. + + .. versionadded:: 2.7 + """ channel_select = 8 + """Represents a channel select component. + + .. versionadded:: 2.7 + """ def __int__(self) -> int: return self.value class ButtonStyle(Enum): + """Represents the style of the button component. + + .. versionadded:: 2.0 + """ + primary = 1 + """Represents a blurple button for the primary action.""" secondary = 2 + """Represents a grey button for the secondary action.""" success = 3 + """Represents a green button for a successful action.""" danger = 4 + """Represents a red button for a dangerous action.""" link = 5 + """Represents a link button.""" # Aliases blurple = 1 + """An alias for :attr:`primary`.""" grey = 2 + """An alias for :attr:`secondary`.""" gray = 2 + """An alias for :attr:`secondary`.""" green = 3 + """An alias for :attr:`success`.""" red = 4 + """An alias for :attr:`danger`.""" url = 5 + """An alias for :attr:`link`.""" def __int__(self) -> int: return self.value class TextInputStyle(Enum): + """Represents a style of the text input component. + + .. versionadded:: 2.4 + """ + short = 1 + """Represents a single-line text input component.""" paragraph = 2 + """Represents a multi-line text input component.""" + # Aliases single_line = 1 + """An alias for :attr:`short`.""" multi_line = 2 + """An alias for :attr:`paragraph`.""" long = 2 + """An alias for :attr:`paragraph`.""" def __int__(self) -> int: return self.value class SelectDefaultValueType(Enum): + """Represents the type of a :class:`SelectDefaultValue`. + + .. versionadded:: 2.10 + """ + user = "user" + """Represents a user/member.""" role = "role" + """Represents a role.""" channel = "channel" + """Represents a channel.""" def __str__(self) -> str: return self.value class ApplicationCommandType(Enum): + """Represents the type of an application command. + + .. versionadded:: 2.1 + """ + chat_input = 1 + """Represents a slash command.""" user = 2 + """Represents a user command from the context menu.""" message = 3 + """Represents a message command from the context menu.""" class ApplicationCommandPermissionType(Enum): + """Represents the type of a permission of an application command. + + .. versionadded:: 2.5 + """ + role = 1 + """Represents a permission that affects roles.""" user = 2 + """Represents a permission that affects users.""" channel = 3 + """Represents a permission that affects channels.""" def __int__(self) -> int: return self.value class OptionType(Enum): + """Represents the type of an option. + + .. versionadded:: 2.1 + """ + sub_command = 1 + """Represents a sub command of the main command or group.""" sub_command_group = 2 + """Represents a sub command group of the main command.""" string = 3 + """Represents a string option.""" integer = 4 + """Represents an integer option.""" boolean = 5 + """Represents a boolean option.""" user = 6 + """Represents a user option.""" channel = 7 + """Represents a channel option.""" role = 8 + """Represents a role option.""" mentionable = 9 + """Represents a role + user option.""" number = 10 + """Represents a float option.""" attachment = 11 + """Represents an attachment option. + + .. versionadded:: 2.4 + """ class StagePrivacyLevel(Enum): + """Represents a stage instance's privacy level. + + .. versionadded:: 2.0 + """ + public = 1 + """The stage instance can be joined by external users. + + .. deprecated:: 2.5 + Public stages are no longer supported by discord. + """ closed = 2 + """The stage instance can only be joined by members of the guild.""" guild_only = 2 + """Alias for :attr:`.closed`""" class NSFWLevel(Enum, comparable=True): + """Represents the NSFW level of a guild. + + .. versionadded:: 2.0 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two NSFW levels are equal. + .. describe:: x != y + + Checks if two NSFW levels are not equal. + .. describe:: x > y + + Checks if an NSFW level is higher than another. + .. describe:: x < y + + Checks if an NSFW level is lower than another. + .. describe:: x >= y + + Checks if an NSFW level is higher or equal to another. + .. describe:: x <= y + + Checks if an NSFW level is lower or equal to another. + """ + default = 0 + """The guild has not been categorised yet.""" explicit = 1 + """The guild contains NSFW content.""" safe = 2 + """The guild does not contain any NSFW content.""" age_restricted = 3 + """The guild may contain NSFW content.""" class GuildScheduledEventEntityType(Enum): + """Represents the type of a guild scheduled event entity. + + .. versionadded:: 2.3 + """ + stage_instance = 1 + """The guild scheduled event will take place in a stage channel.""" voice = 2 + """The guild scheduled event will take place in a voice channel.""" external = 3 + """The guild scheduled event will take place in a custom location.""" class GuildScheduledEventStatus(Enum): + """Represents the status of a guild scheduled event. + + .. versionadded:: 2.3 + """ + scheduled = 1 + """Represents a scheduled event.""" active = 2 + """Represents an active event.""" completed = 3 + """Represents a completed event.""" canceled = 4 + """Represents a canceled event.""" cancelled = 4 + """An alias for :attr:`canceled`. + + .. versionadded:: 2.6 + """ class GuildScheduledEventPrivacyLevel(Enum): + """Represents the privacy level of a guild scheduled event. + + .. versionadded:: 2.3 + """ + guild_only = 2 + """The guild scheduled event is only for a specific guild.""" class ThreadArchiveDuration(Enum): + """Represents the automatic archive duration of a thread in minutes. + + .. versionadded:: 2.3 + """ + hour = 60 + """The thread will archive after an hour of inactivity.""" day = 1440 + """The thread will archive after a day of inactivity.""" three_days = 4320 + """The thread will archive after three days of inactivity.""" week = 10080 + """The thread will archive after a week of inactivity.""" def __int__(self) -> int: return self.value class WidgetStyle(Enum): + """Represents the supported widget image styles. + + .. versionadded:: 2.5 + """ + shield = "shield" + """A shield style image with a Discord icon and the online member count.""" banner1 = "banner1" + """A large image with guild icon, name and online member count and a footer.""" banner2 = "banner2" + """A small image with guild icon, name and online member count.""" banner3 = "banner3" + """A large image with guild icon, name and online member count and a footer, + with a "Chat Now" label on the right. + """ banner4 = "banner4" + """A large image with a large Discord logo, guild icon, name and online member count, + with a "Join My Server" label at the bottom. + """ def __str__(self) -> str: return self.value @@ -796,112 +1495,181 @@ def __str__(self) -> str: # reference: https://discord.com/developers/docs/reference#locales class Locale(Enum): + """Represents supported locales by Discord. + + .. versionadded:: 2.5 + """ + bg = "bg" - "Bulgarian | български" + """The ``bg`` (Bulgarian) locale.""" cs = "cs" - "Czech | Čeština" + """The ``cs`` (Czech) locale.""" da = "da" - "Danish | Dansk" + """The ``da`` (Danish) locale.""" de = "de" - "German | Deutsch" + """The ``de`` (German) locale.""" el = "el" - "Greek | Ελληνικά" + """The ``el`` (Greek) locale.""" en_GB = "en-GB" - "English, UK | English, UK" + """The ``en-GB`` (English, UK) locale.""" en_US = "en-US" - "English, US | English, US" + """The ``en-US`` (English, US) locale.""" es_ES = "es-ES" - "Spanish | Español" + """The ``es-ES`` (Spanish) locale.""" es_LATAM = "es-419" - "Spanish, LATAM | Español, LATAM" + """The ``es-419`` (Spanish, LATAM) locale. + + .. versionadded:: 2.10 + """ fi = "fi" - "Finnish | Suomi" + """The ``fi`` (Finnish) locale.""" fr = "fr" - "French | Français" + """The ``fr`` (French) locale.""" hi = "hi" - "Hindi | हिन्दी" + """The ``hi`` (Hindi) locale.""" hr = "hr" - "Croatian | Hrvatski" + """The ``hr`` (Croatian) locale.""" hu = "hu" - "Hungarian | Magyar" + """The ``hu`` (Hungarian) locale.""" id = "id" - "Indonesian | Bahasa Indonesia" + """The ``id`` (Indonesian) locale. + + .. versionadded:: 2.8 + """ it = "it" - "Italian | Italiano" + """The ``it`` (Italian) locale.""" ja = "ja" - "Japanese | 日本語" + """The ``ja`` (Japanese) locale.""" ko = "ko" - "Korean | 한국어" + """The ``ko`` (Korean) locale.""" lt = "lt" - "Lithuanian | Lietuviškai" + """The ``lt`` (Lithuanian) locale.""" nl = "nl" - "Dutch | Nederlands" + """The ``nl`` (Dutch) locale.""" no = "no" - "Norwegian | Norsk" + """The ``no`` (Norwegian) locale.""" pl = "pl" - "Polish | Polski" + """The ``pl`` (Polish) locale.""" pt_BR = "pt-BR" - "Portuguese, Brazilian | Português do Brasil" + """The ``pt-BR`` (Portuguese) locale.""" ro = "ro" - "Romanian, Romania | Română" + """The ``ro`` (Romanian) locale.""" ru = "ru" - "Russian | Pусский" # noqa: RUF001 + """The ``ru`` (Russian) locale.""" sv_SE = "sv-SE" - "Swedish | Svenska" + """The ``sv-SE`` (Swedish) locale.""" th = "th" - "Thai | ไทย" + """The ``th`` (Thai) locale.""" tr = "tr" - "Turkish | Türkçe" + """The ``tr`` (Turkish) locale.""" uk = "uk" - "Ukrainian | Українська" + """The ``uk`` (Ukrainian) locale.""" vi = "vi" - "Vietnamese | Tiếng Việt" + """The ``vi`` (Vietnamese) locale.""" zh_CN = "zh-CN" - "Chinese, China | 中文" + """The ``zh-CN`` (Chinese, China) locale.""" zh_TW = "zh-TW" - "Chinese, Taiwan | 繁體中文" + """The ``zh-TW`` (Chinese, Taiwan) locale.""" def __str__(self) -> str: return self.value class AutoModActionType(Enum): + """Represents the type of action an auto moderation rule will take upon execution. + + .. versionadded:: 2.6 + """ + block_message = 1 + """The rule will prevent matching messages from being posted.""" send_alert_message = 2 + """The rule will send an alert to a specified channel.""" timeout = 3 + """The rule will timeout the user that sent the message. + + .. note:: + This action type is only available for rules with trigger type + :attr:`~AutoModTriggerType.keyword` or :attr:`~AutoModTriggerType.mention_spam`, + and :attr:`~Permissions.moderate_members` permissions are required to use it. + """ class AutoModEventType(Enum): + """Represents the type of event/context an auto moderation rule will be checked in. + + .. versionadded:: 2.6 + """ + message_send = 1 + """The rule will apply when a member sends or edits a message in the guild.""" class AutoModTriggerType(Enum): + """Represents the type of content that can trigger an auto moderation rule. + + .. versionadded:: 2.6 + + .. versionchanged:: 2.9 + Removed obsolete ``harmful_link`` type. + """ + keyword = 1 + """The rule will filter messages based on a custom keyword list. + + This trigger type requires additional :class:`metadata `. + """ + if not TYPE_CHECKING: harmful_link = 2 # obsolete/deprecated + spam = 3 + """The rule will filter messages suspected of being spam.""" keyword_preset = 4 + """The rule will filter messages based on predefined lists containing commonly flagged words. + + This trigger type requires additional :class:`metadata `. + """ mention_spam = 5 + """The rule will filter messages based on the number of member/role mentions they contain. + + This trigger type requires additional :class:`metadata `. + """ class ThreadSortOrder(Enum): + """Represents the sort order of threads in a :class:`ForumChannel` or :class:`MediaChannel`. + + .. versionadded:: 2.6 + """ + latest_activity = 0 + """Sort forum threads by activity.""" creation_date = 1 + """Sort forum threads by creation date/time (from newest to oldest).""" class ThreadLayout(Enum): + """Represents the layout of threads in :class:`ForumChannel`\\s. + + .. versionadded:: 2.8 + """ + not_set = 0 + """No preferred layout has been set.""" list_view = 1 + """Display forum threads in a text-focused list.""" gallery_view = 2 + """Display forum threads in a media-focused collection of tiles.""" class Event(Enum): - """Represents all the events of the library. + """ + Represents all the events of the library. These offer to register listeners/events in a more pythonic way; additionally autocompletion and documentation are both supported. .. versionadded:: 2.8 - """ connect = "connect" @@ -1412,55 +2180,127 @@ class Event(Enum): class ApplicationRoleConnectionMetadataType(Enum): + """Represents the type of a role connection metadata value. + + These offer comparison operations, which allow guilds to configure role requirements + based on the metadata value for each user and a guild-specified configured value. + + .. versionadded:: 2.8 + """ + integer_less_than_or_equal = 1 + """The metadata value (``integer``) is less than or equal to the guild's configured value.""" integer_greater_than_or_equal = 2 + """The metadata value (``integer``) is greater than or equal to the guild's configured value.""" integer_equal = 3 + """The metadata value (``integer``) is equal to the guild's configured value.""" integer_not_equal = 4 + """The metadata value (``integer``) is not equal to the guild's configured value.""" datetime_less_than_or_equal = 5 + """The metadata value (``ISO8601 string``) is less than or equal to the guild's configured value (``integer``; days before current date).""" datetime_greater_than_or_equal = 6 + """The metadata value (``ISO8601 string``) is greater than or equal to the guild's configured value (``integer``; days before current date).""" boolean_equal = 7 + """The metadata value (``integer``) is equal to the guild's configured value.""" boolean_not_equal = 8 + """The metadata value (``integer``) is not equal to the guild's configured value.""" class OnboardingPromptType(Enum): + """Represents the type of onboarding prompt. + + .. versionadded:: 2.9 + """ + multiple_choice = 0 + """The prompt is a multiple choice prompt.""" dropdown = 1 + """The prompt is a dropdown prompt.""" class SKUType(Enum): + """Represents the type of an SKU. + + .. versionadded:: 2.10 + """ + durable = 2 + """Represents a durable one-time purchase.""" consumable = 3 + """Represents a consumable one-time purchase.""" subscription = 5 + """Represents a recurring subscription.""" subscription_group = 6 + """Represents a system-generated group for each :attr:`subscription` SKU.""" class EntitlementType(Enum): + """Represents the type of an entitlement. + + .. versionadded:: 2.10 + """ + purchase = 1 + """Represents an entitlement purchased by a user.""" premium_subscription = 2 + """Represents an entitlement for a Discord Nitro subscription.""" developer_gift = 3 + """Represents an entitlement gifted by the application developer.""" test_mode_purchase = 4 + """Represents an entitlement purchased by a developer in application test mode.""" free_purchase = 5 + """Represents an entitlement granted when the SKU was free.""" user_gift = 6 + """Represents an entitlement gifted by another user.""" premium_purchase = 7 + """Represents an entitlement claimed by a user for free as a Discord Nitro subscriber.""" application_subscription = 8 + """Represents an entitlement for an application subscription.""" class SubscriptionStatus(Enum): + """Represents the status of a subscription. + + .. versionadded:: 2.10 + """ + active = 0 + """Represents an active Subscription which is scheduled to renew.""" ending = 1 + """Represents an active Subscription which will not renew.""" inactive = 2 + """Represents an inactive Subscription which is not being charged.""" class PollLayoutType(Enum): + """Specifies the layout of a :class:`Poll`. + + .. versionadded:: 2.10 + """ + default = 1 + """The default poll layout type.""" class VoiceChannelEffectAnimationType(Enum): + """The type of an emoji reaction effect animation in a voice channel. + + .. versionadded:: 2.10 + """ + premium = 0 + """A fun animation, sent by a Nitro subscriber.""" basic = 1 + """A standard animation.""" class MessageReferenceType(Enum): + """Specifies the type of :class:`MessageReference`. This can be used to determine + if a message is e.g. a reply or a forwarded message. + + .. versionadded:: 2.10 + """ + default = 0 """A standard message reference used in message replies.""" forward = 1 diff --git a/disnake/ext/commands/cooldowns.py b/disnake/ext/commands/cooldowns.py index aa364948f2..971af2bd0f 100644 --- a/disnake/ext/commands/cooldowns.py +++ b/disnake/ext/commands/cooldowns.py @@ -27,13 +27,25 @@ class BucketType(Enum): + """Specifies a type of bucket for, e.g. a cooldown.""" + default = 0 + """The default bucket operates on a global basis.""" user = 1 + """The user bucket operates on a per-user basis.""" guild = 2 + """The guild bucket operates on a per-guild basis.""" channel = 3 + """The channel bucket operates on a per-channel basis.""" member = 4 + """The member bucket operates on a per-member basis.""" category = 5 + """The category bucket operates on a per-category basis.""" role = 6 + """The role bucket operates on a per-role basis. + + .. versionadded:: 1.3 + """ def get_key(self, msg: Message) -> Any: if self is BucketType.user: diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py index 866566af3b..17fe65531f 100644 --- a/disnake/ext/commands/flags.py +++ b/disnake/ext/commands/flags.py @@ -35,16 +35,16 @@ class CommandSyncFlags(BaseFlags): Checks if two CommandSyncFlags instances are not equal. .. describe:: x <= y - Checks if an CommandSyncFlags instance is a subset of another CommandSyncFlags instance. + Checks if a CommandSyncFlags instance is a subset of another CommandSyncFlags instance. .. describe:: x >= y - Checks if an CommandSyncFlags instance is a superset of another CommandSyncFlags instance. + Checks if a CommandSyncFlags instance is a superset of another CommandSyncFlags instance. .. describe:: x < y - Checks if an CommandSyncFlags instance is a strict subset of another CommandSyncFlags instance. + Checks if a CommandSyncFlags instance is a strict subset of another CommandSyncFlags instance. .. describe:: x > y - Checks if an CommandSyncFlags instance is a strict superset of another CommandSyncFlags instance. + Checks if a CommandSyncFlags instance is a strict superset of another CommandSyncFlags instance. .. describe:: x | y, x |= y Returns a new CommandSyncFlags instance with all enabled flags from both x and y. diff --git a/docs/api/activities.rst b/docs/api/activities.rst index cc2e7414c4..6dd6b34bf2 100644 --- a/docs/api/activities.rst +++ b/docs/api/activities.rst @@ -74,67 +74,14 @@ Enumerations ActivityType ~~~~~~~~~~~~ -.. class:: ActivityType - - Specifies the type of :class:`Activity`. This is used to check how to - interpret the activity itself. - - .. attribute:: unknown - - An unknown activity type. This should generally not happen. - .. attribute:: playing - - A "Playing" activity type. - .. attribute:: streaming - - A "Streaming" activity type. - .. attribute:: listening - - A "Listening" activity type. - .. attribute:: watching - - A "Watching" activity type. - .. attribute:: custom - - A custom activity type. - .. attribute:: competing - - A competing activity type. - - .. versionadded:: 1.5 +.. autoclass:: ActivityType() + :members: Status ~~~~~~ -.. class:: Status - - Specifies a :class:`Member` 's status. - - .. attribute:: online - - The member is online. - .. attribute:: offline - - The member is offline. - .. attribute:: idle - - The member is idle. - .. attribute:: dnd - - The member is "Do Not Disturb". - .. attribute:: do_not_disturb - - An alias for :attr:`dnd`. - .. attribute:: invisible - - The member is "invisible". In reality, this is only used in sending - a presence a la :meth:`Client.change_presence`. When you receive a - user's presence this will be :attr:`offline` instead. - .. attribute:: streaming - - The member is live streaming to Twitch or YouTube. - - .. versionadded:: 2.3 +.. autoclass:: Status() + :members: Events ------ diff --git a/docs/api/app_commands.rst b/docs/api/app_commands.rst index e49bc47ebf..8511c5e764 100644 --- a/docs/api/app_commands.rst +++ b/docs/api/app_commands.rst @@ -130,85 +130,20 @@ Enumerations OptionType ~~~~~~~~~~ -.. class:: OptionType - - Represents the type of an option. - - .. versionadded:: 2.1 - - .. attribute:: sub_command - - Represents a sub command of the main command or group. - .. attribute:: sub_command_group - - Represents a sub command group of the main command. - .. attribute:: string - - Represents a string option. - .. attribute:: integer - - Represents an integer option. - .. attribute:: boolean - - Represents a boolean option. - .. attribute:: user - - Represents a user option. - .. attribute:: channel - - Represents a channel option. - .. attribute:: role - - Represents a role option. - .. attribute:: mentionable - - Represents a role + user option. - .. attribute:: number - - Represents a float option. - .. attribute:: attachment - - Represents an attachment option. - - .. versionadded:: 2.4 +.. autoclass:: OptionType() + :members: ApplicationCommandType ~~~~~~~~~~~~~~~~~~~~~~ -.. class:: ApplicationCommandType - - Represents the type of an application command. - - .. versionadded:: 2.1 - - .. attribute:: chat_input - - Represents a slash command. - .. attribute:: user - - Represents a user command from the context menu. - .. attribute:: message - - Represents a message command from the context menu. +.. autoclass:: ApplicationCommandType() + :members: ApplicationCommandPermissionType ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: ApplicationCommandPermissionType - - Represents the type of a permission of an application command. - - .. versionadded:: 2.5 - - .. attribute:: role - - Represents a permission that affects roles. - .. attribute:: user - - Represents a permission that affects users. - .. attribute:: channel - - Represents a permission that affects channels. +.. autoclass:: ApplicationCommandPermissionType() + :members: Events ------ diff --git a/docs/api/app_info.rst b/docs/api/app_info.rst index 3b10b6c553..ba1d053784 100644 --- a/docs/api/app_info.rst +++ b/docs/api/app_info.rst @@ -84,81 +84,17 @@ Enumerations TeamMembershipState ~~~~~~~~~~~~~~~~~~~ -.. class:: TeamMembershipState - - Represents the membership state of a team member retrieved through :func:`Client.application_info`. - - .. versionadded:: 1.3 - - .. attribute:: invited - - Represents an invited member. - - .. attribute:: accepted - - Represents a member currently in the team. +.. autoclass:: TeamMembershipState() + :members: TeamMemberRole ~~~~~~~~~~~~~~ -.. class:: TeamMemberRole - - Represents the role of a team member retrieved through :func:`Client.application_info`. - - .. versionadded:: 2.10 - - .. attribute:: admin - - Admins have the most permissions. An admin can only take destructive actions on the team or team-owned apps if they are the team owner. - - .. attribute:: developer - - Developers can access information about a team and team-owned applications, and take limited actions on them, like configuring interaction endpoints or resetting the bot token. - - .. attribute:: read_only - - Read-only members can access information about a team and team-owned applications. +.. autoclass:: TeamMemberRole() + :members: ApplicationRoleConnectionMetadataType ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: ApplicationRoleConnectionMetadataType - - Represents the type of a role connection metadata value. - - These offer comparison operations, which allow guilds to configure role requirements - based on the metadata value for each user and a guild-specified configured value. - - .. versionadded:: 2.8 - - .. attribute:: integer_less_than_or_equal - - The metadata value (``integer``) is less than or equal to the guild's configured value. - - .. attribute:: integer_greater_than_or_equal - - The metadata value (``integer``) is greater than or equal to the guild's configured value. - - .. attribute:: integer_equal - - The metadata value (``integer``) is equal to the guild's configured value. - - .. attribute:: integer_not_equal - - The metadata value (``integer``) is not equal to the guild's configured value. - - .. attribute:: datetime_less_than_or_equal - - The metadata value (``ISO8601 string``) is less than or equal to the guild's configured value (``integer``; days before current date). - - .. attribute:: datetime_greater_than_or_equal - - The metadata value (``ISO8601 string``) is greater than or equal to the guild's configured value (``integer``; days before current date). - - .. attribute:: boolean_equal - - The metadata value (``integer``) is equal to the guild's configured value. - - .. attribute:: boolean_not_equal - - The metadata value (``integer``) is not equal to the guild's configured value. +.. autoclass:: ApplicationRoleConnectionMetadataType() + :members: diff --git a/docs/api/audit_logs.rst b/docs/api/audit_logs.rst index 448a3ffcf5..dc1dc4e263 100644 --- a/docs/api/audit_logs.rst +++ b/docs/api/audit_logs.rst @@ -1754,23 +1754,8 @@ AuditLogAction AuditLogActionCategory ~~~~~~~~~~~~~~~~~~~~~~ -.. class:: AuditLogActionCategory - - Represents the category that the :class:`AuditLogAction` belongs to. - - This can be retrieved via :attr:`AuditLogEntry.category`. - - .. attribute:: create - - The action is the creation of something. - - .. attribute:: delete - - The action is the deletion of something. - - .. attribute:: update - - The action is the update of something. +.. autoclass:: AuditLogActionCategory() + :members: Events ------ diff --git a/docs/api/automod.rst b/docs/api/automod.rst index 1f1053674f..7e21b873b8 100644 --- a/docs/api/automod.rst +++ b/docs/api/automod.rst @@ -83,75 +83,20 @@ Enumerations AutoModActionType ~~~~~~~~~~~~~~~~~ -.. class:: AutoModActionType - - Represents the type of action an auto moderation rule will take upon execution. - - .. versionadded:: 2.6 - - .. attribute:: block_message - - The rule will prevent matching messages from being posted. - - .. attribute:: send_alert_message - - The rule will send an alert to a specified channel. - - .. attribute:: timeout - - The rule will timeout the user that sent the message. - - .. note:: - This action type is only available for rules with trigger type - :attr:`~AutoModTriggerType.keyword` or :attr:`~AutoModTriggerType.mention_spam`, - and :attr:`~Permissions.moderate_members` permissions are required to use it. +.. autoclass:: AutoModActionType() + :members: AutoModEventType ~~~~~~~~~~~~~~~~ -.. class:: AutoModEventType - - Represents the type of event/context an auto moderation rule will be checked in. - - .. versionadded:: 2.6 - - .. attribute:: message_send - - The rule will apply when a member sends or edits a message in the guild. +.. autoclass:: AutoModEventType() + :members: AutoModTriggerType ~~~~~~~~~~~~~~~~~~ -.. class:: AutoModTriggerType - - Represents the type of content that can trigger an auto moderation rule. - - .. versionadded:: 2.6 - - .. versionchanged:: 2.9 - Removed obsolete ``harmful_link`` type. - - .. attribute:: keyword - - The rule will filter messages based on a custom keyword list. - - This trigger type requires additional :class:`metadata `. - - .. attribute:: spam - - The rule will filter messages suspected of being spam. - - .. attribute:: keyword_preset - - The rule will filter messages based on predefined lists containing commonly flagged words. - - This trigger type requires additional :class:`metadata `. - - .. attribute:: mention_spam - - The rule will filter messages based on the number of member/role mentions they contain. - - This trigger type requires additional :class:`metadata `. +.. autoclass:: AutoModTriggerType() + :members: Events ------ diff --git a/docs/api/channels.rst b/docs/api/channels.rst index 3086583883..8e8729ccfb 100644 --- a/docs/api/channels.rst +++ b/docs/api/channels.rst @@ -206,150 +206,32 @@ Enumerations ChannelType ~~~~~~~~~~~ -.. class:: ChannelType - - Specifies the type of channel. - - .. attribute:: text - - A text channel. - .. attribute:: voice - - A voice channel. - .. attribute:: private - - A private text channel. Also called a direct message. - .. attribute:: group - - A private group text channel. - .. attribute:: category - - A category channel. - .. attribute:: news - - A guild news channel. - - .. attribute:: stage_voice - - A guild stage voice channel. - - .. versionadded:: 1.7 - - .. attribute:: news_thread - - A news thread. - - .. versionadded:: 2.0 - - .. attribute:: public_thread - - A public thread. - - .. versionadded:: 2.0 - - .. attribute:: private_thread - - A private thread. - - .. versionadded:: 2.0 - - .. attribute:: guild_directory - - A student hub channel. - - .. versionadded:: 2.1 - - .. attribute:: forum - - A channel of only threads. - - .. versionadded:: 2.5 - - .. attribute:: media - - A channel of only threads but with a focus on media, similar to forum channels. - - .. versionadded:: 2.10 +.. autoclass:: ChannelType() + :members: ThreadArchiveDuration ~~~~~~~~~~~~~~~~~~~~~ -.. class:: ThreadArchiveDuration - - Represents the automatic archive duration of a thread in minutes. - - .. versionadded:: 2.3 - - .. attribute:: hour - - The thread will archive after an hour of inactivity. - - .. attribute:: day - - The thread will archive after a day of inactivity. - - .. attribute:: three_days - - The thread will archive after three days of inactivity. - - .. attribute:: week - - The thread will archive after a week of inactivity. +.. autoclass:: ThreadArchiveDuration() + :members: VideoQualityMode ~~~~~~~~~~~~~~~~ -.. class:: VideoQualityMode - - Represents the camera video quality mode for voice channel participants. - - .. versionadded:: 2.0 - - .. attribute:: auto - - Represents auto camera video quality. - - .. attribute:: full - - Represents full camera video quality. +.. autoclass:: VideoQualityMode() + :members: ThreadSortOrder ~~~~~~~~~~~~~~~ -.. class:: ThreadSortOrder - - Represents the sort order of threads in a :class:`ForumChannel` or :class:`MediaChannel`. - - .. versionadded:: 2.6 - - .. attribute:: latest_activity - - Sort forum threads by activity. - - .. attribute:: creation_date - - Sort forum threads by creation date/time (from newest to oldest). +.. autoclass:: ThreadSortOrder() + :members: ThreadLayout ~~~~~~~~~~~~ -.. class:: ThreadLayout - - Represents the layout of threads in :class:`ForumChannel`\s. - - .. versionadded:: 2.8 - - .. attribute:: not_set - - No preferred layout has been set. - - .. attribute:: list_view - - Display forum threads in a text-focused list. - - .. attribute:: gallery_view - - Display forum threads in a media-focused collection of tiles. +.. autoclass:: ThreadLayout() + :members: Events ------ diff --git a/docs/api/components.rst b/docs/api/components.rst index 628d6c6430..14f1153679 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -129,137 +129,23 @@ Enumerations ComponentType ~~~~~~~~~~~~~ -.. class:: ComponentType - - Represents the type of component. - - .. versionadded:: 2.0 - - .. attribute:: action_row - - Represents the group component which holds different components in a row. - .. attribute:: button - - Represents a button component. - .. attribute:: string_select - - Represents a string select component. - - .. versionadded:: 2.7 - .. attribute:: select - - An alias of :attr:`string_select`. - - .. attribute:: text_input - - Represents a text input component. - .. attribute:: user_select - - Represents a user select component. - - .. versionadded:: 2.7 - .. attribute:: role_select - - Represents a role select component. - - .. versionadded:: 2.7 - .. attribute:: mentionable_select - - Represents a mentionable (user/member/role) select component. - - .. versionadded:: 2.7 - .. attribute:: channel_select - - Represents a channel select component. - - .. versionadded:: 2.7 +.. autoclass:: ComponentType() + :members: ButtonStyle ~~~~~~~~~~~ -.. class:: ButtonStyle - - Represents the style of the button component. - - .. versionadded:: 2.0 - - .. attribute:: primary - - Represents a blurple button for the primary action. - .. attribute:: secondary - - Represents a grey button for the secondary action. - .. attribute:: success - - Represents a green button for a successful action. - .. attribute:: danger - - Represents a red button for a dangerous action. - .. attribute:: link - - Represents a link button. - - .. attribute:: blurple - - An alias for :attr:`primary`. - .. attribute:: grey - - An alias for :attr:`secondary`. - .. attribute:: gray - - An alias for :attr:`secondary`. - .. attribute:: green - - An alias for :attr:`success`. - .. attribute:: red - - An alias for :attr:`danger`. - .. attribute:: url - - An alias for :attr:`link`. +.. autoclass:: ButtonStyle() + :members: TextInputStyle ~~~~~~~~~~~~~~ -.. class:: TextInputStyle - - Represents a style of the text input component. - - .. versionadded:: 2.4 - - .. attribute:: short - - Represents a single-line text input component. - .. attribute:: paragraph - - Represents a multi-line text input component. - .. attribute:: single_line - - An alias for :attr:`short`. - .. attribute:: multi_line - - An alias for :attr:`paragraph`. - .. attribute:: long - - An alias for :attr:`paragraph`. +.. autoclass:: TextInputStyle() + :members: SelectDefaultValueType ~~~~~~~~~~~~~~~~~~~~~~ -.. class:: SelectDefaultValueType - - Represents the type of a :class:`SelectDefaultValue`. - - .. versionadded:: 2.10 - - .. attribute:: user - - Represents a user/member. - - .. attribute:: role - - Represents a role. - - .. attribute:: channel - - Represents a channel. +.. autoclass:: SelectDefaultValueType() + :members: diff --git a/docs/api/entitlements.rst b/docs/api/entitlements.rst index e8b0c4aab4..6a1daeb9c0 100644 --- a/docs/api/entitlements.rst +++ b/docs/api/entitlements.rst @@ -25,43 +25,8 @@ Enumerations EntitlementType ~~~~~~~~~~~~~~~ -.. class:: EntitlementType - - Represents the type of an entitlement. - - .. versionadded:: 2.10 - - .. attribute:: purchase - - Represents an entitlement purchased by a user. - - .. attribute:: premium_subscription - - Represents an entitlement for a Discord Nitro subscription. - - .. attribute:: developer_gift - - Represents an entitlement gifted by the application developer. - - .. attribute:: test_mode_purchase - - Represents an entitlement purchased by a developer in application test mode. - - .. attribute:: free_purchase - - Represents an entitlement granted when the SKU was free. - - .. attribute:: user_gift - - Represents an entitlement gifted by another user. - - .. attribute:: premium_purchase - - Represents an entitlement claimed by a user for free as a Discord Nitro subscriber. - - .. attribute:: application_subscription - - Represents an entitlement for an application subscription. +.. autoclass:: EntitlementType() + :members: Events ------ diff --git a/docs/api/events.rst b/docs/api/events.rst index 99599fa400..f147faccd3 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -1634,5 +1634,5 @@ Enumerations Event ~~~~~ -.. autoclass:: Event +.. autoclass:: Event() :members: diff --git a/docs/api/guild_scheduled_events.rst b/docs/api/guild_scheduled_events.rst index 8bc8c6985f..f58625ec19 100644 --- a/docs/api/guild_scheduled_events.rst +++ b/docs/api/guild_scheduled_events.rst @@ -43,67 +43,20 @@ Enumerations GuildScheduledEventEntityType ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: GuildScheduledEventEntityType - - Represents the type of a guild scheduled event entity. - - .. versionadded:: 2.3 - - .. attribute:: stage_instance - - The guild scheduled event will take place in a stage channel. - - .. attribute:: voice - - The guild scheduled event will take place in a voice channel. - - .. attribute:: external - - The guild scheduled event will take place in a custom location. +.. autoclass:: GuildScheduledEventEntityType() + :members: GuildScheduledEventStatus ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: GuildScheduledEventStatus - - Represents the status of a guild scheduled event. - - .. versionadded:: 2.3 - - .. attribute:: scheduled - - Represents a scheduled event. - - .. attribute:: active - - Represents an active event. - - .. attribute:: completed - - Represents a completed event. - - .. attribute:: canceled - - Represents a canceled event. - - .. attribute:: cancelled - - An alias for :attr:`canceled`. - - .. versionadded:: 2.6 +.. autoclass:: GuildScheduledEventStatus() + :members: GuildScheduledEventPrivacyLevel ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: GuildScheduledEventPrivacyLevel - - Represents the privacy level of a guild scheduled event. - - .. versionadded:: 2.3 - - .. attribute:: guild_only - - The guild scheduled event is only for a specific guild. +.. autoclass:: GuildScheduledEventPrivacyLevel() + :members: Events ------ diff --git a/docs/api/guilds.rst b/docs/api/guilds.rst index 614ad3f355..25aaaa6f6d 100644 --- a/docs/api/guilds.rst +++ b/docs/api/guilds.rst @@ -146,192 +146,32 @@ Enumerations VerificationLevel ~~~~~~~~~~~~~~~~~ -.. class:: VerificationLevel - - Specifies a :class:`Guild`\'s verification level, which is the criteria in - which a member must meet before being able to send messages to the guild. - - .. collapse:: operations - - .. versionadded:: 2.0 - - .. describe:: x == y - - Checks if two verification levels are equal. - .. describe:: x != y - - Checks if two verification levels are not equal. - .. describe:: x > y - - Checks if a verification level is higher than another. - .. describe:: x < y - - Checks if a verification level is lower than another. - .. describe:: x >= y - - Checks if a verification level is higher or equal to another. - .. describe:: x <= y - - Checks if a verification level is lower or equal to another. - - .. attribute:: none - - No criteria set. - .. attribute:: low - - Member must have a verified email on their Discord account. - .. attribute:: medium - - Member must have a verified email and be registered on Discord for more - than five minutes. - .. attribute:: high - - Member must have a verified email, be registered on Discord for more - than five minutes, and be a member of the guild itself for more than - ten minutes. - .. attribute:: highest - - Member must have a verified phone on their Discord account. +.. autoclass:: VerificationLevel() + :members: NotificationLevel ~~~~~~~~~~~~~~~~~ -.. class:: NotificationLevel - - Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default. - - .. collapse:: operations - - .. describe:: x == y - - Checks if two notification levels are equal. - .. describe:: x != y - - Checks if two notification levels are not equal. - .. describe:: x > y - - Checks if a notification level is higher than another. - .. describe:: x < y - - Checks if a notification level is lower than another. - .. describe:: x >= y - - Checks if a notification level is higher or equal to another. - .. describe:: x <= y - - Checks if a notification level is lower or equal to another. - - .. attribute:: all_messages - - Members receive notifications for every message regardless of them being mentioned. - .. attribute:: only_mentions - - Members receive notifications for messages they are mentioned in. +.. autoclass:: NotificationLevel() + :members: ContentFilter ~~~~~~~~~~~~~ -.. class:: ContentFilter - - Specifies a :class:`Guild`\'s explicit content filter, which is the machine - learning algorithms that Discord uses to detect if an image contains - NSFW content. - - .. collapse:: operations - - .. describe:: x == y - - Checks if two content filter levels are equal. - .. describe:: x != y - - Checks if two content filter levels are not equal. - .. describe:: x > y - - Checks if a content filter level is higher than another. - .. describe:: x < y - - Checks if a content filter level is lower than another. - .. describe:: x >= y - - Checks if a content filter level is higher or equal to another. - .. describe:: x <= y - - Checks if a content filter level is lower or equal to another. - - .. attribute:: disabled - - The guild does not have the content filter enabled. - .. attribute:: no_role - - The guild has the content filter enabled for members without a role. - .. attribute:: all_members - - The guild has the content filter enabled for every member. +.. autoclass:: ContentFilter() + :members: NSFWLevel ~~~~~~~~~ -.. class:: NSFWLevel - - Represents the NSFW level of a guild. - - .. versionadded:: 2.0 - - .. collapse:: operations - - .. describe:: x == y - - Checks if two NSFW levels are equal. - .. describe:: x != y - - Checks if two NSFW levels are not equal. - .. describe:: x > y - - Checks if a NSFW level is higher than another. - .. describe:: x < y - - Checks if a NSFW level is lower than another. - .. describe:: x >= y - - Checks if a NSFW level is higher or equal to another. - .. describe:: x <= y - - Checks if a NSFW level is lower or equal to another. - - .. attribute:: default - - The guild has not been categorised yet. - - .. attribute:: explicit - - The guild contains NSFW content. - - .. attribute:: safe - - The guild does not contain any NSFW content. - - .. attribute:: age_restricted - - The guild may contain NSFW content. - +.. autoclass:: NSFWLevel() + :members: OnboardingPromptType ~~~~~~~~~~~~~~~~~~~~ -.. class:: OnboardingPromptType - - Represents the type of onboarding prompt. - - .. versionadded:: 2.9 - - .. attribute:: multiple_choice - - The prompt is a multiple choice prompt. - - .. attribute:: dropdown - - The prompt is a dropdown prompt. - +.. autoclass:: OnboardingPromptType() + :members: Events ------ diff --git a/docs/api/index.rst b/docs/api/index.rst index 0b04ea5e1d..0732544b14 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -96,7 +96,6 @@ Documents app_info audit_logs automod - ui channels clients components @@ -116,10 +115,11 @@ Documents permissions roles skus - subscriptions soundboard stage_instances stickers + subscriptions + ui users utilities voice diff --git a/docs/api/integrations.rst b/docs/api/integrations.rst index afbcc67728..4a5b9af43b 100644 --- a/docs/api/integrations.rst +++ b/docs/api/integrations.rst @@ -68,23 +68,8 @@ Enumerations ExpireBehaviour ~~~~~~~~~~~~~~~ -.. class:: ExpireBehaviour - - Represents the behaviour the :class:`Integration` should perform - when a user's subscription has finished. - - There is an alias for this called ``ExpireBehavior``. - - .. versionadded:: 1.4 - - .. attribute:: remove_role - - This will remove the :attr:`StreamIntegration.role` from the user - when their subscription is finished. - - .. attribute:: kick - - This will kick the user when their subscription is finished. +.. autoclass:: ExpireBehaviour() + :members: Events ------ diff --git a/docs/api/interactions.rst b/docs/api/interactions.rst index 67bf5a16ab..459fa74fa4 100644 --- a/docs/api/interactions.rst +++ b/docs/api/interactions.rst @@ -189,84 +189,14 @@ Enumerations InteractionType ~~~~~~~~~~~~~~~ -.. class:: InteractionType - - Specifies the type of :class:`Interaction`. - - .. versionadded:: 2.0 - - .. attribute:: ping - - Represents Discord pinging to see if the interaction response server is alive. - .. attribute:: application_command - - Represents an application command interaction. - .. attribute:: component - - Represents a component based interaction, i.e. using the Discord Bot UI Kit. - .. attribute:: application_command_autocomplete - - Represents an application command autocomplete interaction. - .. attribute:: modal_submit - - Represents a modal submit interaction. +.. autoclass:: InteractionType() + :members: InteractionResponseType ~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: InteractionResponseType - - Specifies the response type for the interaction. - - .. versionadded:: 2.0 - - .. attribute:: pong - - Pongs the interaction when given a ping. - - See also :meth:`InteractionResponse.pong` - .. attribute:: channel_message - - Responds to the interaction with a message. - - See also :meth:`InteractionResponse.send_message` - .. attribute:: deferred_channel_message - - Responds to the interaction with a message at a later time. - - See also :meth:`InteractionResponse.defer` - .. attribute:: deferred_message_update - - Acknowledges the component interaction with a promise that - the message will update later (though there is no need to actually update the message). - - See also :meth:`InteractionResponse.defer` - .. attribute:: message_update - - Responds to the interaction by editing the message. - - See also :meth:`InteractionResponse.edit_message` - .. attribute:: application_command_autocomplete_result - - Responds to the autocomplete interaction with suggested choices. - - See also :meth:`InteractionResponse.autocomplete` - .. attribute:: modal - - Responds to the interaction by displaying a modal. - - See also :meth:`InteractionResponse.send_modal` - - .. versionadded:: 2.4 - - .. attribute:: premium_required - - Responds to the interaction with a message containing an upgrade button. - Only available for applications with monetization enabled. - - See also :meth:`InteractionResponse.require_premium` - - .. versionadded:: 2.10 +.. autoclass:: InteractionResponseType() + :members: Events ------ diff --git a/docs/api/invites.rst b/docs/api/invites.rst index aa58074c2f..94d4cf9ad4 100644 --- a/docs/api/invites.rst +++ b/docs/api/invites.rst @@ -40,44 +40,14 @@ Enumerations InviteType ~~~~~~~~~~ -.. class:: InviteType - - Represents the type of an invite. - - .. versionadded:: 2.10 - - .. attribute:: guild - - Represents an invite to a guild. - - .. attribute:: group_dm - - Represents an invite to a group channel. - - .. attribute:: friend - - Represents a friend invite. +.. autoclass:: InviteType() + :members: InviteTarget ~~~~~~~~~~~~ -.. class:: InviteTarget - - Represents the invite type for voice channel invites. - - .. versionadded:: 2.0 - - .. attribute:: unknown - - The invite doesn't target anyone or anything. - - .. attribute:: stream - - A stream invite that targets a user. - - .. attribute:: embedded_application - - A stream invite that targets an embedded application. +.. autoclass:: InviteTarget() + :members: Events ------ diff --git a/docs/api/localization.rst b/docs/api/localization.rst index d9ab008d42..8c8e991fe7 100644 --- a/docs/api/localization.rst +++ b/docs/api/localization.rst @@ -43,140 +43,5 @@ Enumerations Locale ~~~~~~ -.. class:: Locale - - Represents supported locales by Discord. - - .. versionadded:: 2.5 - - .. attribute:: bg - - The ``bg`` (Bulgarian) locale. - - .. attribute:: cs - - The ``cs`` (Czech) locale. - - .. attribute:: da - - The ``da`` (Danish) locale. - - .. attribute:: de - - The ``de`` (German) locale. - - .. attribute:: el - - The ``el`` (Greek) locale. - - .. attribute:: en_GB - - The ``en-GB`` (English, UK) locale. - - .. attribute:: en_US - - The ``en-US`` (English, US) locale. - - .. attribute:: es_ES - - The ``es-ES`` (Spanish) locale. - - .. attribute:: es_LATAM - - The ``es-419`` (Spanish, LATAM) locale. - - .. versionadded:: 2.10 - - .. attribute:: fi - - The ``fi`` (Finnish) locale. - - .. attribute:: fr - - The ``fr`` (French) locale. - - .. attribute:: hi - - The ``hi`` (Hindi) locale. - - .. attribute:: hr - - The ``hr`` (Croatian) locale. - - .. attribute:: hu - - The ``hu`` (Hungarian) locale. - - .. attribute:: id - - The ``id`` (Indonesian) locale. - - .. versionadded:: 2.8 - - .. attribute:: it - - The ``it`` (Italian) locale. - - .. attribute:: ja - - The ``ja`` (Japanese) locale. - - .. attribute:: ko - - The ``ko`` (Korean) locale. - - .. attribute:: lt - - The ``lt`` (Lithuanian) locale. - - .. attribute:: nl - - The ``nl`` (Dutch) locale. - - .. attribute:: no - - The ``no`` (Norwegian) locale. - - .. attribute:: pl - - The ``pl`` (Polish) locale. - - .. attribute:: pt_BR - - The ``pt-BR`` (Portuguese) locale. - - .. attribute:: ro - - The ``ro`` (Romanian) locale. - - .. attribute:: ru - - The ``ru`` (Russian) locale. - - .. attribute:: sv_SE - - The ``sv-SE`` (Swedish) locale. - - .. attribute:: th - - The ``th`` (Thai) locale. - - .. attribute:: tr - - The ``tr`` (Turkish) locale. - - .. attribute:: uk - - The ``uk`` (Ukrainian) locale. - - .. attribute:: vi - - The ``vi`` (Vietnamese) locale. - - .. attribute:: zh_CN - - The ``zh-CN`` (Chinese, China) locale. - - .. attribute:: zh_TW - - The ``zh-TW`` (Chinese, Taiwan) locale. +.. autoclass:: Locale() + :members: diff --git a/docs/api/messages.rst b/docs/api/messages.rst index 2e616af310..64fde721c3 100644 --- a/docs/api/messages.rst +++ b/docs/api/messages.rst @@ -231,228 +231,20 @@ Enumerations MessageType ~~~~~~~~~~~ -.. class:: MessageType - - Specifies the type of :class:`Message`. This is used to denote if a message - is to be interpreted as a system message or a regular message. - - .. collapse:: operations - - .. describe:: x == y - - Checks if two messages are equal. - .. describe:: x != y - - Checks if two messages are not equal. - - .. attribute:: default - - The default message type. This is the same as regular messages. - .. attribute:: recipient_add - - The system message when a user is added to a group private - message or a thread. - .. attribute:: recipient_remove - - The system message when a user is removed from a group private - message or a thread. - .. attribute:: call - - The system message denoting call state, e.g. missed call, started call, - etc. - .. attribute:: channel_name_change - - The system message denoting that a channel's name has been changed. - .. attribute:: channel_icon_change - - The system message denoting that a channel's icon has been changed. - .. attribute:: pins_add - - The system message denoting that a pinned message has been added to a channel. - .. attribute:: new_member - - The system message denoting that a new member has joined a Guild. - - .. attribute:: premium_guild_subscription - - The system message denoting that a member has "nitro boosted" a guild. - .. attribute:: premium_guild_tier_1 - - The system message denoting that a member has "nitro boosted" a guild - and it achieved level 1. - .. attribute:: premium_guild_tier_2 - - The system message denoting that a member has "nitro boosted" a guild - and it achieved level 2. - .. attribute:: premium_guild_tier_3 - - The system message denoting that a member has "nitro boosted" a guild - and it achieved level 3. - .. attribute:: channel_follow_add - - The system message denoting that an announcement channel has been followed. - - .. versionadded:: 1.3 - .. attribute:: guild_stream - - The system message denoting that a member is streaming in the guild. - - .. versionadded:: 1.7 - .. attribute:: guild_discovery_disqualified - - The system message denoting that the guild is no longer eligible for Server - Discovery. - - .. versionadded:: 1.7 - .. attribute:: guild_discovery_requalified - - The system message denoting that the guild has become eligible again for Server - Discovery. - - .. versionadded:: 1.7 - .. attribute:: guild_discovery_grace_period_initial_warning - - The system message denoting that the guild has failed to meet the Server - Discovery requirements for one week. - - .. versionadded:: 1.7 - .. attribute:: guild_discovery_grace_period_final_warning - - The system message denoting that the guild has failed to meet the Server - Discovery requirements for 3 weeks in a row. - - .. versionadded:: 1.7 - .. attribute:: thread_created - - The system message denoting that a thread has been created. This is only - sent if the thread has been created from an older message. The period of time - required for a message to be considered old cannot be relied upon and is up to - Discord. - - .. versionadded:: 2.0 - .. attribute:: reply - - The system message denoting that the author is replying to a message. - - .. versionadded:: 2.0 - .. attribute:: application_command - - The system message denoting that an application (or "slash") command was executed. - - .. versionadded:: 2.0 - .. attribute:: guild_invite_reminder - - The system message sent as a reminder to invite people to the guild. - - .. versionadded:: 2.0 - .. attribute:: thread_starter_message - - The system message denoting the message in the thread that is the one that started the - thread's conversation topic. - - .. versionadded:: 2.0 - .. attribute:: context_menu_command - - The system message denoting that a context menu command was executed. - - .. versionadded:: 2.3 - .. attribute:: auto_moderation_action - - The system message denoting that an auto moderation action was executed. - - .. versionadded:: 2.5 - .. attribute:: role_subscription_purchase - - The system message denoting that a role subscription was purchased. - - .. versionadded:: 2.9 - .. attribute:: interaction_premium_upsell - - The system message for an application premium subscription upsell. - - .. versionadded:: 2.8 - .. attribute:: stage_start - - The system message denoting the stage has been started. - - .. versionadded:: 2.9 - .. attribute:: stage_end - - The system message denoting the stage has ended. - - .. versionadded:: 2.9 - .. attribute:: stage_speaker - - The system message denoting a user has become a speaker. - - .. versionadded:: 2.9 - .. attribute:: stage_topic - - The system message denoting the stage topic has been changed. - - .. versionadded:: 2.9 - .. attribute:: guild_application_premium_subscription - - The system message denoting that a guild member has subscribed to an application. - - .. versionadded:: 2.8 - .. attribute:: guild_incident_alert_mode_enabled - - The system message denoting that an admin enabled security actions. - - .. versionadded:: 2.10 - .. attribute:: guild_incident_alert_mode_disabled - - The system message denoting that an admin disabled security actions. - - .. versionadded:: 2.10 - .. attribute:: guild_incident_report_raid - - The system message denoting that an admin reported a raid. - - .. versionadded:: 2.10 - .. attribute:: guild_incident_report_false_alarm - - The system message denoting that a raid report was a false alarm. - - .. versionadded:: 2.10 - - .. attribute:: poll_result - - The system message denoting that a poll expired, announcing the most voted answer. - - .. versionadded:: 2.10 +.. autoclass:: MessageType() + :members: PollLayoutType ~~~~~~~~~~~~~~ -.. class:: PollLayoutType - - Specifies the layout of a :class:`Poll`. - - .. versionadded:: 2.10 - - .. attribute:: default - - The default poll layout type. +.. autoclass:: PollLayoutType() + :members: MessageReferenceType ~~~~~~~~~~~~~~~~~~~~ -.. class:: MessageReferenceType - - Specifies the type of :class:`MessageReference`. This can be used to determine - if a message is e.g. a reply or a forwarded message. - - .. versionadded:: 2.10 - - .. attribute:: default - - A standard message reference used in message replies. - - .. attribute:: forward - - Reference used to point to a message at a point in time (forward). +.. autoclass:: MessageReferenceType() + :members: Events ------ diff --git a/docs/api/skus.rst b/docs/api/skus.rst index 05c457f947..1041d1b38b 100644 --- a/docs/api/skus.rst +++ b/docs/api/skus.rst @@ -39,24 +39,5 @@ Enumerations SKUType ~~~~~~~ -.. class:: SKUType - - Represents the type of an SKU. - - .. versionadded:: 2.10 - - .. attribute:: durable - - Represents a durable one-time purchase. - - .. attribute:: consumable - - Represents a consumable one-time purchase. - - .. attribute:: subscription - - Represents a recurring subscription. - - .. attribute:: subscription_group - - Represents a system-generated group for each :attr:`subscription` SKU. +.. autoclass:: SKUType() + :members: diff --git a/docs/api/stage_instances.rst b/docs/api/stage_instances.rst index f3c297f32e..2178922b39 100644 --- a/docs/api/stage_instances.rst +++ b/docs/api/stage_instances.rst @@ -25,27 +25,8 @@ Enumerations StagePrivacyLevel ~~~~~~~~~~~~~~~~~ -.. class:: StagePrivacyLevel - - Represents a stage instance's privacy level. - - .. versionadded:: 2.0 - - .. attribute:: public - - The stage instance can be joined by external users. - - .. deprecated:: 2.5 - - Public stages are no longer supported by discord. - - .. attribute:: closed - - The stage instance can only be joined by members of the guild. - - .. attribute:: guild_only - - Alias for :attr:`.closed` +.. autoclass:: StagePrivacyLevel() + :members: Events ------ diff --git a/docs/api/stickers.rst b/docs/api/stickers.rst index 8719958753..7d7a2b5b2a 100644 --- a/docs/api/stickers.rst +++ b/docs/api/stickers.rst @@ -60,46 +60,14 @@ Enumerations StickerType ~~~~~~~~~~~ -.. class:: StickerType - - Represents the type of sticker. - - .. versionadded:: 2.0 - - .. attribute:: standard - - Represents a standard sticker that all users can use. - - .. attribute:: guild - - Represents a custom sticker created in a guild. +.. autoclass:: StickerType() + :members: StickerFormatType ~~~~~~~~~~~~~~~~~ -.. class:: StickerFormatType - - Represents the type of sticker images. - - .. versionadded:: 1.6 - - .. attribute:: png - - Represents a sticker with a png image. - - .. attribute:: apng - - Represents a sticker with an apng image. - - .. attribute:: lottie - - Represents a sticker with a lottie image. - - .. attribute:: gif - - Represents a sticker with a gif image. - - .. versionadded:: 2.8 +.. autoclass:: StickerFormatType() + :members: Events ------ diff --git a/docs/api/subscriptions.rst b/docs/api/subscriptions.rst index 338cb9e786..e14decb750 100644 --- a/docs/api/subscriptions.rst +++ b/docs/api/subscriptions.rst @@ -25,20 +25,5 @@ Enumerations SubscriptionStatus ~~~~~~~~~~~~~~~~~~ -.. class:: SubscriptionStatus - - Represents the status of a subscription. - - .. versionadded:: 2.10 - - .. attribute:: active - - Represents an active Subscription which is scheduled to renew. - - .. attribute:: ending - - Represents an active Subscription which will not renew. - - .. attribute:: inactive - - Represents an inactive Subscription which is not being charged. +.. autoclass:: SubscriptionStatus() + :members: diff --git a/docs/api/users.rst b/docs/api/users.rst index 36bfdc1faa..3b4d2309d9 100644 --- a/docs/api/users.rst +++ b/docs/api/users.rst @@ -51,113 +51,14 @@ Enumerations UserFlags ~~~~~~~~~ -.. class:: UserFlags - - Represents Discord User flags. - - .. attribute:: staff - - The user is a Discord Employee. - .. attribute:: partner - - The user is a Discord Partner. - .. attribute:: hypesquad - - The user is a HypeSquad Events member. - .. attribute:: bug_hunter - - The user is a Bug Hunter. - .. attribute:: mfa_sms - - The user has SMS recovery for Multi Factor Authentication enabled. - .. attribute:: premium_promo_dismissed - - The user has dismissed the Discord Nitro promotion. - .. attribute:: hypesquad_bravery - - The user is a HypeSquad Bravery member. - .. attribute:: hypesquad_brilliance - - The user is a HypeSquad Brilliance member. - .. attribute:: hypesquad_balance - - The user is a HypeSquad Balance member. - .. attribute:: early_supporter - - The user is an Early Supporter. - .. attribute:: team_user - - The user is a Team User. - .. attribute:: system - - The user is a system user (i.e. represents Discord officially). - .. attribute:: has_unread_urgent_messages - - The user has an unread system message. - .. attribute:: bug_hunter_level_2 - - The user is a Bug Hunter Level 2. - .. attribute:: verified_bot - - The user is a Verified Bot. - .. attribute:: verified_bot_developer - - The user is an Early Verified Bot Developer. - .. attribute:: discord_certified_moderator - - The user is a Discord Certified Moderator. - .. attribute:: http_interactions_bot - - The user is a bot that only uses HTTP interactions. - - .. versionadded:: 2.3 - .. attribute:: spammer - - The user is marked as a spammer. - - .. versionadded:: 2.3 - .. attribute:: active_developer - - The user is an Active Developer. - - .. versionadded:: 2.8 +.. autoclass:: UserFlags() + :members: DefaultAvatar ~~~~~~~~~~~~~ -.. class:: DefaultAvatar - - Represents the default avatar of a Discord :class:`User`. - - .. attribute:: blurple - - Represents the default avatar with the color blurple. - See also :attr:`Colour.blurple` - .. attribute:: grey - - Represents the default avatar with the color grey. - See also :attr:`Colour.greyple` - .. attribute:: gray - - An alias for :attr:`grey`. - .. attribute:: green - - Represents the default avatar with the color green. - See also :attr:`Colour.green` - .. attribute:: orange - - Represents the default avatar with the color orange. - See also :attr:`Colour.orange` - .. attribute:: red - - Represents the default avatar with the color red. - See also :attr:`Colour.red` - .. attribute:: fuchsia - - Represents the default avatar with the color fuchsia. - See also :attr:`Colour.fuchsia` - - .. versionadded:: 2.9 +.. autoclass:: DefaultAvatar() + :members: Events ------ diff --git a/docs/api/voice.rst b/docs/api/voice.rst index e56d1a2eb9..0ec05c694c 100644 --- a/docs/api/voice.rst +++ b/docs/api/voice.rst @@ -124,78 +124,14 @@ Enumerations VoiceChannelEffectAnimationType ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: VoiceChannelEffectAnimationType - - The type of an emoji reaction effect animation in a voice channel. - - .. versionadded:: 2.10 - - .. attribute:: premium - - A fun animation, sent by a Nitro subscriber. - - .. attribute:: basic - - A standard animation. +.. autoclass:: VoiceChannelEffectAnimationType() + :members: PartyType ~~~~~~~~~ -.. class:: PartyType - - Represents the type of a voice channel activity/application. - - .. attribute:: poker - - The "Poker Night" activity. - .. attribute:: betrayal - - The "Betrayal.io" activity. - .. attribute:: fishing - - The "Fishington.io" activity. - .. attribute:: chess - - The "Chess In The Park" activity. - .. attribute:: letter_tile - - The "Letter Tile" activity. - .. attribute:: word_snack - - The "Word Snacks" activity. - .. attribute:: doodle_crew - - The "Doodle Crew" activity. - .. attribute:: checkers - - The "Checkers In The Park" activity. - - .. versionadded:: 2.3 - .. attribute:: spellcast - - The "SpellCast" activity. - - .. versionadded:: 2.3 - .. attribute:: watch_together - - The "Watch Together" activity, a Youtube application. - - .. versionadded:: 2.3 - .. attribute:: sketch_heads - - The "Sketch Heads" activity. - - .. versionadded:: 2.4 - .. attribute:: ocho - - The "Ocho" activity. - - .. versionadded:: 2.4 - .. attribute:: gartic_phone - - The "Gartic Phone" activity. - - .. versionadded:: 2.9 +.. autoclass:: PartyType() + :members: Events ------ diff --git a/docs/api/webhooks.rst b/docs/api/webhooks.rst index eeecf7fce9..09b63a5205 100644 --- a/docs/api/webhooks.rst +++ b/docs/api/webhooks.rst @@ -67,25 +67,8 @@ Enumerations WebhookType ~~~~~~~~~~~ -.. class:: WebhookType - - Represents the type of webhook that can be received. - - .. versionadded:: 1.3 - - .. attribute:: incoming - - Represents a webhook that can post messages to channels with a token. - - .. attribute:: channel_follower - - Represents a webhook that is internally managed by Discord, used for following channels. - - .. attribute:: application - - Represents a webhook that is used for interactions or applications. - - .. versionadded:: 2.0 +.. autoclass:: WebhookType() + :members: Events ------ diff --git a/docs/api/widgets.rst b/docs/api/widgets.rst index 386a91f37e..8349e4dc1a 100644 --- a/docs/api/widgets.rst +++ b/docs/api/widgets.rst @@ -52,30 +52,5 @@ Enumerations WidgetStyle ~~~~~~~~~~~ -.. class:: WidgetStyle - - Represents the supported widget image styles. - - .. versionadded:: 2.5 - - .. attribute:: shield - - A shield style image with a Discord icon and the online member count. - - .. attribute:: banner1 - - A large image with guild icon, name and online member count and a footer. - - .. attribute:: banner2 - - A small image with guild icon, name and online member count. - - .. attribute:: banner3 - - A large image with guild icon, name and online member count and a footer, - with a "Chat Now" label on the right. - - .. attribute:: banner4 - - A large image with a large Discord logo, guild icon, name and online member count, - with a "Join My Server" label at the bottom. +.. autoclass:: WidgetStyle() + :members: diff --git a/docs/conf.py b/docs/conf.py index 575602bda9..99e6fa6c9e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,6 +53,7 @@ "attributetable", "resourcelinks", "collapse", + "enumattrs", "nitpick_file_ignorer", ] diff --git a/docs/ext/commands/api/checks.rst b/docs/ext/commands/api/checks.rst index 3e8579cbae..305d8e6474 100644 --- a/docs/ext/commands/api/checks.rst +++ b/docs/ext/commands/api/checks.rst @@ -26,33 +26,8 @@ Enumerations BucketType ~~~~~~~~~~ -.. class:: BucketType - - Specifies a type of bucket for, e.g. a cooldown. - - .. attribute:: default - - The default bucket operates on a global basis. - .. attribute:: user - - The user bucket operates on a per-user basis. - .. attribute:: guild - - The guild bucket operates on a per-guild basis. - .. attribute:: channel - - The channel bucket operates on a per-channel basis. - .. attribute:: member - - The member bucket operates on a per-member basis. - .. attribute:: category - - The category bucket operates on a per-category basis. - .. attribute:: role - - The role bucket operates on a per-role basis. - - .. versionadded:: 1.3 +.. autoclass:: BucketType() + :members: Functions --------- diff --git a/docs/extensions/enumattrs.py b/docs/extensions/enumattrs.py new file mode 100644 index 0000000000..f022841baf --- /dev/null +++ b/docs/extensions/enumattrs.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from sphinx.ext.autodoc import AttributeDocumenter + +import disnake + +if TYPE_CHECKING: + from sphinx.application import Sphinx + + from ._types import SphinxExtensionMeta + + +class EnumMemberDocumenter(AttributeDocumenter): + """ + Custom enum member documenter which hides enum values. + Gets used automatically for all `_EnumValueBase` instances. + """ + + objtype = "enumattribute" + directivetype = AttributeDocumenter.objtype + priority = 10 + AttributeDocumenter.priority + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool: + return super().can_document_member(member, membername, isattr, parent) and isinstance( + member, disnake.enums._EnumValueBase + ) + + def should_suppress_value_header(self) -> bool: + # always hide enum member values + return True + + +def setup(app: Sphinx) -> SphinxExtensionMeta: + app.setup_extension("sphinx.ext.autodoc") + + app.add_autodocumenter(EnumMemberDocumenter) + + # show `Enum.name` instead of `` in signatures + disnake.enums._EnumValueBase.__repr__ = disnake.enums._EnumValueBase.__str__ + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } From 18535bb4c754f7f06de2477b5562d41a3f666db9 Mon Sep 17 00:00:00 2001 From: vi <8530778+shiftinv@users.noreply.github.com> Date: Sun, 29 Dec 2024 18:05:40 +0100 Subject: [PATCH 7/7] fix(http): escape `/` in url parameters (#1264) --- changelog/1264.bugfix.rst | 1 + disnake/http.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog/1264.bugfix.rst diff --git a/changelog/1264.bugfix.rst b/changelog/1264.bugfix.rst new file mode 100644 index 0000000000..417429388e --- /dev/null +++ b/changelog/1264.bugfix.rst @@ -0,0 +1 @@ +Escape forward slashes in HTTP route parameters. diff --git a/disnake/http.py b/disnake/http.py index 4d3132c2ea..b258985016 100644 --- a/disnake/http.py +++ b/disnake/http.py @@ -177,7 +177,11 @@ def __init__(self, method: str, path: str, **parameters: Any) -> None: url = self.BASE + self.path if parameters: url = url.format_map( - {k: _uriquote(v) if isinstance(v, str) else v for k, v in parameters.items()} + { + # `/` should not be considered a safe character by default + k: _uriquote(v, safe="") if isinstance(v, str) else v + for k, v in parameters.items() + } ) self.url: str = url