"""Interaction implementations extending disnake interactions."""
from __future__ import annotations
import typing
import disnake
from disnake.ext.components.api import component as component_api
__all__: typing.Sequence[str] = ("MessageInteraction", "wrap_interaction")
ComponentT = typing.TypeVar(
"ComponentT",
bound=typing.Union[component_api.RichComponent, disnake.ui.WrappedComponent],
)
Components = typing.Union[
# ActionRow[ComponentT],
ComponentT,
typing.Sequence[
typing.Union[
# "ActionRow[ComponentT]",
ComponentT,
typing.Sequence[ComponentT],
]
],
]
# TODO: Custom action rows?
MessageComponents = typing.Union[
component_api.RichButton,
disnake.ui.Button[typing.Any],
component_api.RichSelect,
disnake.ui.StringSelect[typing.Any],
disnake.ui.ChannelSelect[typing.Any],
disnake.ui.RoleSelect[typing.Any],
disnake.ui.UserSelect[typing.Any],
disnake.ui.MentionableSelect[typing.Any],
]
MISSING = disnake.utils.MISSING
async def _prepare(component: MessageComponents) -> disnake.ui.MessageUIComponent:
if isinstance(
component, (component_api.RichButton, component_api.RichSelect)
): # TODO: add select
return await component.as_ui_component() # pyright: ignore
return component
@typing.overload
async def _to_ui_components(
components: Components[MessageComponents] = MISSING,
*,
allow_none: typing.Literal[False] = False,
) -> disnake.ui.Components[disnake.ui.MessageUIComponent]:
...
@typing.overload
async def _to_ui_components(
components: typing.Optional[Components[MessageComponents]] = MISSING,
*,
allow_none: typing.Literal[True],
) -> typing.Optional[disnake.ui.Components[disnake.ui.MessageUIComponent]]:
...
async def _to_ui_components(
components: typing.Optional[Components[MessageComponents]] = MISSING,
*,
allow_none: bool = False,
) -> typing.Optional[disnake.ui.Components[disnake.ui.MessageUIComponent]]:
if components is None:
if not allow_none:
msg = "Components cannot be None in this method."
raise TypeError(msg)
return components
if components is MISSING:
return MISSING
if not isinstance(components, typing.Sequence):
return await _prepare(components)
finalised: list[
typing.Union[disnake.ui.MessageUIComponent, list[disnake.ui.MessageUIComponent]]
] = []
for component in components:
if not isinstance(component, typing.Sequence):
finalised.append(await _prepare(component))
continue
finalised.append([await _prepare(nested) for nested in component])
return finalised
class WrappedInteraction(disnake.Interaction):
"""Interaction implementation that wraps :class:`disnake.Interaction`.
This wrapped interaction class natively supports disnake-ext-components'
specialised components classes and -- unlike vanilla disnake interactions --
can send them without manually having to convert them to native disnake
components first.
Attribute access is simply proxied to the wrapped interaction object by
means of a custom :meth:`__getattr__` implementation.
"""
__slots__ = ("_wrapped",)
def __init__(self, wrapped: disnake.Interaction):
self._wrapped = wrapped
def __getattribute__(self, name: str) -> typing.Any: # noqa: ANN401
"""Get an attribute of this class or the wrapped interaction."""
try:
return super().__getattribute__(name)
except AttributeError:
return getattr(self._wrapped, name)
@disnake.utils.cached_slot_property("_cs_response")
def response(self) -> WrappedInteractionResponse: # noqa: D102
# <<docstring inherited from disnake.Interaction>>
return WrappedInteractionResponse(super().response)
@disnake.utils.cached_slot_property("_cs_response")
def followup(self) -> ...: # noqa: D102
# <<docstring inherited from disnake.Interaction>>
return self._wrapped.followup # TODO: custom followup object
async def edit_original_response( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102, E501
self,
content: typing.Optional[str] = MISSING,
*,
embed: typing.Optional[disnake.Embed] = MISSING,
embeds: list[disnake.Embed] = MISSING,
file: disnake.File = MISSING,
files: list[disnake.File] = MISSING,
attachments: typing.Optional[list[disnake.Attachment]] = MISSING,
view: typing.Optional[disnake.ui.View] = MISSING,
components: typing.Optional[Components[MessageComponents]] = MISSING,
suppress_embeds: bool = MISSING,
allowed_mentions: typing.Optional[disnake.AllowedMentions] = None,
) -> disnake.InteractionMessage:
# <<docstring inherited from disnake.Interaction>>
return await self._wrapped.edit_original_response(
content=content,
embed=embed,
embeds=embeds,
file=file,
files=files,
attachments=attachments,
view=view,
components=await _to_ui_components(components, allow_none=True),
suppress_embeds=suppress_embeds,
allowed_mentions=allowed_mentions,
)
async def send( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102
self,
content: typing.Optional[str] = None,
*,
embed: disnake.Embed = MISSING,
embeds: list[disnake.Embed] = MISSING,
file: disnake.File = MISSING,
files: list[disnake.File] = MISSING,
allowed_mentions: disnake.AllowedMentions = MISSING,
view: disnake.ui.View = MISSING,
components: Components[MessageComponents] = MISSING,
tts: bool = False,
ephemeral: bool = False,
suppress_embeds: bool = False,
delete_after: float = MISSING,
) -> None:
# <<docstring inherited from disnake.Interaction>>
return await self._wrapped.send(
content=content,
embed=embed,
embeds=embeds,
file=file,
files=files,
allowed_mentions=allowed_mentions,
view=view,
components=await _to_ui_components(components),
tts=tts,
ephemeral=ephemeral,
suppress_embeds=suppress_embeds,
delete_after=delete_after,
)
class WrappedInteractionResponse(disnake.InteractionResponse):
"""Interaction response implementation that wraps :class:`disnake.InteractionResponse`.
This wrapped interaction response class natively supports
disnake-ext-components' specialised components classes and -- unlike
vanilla disnake interactions -- can send them without manually having to
convert them to native disnake components first.
Attribute access is simply proxied to the wrapped interaction response
object by means of a custom :meth:`__getattr__` implementation.
""" # noqa: E501
__slots__ = ("_wrapped",)
def __init__(self, wrapped: disnake.InteractionResponse):
self._wrapped = wrapped
async def send_message( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102, E501
self,
content: typing.Optional[str] = None,
*,
embed: disnake.Embed = MISSING,
embeds: list[disnake.Embed] = MISSING,
file: disnake.File = MISSING,
files: list[disnake.File] = MISSING,
allowed_mentions: disnake.AllowedMentions = MISSING,
view: disnake.ui.View = MISSING,
components: Components[MessageComponents] = MISSING,
tts: bool = False,
ephemeral: bool = False,
suppress_embeds: bool = False,
delete_after: float = MISSING,
) -> None:
# <<docstring inherited from disnake.Interaction>>
return await self._wrapped.send_message(
content=content,
embed=embed,
embeds=embeds,
file=file,
files=files,
allowed_mentions=allowed_mentions,
view=view,
components=await _to_ui_components(components),
tts=tts,
ephemeral=ephemeral,
suppress_embeds=suppress_embeds,
delete_after=delete_after,
)
async def edit_message( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102, E501
self,
content: typing.Optional[str] = None,
*,
embed: disnake.Embed = MISSING,
embeds: list[disnake.Embed] = MISSING,
file: disnake.File = MISSING,
files: list[disnake.File] = MISSING,
attachments: typing.Optional[list[disnake.Attachment]] = MISSING,
allowed_mentions: disnake.AllowedMentions = MISSING,
view: disnake.ui.View = MISSING,
components: typing.Optional[Components[MessageComponents]] = MISSING,
) -> None:
# <<docstring inherited from disnake.Interaction>>
return await self._wrapped.edit_message(
content=content,
embed=embed,
embeds=embeds,
file=file,
files=files,
attachments=attachments,
allowed_mentions=allowed_mentions,
view=view,
components=await _to_ui_components(components, allow_none=True),
)
[docs]class MessageInteraction( # pyright: ignore
WrappedInteraction, disnake.MessageInteraction
):
"""Message interaction implementation that wraps :class:`disnake.MessageInteraction`.
This wrapped message interaction class natively supports
disnake-ext-components' specialised components classes and -- unlike
vanilla disnake interactions -- can send them without manually having to
convert them to native disnake components first.
Attribute access is simply proxied to the wrapped message interaction
object by means of a custom :meth:`__getattr__` implementation.
""" # noqa: E501
# __slots__ = () # No slots on disnake.MessageInteraction...
def __init__(self, wrapped: disnake.MessageInteraction):
self._wrapped = wrapped
# message = proxy.ProxiedProperty("_wrapped")
def wrap_interaction(interaction: disnake.Interaction) -> WrappedInteraction:
"""Wrap a disnake interaction type for disnake-ext-components compatibility.
Interactions wrapped in this way can send disnake-ext-components'
specialised components directly, without having to first convert them to
native disnake components.
Parameters
----------
interaction:
The interaction to wrap.
Returns
-------
WrappedInteraction:
The wrapped interaction. Note that this can be any subclass of
:class:`WrappedInteraction`:
- Wrapping a (subclass of) :class:`disnake.MessageInteraction` returns
a :class:`MessageInteraction`,
- Wrapping a (subclass of) :class:`disnake.ModalInteraction` returns a
:class:`ModalInteraction`,
- Wrapping any other interaction class returns a
:class:`WrappedInteraction`.
"""
if isinstance(interaction, disnake.MessageInteraction):
return MessageInteraction(interaction)
# TODO: ModalInteraction
return WrappedInteraction(interaction)