Files
2026-02-03 20:32:43 +02:00

365 lines
12 KiB
Python

# SPDX-License-Identifier: MIT
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Tuple, TypeVar, Union, overload
from ..components import Button as ButtonComponent
from ..enums import ButtonStyle, ComponentType
from ..partial_emoji import PartialEmoji, _EmojiTag
from ..utils import MISSING, iscoroutinefunction
from .item import DecoratedItem, Item
__all__ = (
"Button",
"button",
)
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Self
from ..emoji import Emoji
from .item import ItemCallbackType
from .view import View
else:
ParamSpec = TypeVar
B = TypeVar("B", bound="Button")
B_co = TypeVar("B_co", bound="Button", covariant=True)
V_co = TypeVar("V_co", bound="Optional[View]", covariant=True)
P = ParamSpec("P")
class Button(Item[V_co]):
"""Represents a UI button.
.. versionadded:: 2.0
Parameters
----------
style: :class:`disnake.ButtonStyle`
The style of the button.
custom_id: Optional[:class:`str`]
The ID of the button that gets received during an interaction.
If this button is for a URL or an SKU, it does not have a custom ID.
url: Optional[:class:`str`]
The URL this button sends you to.
disabled: :class:`bool`
Whether the button is disabled.
label: Optional[:class:`str`]
The label of the button, if any.
emoji: Optional[Union[:class:`.PartialEmoji`, :class:`.Emoji`, :class:`str`]]
The emoji of the button, if available.
sku_id: Optional[:class:`int`]
The ID of a purchasable SKU, for premium buttons.
Premium buttons additionally cannot have a ``label``, ``url``, or ``emoji``.
.. versionadded:: 2.11
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
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).
"""
__repr_attributes__: ClassVar[Tuple[str, ...]] = (
"style",
"url",
"disabled",
"label",
"emoji",
"sku_id",
"row",
)
# We have to set this to MISSING in order to overwrite the abstract property from UIComponent
_underlying: ButtonComponent = MISSING
@overload
def __init__(
self: Button[None],
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
@overload
def __init__(
self: Button[V_co],
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None: ...
def __init__(
self,
*,
style: ButtonStyle = ButtonStyle.secondary,
label: Optional[str] = None,
disabled: bool = False,
custom_id: Optional[str] = None,
url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
sku_id: Optional[int] = None,
id: int = 0,
row: Optional[int] = None,
) -> None:
super().__init__()
self._provided_custom_id = custom_id is not None
mutually_exclusive = 3 - (custom_id, url, sku_id).count(None)
if mutually_exclusive == 0:
custom_id = os.urandom(16).hex()
elif mutually_exclusive != 1:
raise TypeError("cannot mix url, sku_id and custom_id with Button")
if url is not None:
style = ButtonStyle.link
if sku_id is not None:
style = ButtonStyle.premium
if emoji is not None:
if isinstance(emoji, str):
emoji = PartialEmoji.from_str(emoji)
elif isinstance(emoji, _EmojiTag):
emoji = emoji._to_partial()
else:
raise TypeError(
f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}"
)
self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button,
id=id,
custom_id=custom_id,
url=url,
disabled=disabled,
label=label,
style=style,
emoji=emoji,
sku_id=sku_id,
)
self.row = row
@property
def width(self) -> int:
return 1
@property
def style(self) -> ButtonStyle:
""":class:`disnake.ButtonStyle`: The style of the button."""
return self._underlying.style
@style.setter
def style(self, value: ButtonStyle) -> None:
self._underlying.style = value
@property
def custom_id(self) -> Optional[str]:
"""Optional[:class:`str`]: The ID of the button that gets received during an interaction.
If this button is for a URL or an SKU, it does not have a custom ID.
"""
return self._underlying.custom_id
@custom_id.setter
def custom_id(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError("custom_id must be None or str")
self._underlying.custom_id = value
@property
def url(self) -> Optional[str]:
"""Optional[:class:`str`]: The URL this button sends you to."""
return self._underlying.url
@url.setter
def url(self, value: Optional[str]) -> None:
if value is not None and not isinstance(value, str):
raise TypeError("url must be None or str")
self._underlying.url = value
@property
def disabled(self) -> bool:
""":class:`bool`: Whether the button is disabled."""
return self._underlying.disabled
@disabled.setter
def disabled(self, value: bool) -> None:
self._underlying.disabled = bool(value)
@property
def label(self) -> Optional[str]:
"""Optional[:class:`str`]: The label of the button, if available."""
return self._underlying.label
@label.setter
def label(self, value: Optional[str]) -> None:
self._underlying.label = str(value) if value is not None else value
@property
def emoji(self) -> Optional[PartialEmoji]:
"""Optional[:class:`.PartialEmoji`]: The emoji of the button, if available."""
return self._underlying.emoji
@emoji.setter
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]) -> None:
if value is not None:
if isinstance(value, str):
self._underlying.emoji = PartialEmoji.from_str(value)
elif isinstance(value, _EmojiTag):
self._underlying.emoji = value._to_partial()
else:
raise TypeError(
f"expected str, Emoji, or PartialEmoji, received {value.__class__} instead"
)
else:
self._underlying.emoji = None
@property
def sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of a purchasable SKU, for premium buttons.
.. versionadded:: 2.11
"""
return self._underlying.sku_id
@sku_id.setter
def sku_id(self, value: Optional[int]) -> None:
if value is not None and not isinstance(value, int):
raise TypeError("sku_id must be None or int")
self._underlying.sku_id = value
@classmethod
def from_component(cls, button: ButtonComponent) -> Self:
return cls(
style=button.style,
label=button.label,
disabled=button.disabled,
custom_id=button.custom_id,
url=button.url,
emoji=button.emoji,
sku_id=button.sku_id,
id=button.id,
row=None,
)
def is_dispatchable(self) -> bool:
return self.custom_id is not None
def is_persistent(self) -> bool:
if self.style is ButtonStyle.link:
return self.url is not None
elif self.style is ButtonStyle.premium:
return self.sku_id is not None
return super().is_persistent()
def refresh_component(self, button: ButtonComponent) -> None:
self._underlying = button
@overload
def button(
*,
label: Optional[str] = None,
custom_id: Optional[str] = None,
disabled: bool = False,
style: ButtonStyle = ButtonStyle.secondary,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
id: int = 0,
row: Optional[int] = None,
) -> Callable[[ItemCallbackType[V_co, Button[V_co]]], DecoratedItem[Button[V_co]]]: ...
@overload
def button(
cls: Callable[P, B_co], *_: P.args, **kwargs: P.kwargs
) -> Callable[[ItemCallbackType[V_co, B_co]], DecoratedItem[B_co]]: ...
def button(
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
the :class:`disnake.ui.View`, the :class:`disnake.ui.Button` that was
interacted with, and the :class:`disnake.MessageInteraction`.
.. note::
Link/Premium buttons cannot be created with this function,
since these buttons do not have a callback associated with them.
Consider creating a :class:`Button` manually instead, and adding it
using :meth:`View.add_item`.
Parameters
----------
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`]
The ID of the button that gets received during an interaction.
It is recommended not to set this parameter to prevent conflicts.
style: :class:`.ButtonStyle`
The style of the button. Defaults to :attr:`.ButtonStyle.grey`.
disabled: :class:`bool`
Whether the button is disabled. Defaults to ``False``.
emoji: Optional[Union[:class:`str`, :class:`.Emoji`, :class:`.PartialEmoji`]]
The emoji of the button. This can be in string form or a :class:`.PartialEmoji`
or a full :class:`.Emoji`.
id: :class:`int`
The numeric identifier for the component. Must be unique within the message.
If set to ``0`` (the default) when sending a component, the API will assign
sequential identifiers to the components in the message.
.. versionadded:: 2.11
row: Optional[:class:`int`]
The relative row this button belongs to. A Discord component can only have 5
rows. By default, items are arranged automatically into those 5 rows. If you'd
like to control the relative positioning of the row then passing an index is advised.
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 not callable(cls):
raise TypeError("cls argument must be callable")
def decorator(func: ItemCallbackType[V_co, B_co]) -> DecoratedItem[B_co]:
if not iscoroutinefunction(func):
raise TypeError("button function must be a coroutine function")
func.__discord_ui_model_type__ = cls
func.__discord_ui_model_kwargs__ = kwargs
return func # type: ignore
return decorator