from __future__ import annotations
import abc
import os
import typing as t
from abc import abstractmethod
from functools import partial
import hikari
from miru.exceptions import ItemAlreadyAttachedError
if t.TYPE_CHECKING:
from ..context import Context, ViewContext
from ..modal import Modal
from ..view import View
from .item_handler import ItemHandler
__all__ = ("Item", "DecoratedItem", "ViewItem", "ModalItem")
BuilderT = t.TypeVar("BuilderT", bound=hikari.api.ComponentBuilder)
ViewT = t.TypeVar("ViewT", bound="View")
ViewItemT = t.TypeVar("ViewItemT", bound="ViewItem")
ViewContextT = t.TypeVar("ViewContextT", bound="ViewContext")
[docs]
class Item(abc.ABC, t.Generic[BuilderT]):
"""An abstract base class for all components. Cannot be directly instantiated."""
def __init__(
self,
*,
custom_id: t.Optional[str] = None,
row: t.Optional[int] = None,
position: t.Optional[int] = None,
width: int = 1,
) -> None:
self._rendered_row: t.Optional[int] = None
"""The row the item was placed at when rendered. None if this item was not sent to a message yet."""
self.row = row
"""The row the item should occupy. Leave as None for automatic placement."""
self._width: int = width
"""The relative width of the item. 5 takes up a whole row."""
self.position = position
"""The position of the item within the row it occupies. Leave as None for automatic placement."""
self.custom_id = custom_id # type: ignore[assignment]
"""The Discord custom_id of the item."""
self._is_persistent: bool = bool(custom_id)
"""If True, the custom_id was provided by the user, and not randomly generated."""
self._handler: t.Optional[ItemHandler[BuilderT, t.Any, t.Any]] = None
"""The handler the item was added to, if any."""
@property
def position(self) -> t.Optional[int]:
"""The position of the item within the row it occupies."""
return self._position
@position.setter
def position(self, value: t.Optional[int]) -> None:
if value is None or 4 >= value >= 0:
self._position = value
else:
raise ValueError(f"Position of item {type(self).__name__} must be between 0 and 4.")
@property
def row(self) -> t.Optional[int]:
"""The row the item should occupy. Leave as None for automatic placement."""
return self._row
@row.setter
def row(self, value: t.Optional[int]) -> None:
if self._rendered_row is not None:
raise ItemAlreadyAttachedError("Item is already attached to an item handler, row cannot be changed.")
if value is None or 5 > value >= 0:
self._row = value
else:
raise ValueError("Row must be between 0 and 4.")
@property
def width(self) -> int:
"""The item's width taken up in a Discord UI action row."""
return self._width
@property
def custom_id(self) -> str:
"""The item's custom identifier. This will be used to track the item through interactions and
is required for persistent views.
"""
return self._custom_id
@custom_id.setter
def custom_id(self, value: t.Optional[str]) -> None:
if value and not isinstance(value, str):
raise TypeError("Expected type str for property custom_id.")
if value and len(value) > 100:
raise ValueError("custom_id has a max length of 100.")
self._is_persistent = bool(value)
self._custom_id = value or os.urandom(16).hex()
@abc.abstractmethod
def _build(self, action_row: t.Any) -> None:
...
@property
@abstractmethod
def type(self) -> hikari.ComponentType:
"""The component's underlying component type."""
...
async def _refresh_state(self, context: Context[t.Any]) -> None:
"""Called on an item to refresh it's internal state."""
pass
[docs]
class ViewItem(Item[hikari.impl.MessageActionRowBuilder], abc.ABC):
"""An abstract base class for view components. Cannot be directly instantiated."""
def __init__(
self,
*,
custom_id: t.Optional[str] = None,
row: t.Optional[int] = None,
position: t.Optional[int] = None,
width: int = 1,
disabled: bool = False,
) -> None:
super().__init__(custom_id=custom_id, row=row, position=position, width=width)
self._handler: t.Optional[View] = None
self._disabled: bool = disabled
@property
def view(self) -> View:
"""The view this item is attached to."""
if not self._handler:
raise AttributeError(f"{type(self).__name__} hasn't been attached to a view yet.")
return self._handler
@property
def disabled(self) -> bool:
"""Indicates whether the item is disabled or not."""
return self._disabled
@disabled.setter
def disabled(self, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("Expected type 'bool' for property 'disabled'.")
self._disabled = value
@abstractmethod
def _build(self, action_row: hikari.api.MessageActionRowBuilder) -> None:
"""Called internally to build and append the item to an action row."""
...
@classmethod
@abstractmethod
def _from_component(cls, component: hikari.PartialComponent, row: t.Optional[int] = None) -> ViewItem:
"""Converts the passed hikari component into a miru ViewItem."""
...
[docs]
async def callback(self, context: ViewContextT) -> None:
"""The component's callback, gets called when the component receives an interaction.
Parameters
----------
context : ViewContextT
The context, proxying the incoming interaction.
"""
pass
[docs]
class ModalItem(Item[hikari.impl.ModalActionRowBuilder], abc.ABC):
"""An abstract base class for modal components. Cannot be directly instantiated."""
def __init__(
self,
*,
custom_id: t.Optional[str] = None,
row: t.Optional[int] = None,
position: t.Optional[int] = None,
width: int = 1,
required: bool = False,
) -> None:
super().__init__(custom_id=custom_id, row=row, position=position, width=width)
self._handler: t.Optional[Modal] = None
self._required: bool = required
@property
def modal(self) -> t.Optional[Modal]:
"""The modal this item is attached to."""
if not self._handler:
raise AttributeError(f"{type(self).__name__} hasn't been attached to a modal yet.")
return self._handler
@property
def required(self) -> bool:
"""Indicates whether the item is required or not."""
return self._required
@required.setter
def required(self, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("Expected type 'bool' for property 'required'.")
self._required = value
@abstractmethod
def _build(self, action_row: hikari.api.ModalActionRowBuilder) -> None:
"""Called internally to build and append the item to an action row."""
...
[docs]
class DecoratedItem(t.Generic[ViewT, ViewItemT, ViewContextT]):
"""A partial item made using a decorator."""
__slots__ = ("item", "callback")
def __init__(
self, item: ViewItemT, callback: t.Callable[[ViewT, ViewItemT, ViewContextT], t.Awaitable[None]]
) -> None:
self.item = item
self.callback = callback
[docs]
def build(self, view: View) -> ViewItemT:
"""Convert a DecoratedItem into a ViewItem.
Parameters
----------
view : ViewT
The view this decorated item is attached to.
Returns
-------
ViewItem[ViewT]
The converted item.
"""
self.item.callback = partial(self.callback, view, self.item) # type: ignore[assignment]
return self.item
@property
def name(self) -> str:
"""The name of the callback this item decorates.
Returns
-------
str
The name of the callback.
"""
return self.callback.__name__
def __call__(self, view: ViewT, item: ViewItemT, context: ViewContextT) -> t.Awaitable[None]:
"""Call the callback this DecoratedItem wraps."""
return self.callback(view, item, context)
# MIT License
#
# Copyright (c) 2022-present hypergonial
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.