From 84759ab29407d84b8f4190441eb84ef8db08e3fd Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 07:03:25 -0400 Subject: [PATCH] Add case commands --- modules/moderation/cases.py | 168 ++++++ poetry.lock | 16 +- pyproject.toml | 1 + stubs/reactionmenu/__init__.pyi | 21 + stubs/reactionmenu/abc.pyi | 816 ++++++++++++++++++++++++++++++ stubs/reactionmenu/buttons.pyi | 549 ++++++++++++++++++++ stubs/reactionmenu/core.pyi | 332 ++++++++++++ stubs/reactionmenu/decorators.pyi | 31 ++ stubs/reactionmenu/errors.pyi | 122 +++++ stubs/reactionmenu/views_menu.pyi | 714 ++++++++++++++++++++++++++ 10 files changed, 2769 insertions(+), 1 deletion(-) create mode 100644 modules/moderation/cases.py create mode 100644 stubs/reactionmenu/__init__.pyi create mode 100644 stubs/reactionmenu/abc.pyi create mode 100644 stubs/reactionmenu/buttons.pyi create mode 100644 stubs/reactionmenu/core.pyi create mode 100644 stubs/reactionmenu/decorators.pyi create mode 100644 stubs/reactionmenu/errors.pyi create mode 100644 stubs/reactionmenu/views_menu.pyi diff --git a/modules/moderation/cases.py b/modules/moderation/cases.py new file mode 100644 index 0000000..7d79946 --- /dev/null +++ b/modules/moderation/cases.py @@ -0,0 +1,168 @@ +import asyncio + +import discord +from discord.ext import commands +from reactionmenu import ViewButton, ViewMenu + +from lib.case_handler import edit_case_modlog +from lib.const import CONST +from lib.exceptions import LumiException +from lib.format import format_case_number +from services.case_service import CaseService +from ui.cases import ( + create_case_embed, + create_case_list_embed, +) +from ui.embeds import Builder + +case_service = CaseService() + + +class Cases(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command(name="case", aliases=["c", "ca"], description="View a specific case by number") + @commands.has_permissions(manage_messages=True) + async def view_case_by_number(self, ctx: commands.Context[commands.Bot], case_number: int) -> None: + guild_id = ctx.guild.id if ctx.guild else 0 + case = case_service.fetch_case_by_guild_and_number(guild_id, case_number) + + if not case: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["error_no_case_found_author"], + description=CONST.STRINGS["error_no_case_found_description"], + ) + await ctx.send(embed=embed) + return + + target = await commands.UserConverter().convert(ctx, str(case["target_id"])) + embed: discord.Embed = create_case_embed( + ctx=ctx, + target=target, + case_number=case["case_number"], + action_type=case["action_type"], + reason=case["reason"], + timestamp=case["created_at"], + duration=case["duration"] or None, + ) + await ctx.send(embed=embed) + + @commands.hybrid_command(name="cases", description="View all cases in the guild") + @commands.has_permissions(manage_messages=True) + async def view_all_cases_in_guild(self, ctx: commands.Context[commands.Bot]) -> None: + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + cases = case_service.fetch_cases_by_guild(guild_id) + + if not cases: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_guild_no_cases_author"], + description=CONST.STRINGS["case_guild_no_cases"], + ) + await ctx.send(embed=embed) + return + + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True) + + for i in range(0, len(cases), 10): + chunk = cases[i : i + 10] + embed = create_case_list_embed( + ctx, + chunk, + CONST.STRINGS["case_guild_cases_author"], + ) + menu.add_page(embed) + + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"), + ) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"), + ) + menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩")) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"), + ) + + await menu.start() + + @commands.hybrid_command(name="modcases", aliases=["mc", "modc"], description="View all cases in the guild") + @commands.has_permissions(manage_messages=True) + async def view_all_cases_by_mod(self, ctx: commands.Context[commands.Bot], moderator: discord.Member) -> None: + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + cases = case_service.fetch_cases_by_moderator(guild_id, moderator.id) + + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True) + + if not cases: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_mod_no_cases_author"], + description=CONST.STRINGS["case_mod_no_cases"], + ) + await ctx.send(embed=embed) + return + + for i in range(0, len(cases), 10): + chunk = cases[i : i + 10] + embed = create_case_list_embed( + ctx, + chunk, + CONST.STRINGS["case_mod_cases_author"].format(moderator.name), + ) + menu.add_page(embed) + + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"), + ) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"), + ) + menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩")) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"), + ) + + await menu.start() + + @commands.hybrid_command(name="editcase", aliases=["ec"], description="Edit the reason for a case") + @commands.has_permissions(manage_messages=True) + async def edit_case_reason(self, ctx: commands.Context[commands.Bot], case_number: int, *, new_reason: str): + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + + case_service.edit_case_reason( + guild_id, + case_number, + new_reason, + ) + + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_reason_update_author"], + description=CONST.STRINGS["case_reason_update_description"].format( + format_case_number(case_number), + ), + ) + + async def update_tasks(): + await asyncio.gather( + ctx.send(embed=embed), + edit_case_modlog(ctx, guild_id, case_number, new_reason), + ) + + await update_tasks() + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Cases(bot)) diff --git a/poetry.lock b/poetry.lock index 40b5a73..14b513e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1056,6 +1056,20 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "reactionmenu" +version = "3.1.7" +description = "A library to create a discord.py 2.0+ paginator. Supports pagination with buttons, reactions, and category selection using selects." +optional = false +python-versions = ">=3.8" +files = [ + {file = "reactionmenu-3.1.7-py3-none-any.whl", hash = "sha256:51a217c920382dfecbb2f05d60bd20b79ed9895e9f5663f6c0edb75e806f863a"}, + {file = "reactionmenu-3.1.7.tar.gz", hash = "sha256:10da3c1966de2b6264fcdf72537348923c5e151501644375c25f430bfd870463"}, +] + +[package.dependencies] +"discord.py" = ">=2.0.0" + [[package]] name = "requests" version = "2.32.3" @@ -1310,4 +1324,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4a7a75036f4de7e0126a8f6b058eb3deb52f710becf44e4da6bac4a0ea0a1a2f" +content-hash = "122c3bd137956c87143ead4ae71a7800669a9f379a62c93da21a7ef61231c0f5" diff --git a/pyproject.toml b/pyproject.toml index eab4b62..4ff0adc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ ruff = "^0.6.2" typing-extensions = "^4.12.2" pydantic = "^2.8.2" pytimeparse = "^1.1.8" +reactionmenu = "^3.1.7" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/stubs/reactionmenu/__init__.pyi b/stubs/reactionmenu/__init__.pyi new file mode 100644 index 0000000..f7a090f --- /dev/null +++ b/stubs/reactionmenu/__init__.pyi @@ -0,0 +1,21 @@ +""" +This type stub file was generated by pyright. +""" + +from .buttons import ReactionButton, ViewButton +from .core import ReactionMenu +from .views_menu import ViewMenu, ViewSelect +from .abc import Page + +""" +reactionmenu • discord pagination +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A library to create a discord.py 2.0+ paginator. Supports pagination with buttons, reactions, and category selection using selects. + +:copyright: (c) 2021-present @defxult +:license: MIT + +""" +__source__ = ... +__all__ = ('ReactionMenu', 'ReactionButton', 'ViewMenu', 'ViewButton', 'ViewSelect', 'Page') diff --git a/stubs/reactionmenu/abc.pyi b/stubs/reactionmenu/abc.pyi new file mode 100644 index 0000000..69bdd1a --- /dev/null +++ b/stubs/reactionmenu/abc.pyi @@ -0,0 +1,816 @@ +""" +This type stub file was generated by pyright. +""" + +import abc +import discord +from typing import Any, Callable, ClassVar, Final, Generic, List, Literal, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, TypeVar, Union, overload +from datetime import datetime +from typing_extensions import Self +from collections.abc import Sequence +from enum import Enum +from discord.ext.commands import Context +from discord.utils import MISSING +from .decorators import ensure_not_primed +from .errors import * + +""" +MIT License + +Copyright (c) 2021-present @defxult + +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. +""" +if TYPE_CHECKING: + ... +_DYNAMIC_EMBED_LIMIT: Final[int] = ... +_DEFAULT_STYLE: Final[str] = ... +DEFAULT_BUTTONS = ... +DEFAULT = MISSING +GB = TypeVar('GB', bound='_BaseButton') +M = TypeVar('M', bound='_BaseMenu') +class Page: + """Represents a single "page" in the pagination process + + .. added:: v3.1.0 + """ + __slots__ = ... + def __init__(self, *, content: Optional[str] = ..., embed: Optional[discord.Embed] = ..., files: Optional[List[discord.File]] = ...) -> None: + ... + + def __repr__(self) -> str: + ... + + @staticmethod + def from_embeds(embeds: Sequence[discord.Embed]) -> List[Page]: + """|static method| + + Converts a sequence of embeds into a list of :class:`Page` + """ + ... + + + +class PaginationEmojis: + """A set of basic emojis for your convenience to use for your buttons emoji + - ◀️ as `BACK_BUTTON` + - ▶️ as `NEXT_BUTTON` + - ⏪ as `FIRST_PAGE` + - ⏩ as `LAST_PAGE` + - 🔢 as `GO_TO_PAGE` + - ⏹️ as `END_SESSION` + """ + BACK_BUTTON: ClassVar[str] = ... + NEXT_BUTTON: ClassVar[str] = ... + FIRST_PAGE: ClassVar[str] = ... + LAST_PAGE: ClassVar[str] = ... + GO_TO_PAGE: ClassVar[str] = ... + END_SESSION: ClassVar[str] = ... + + +class _PageController: + def __init__(self, pages: List[Page]) -> None: + ... + + @property + def current_page(self) -> Page: + ... + + @property + def total_pages(self) -> int: + """Return the total amount of pages registered to the menu""" + ... + + def validate_index(self) -> Page: + """If the index is out of bounds, assign the appropriate values so the pagination process can continue and return the associated page""" + ... + + def skip_loop(self, action: str, amount: int) -> None: + """Using `self.index += amount` does not work because this library is used to operating on a +-1 basis. This loop + provides a simple way to still operate on the +-1 standard. + """ + ... + + def skip(self, skip: _BaseButton.Skip) -> Page: + """Return the page that the skip value was set to""" + ... + + def next(self) -> Page: + """Return the next page in the pagination process""" + ... + + def prev(self) -> Page: + """Return the previous page in the pagination process""" + ... + + def first_page(self) -> Page: + """Return the first page in the pagination process""" + ... + + def last_page(self) -> Page: + """Return the last page in the pagination process""" + ... + + + +class _MenuType(Enum): + TypeEmbed = ... + TypeEmbedDynamic = ... + TypeText = ... + + +MenuType = _MenuType +class _LimitDetails(NamedTuple): + limit: int + per: str + message: str + set_by_user: bool = ... + @classmethod + def default(cls) -> Self: + ... + + + +class _BaseButton(Generic[GB], metaclass=abc.ABCMeta): + Emojis: ClassVar[PaginationEmojis] = ... + def __init__(self, name: str, event: Optional[_BaseButton.Event], skip: _BaseButton.Skip) -> None: + ... + + @property + @abc.abstractmethod + def menu(self): + ... + + @property + def clicked_by(self) -> Set[discord.Member]: + """ + Returns + ------- + Set[:class:`discord.Member`]: The members who clicked the button + """ + ... + + @property + def total_clicks(self) -> int: + """ + Returns + ------- + :class:`int`: The amount of clicks on the button + """ + ... + + @property + def last_clicked(self) -> Optional[datetime]: + """ + Returns + ------- + Optional[:class:`datetime.datetime`]: The time in UTC for when the button was last clicked. Can be :class:`None` if the button has not been clicked + """ + ... + + class Skip: + """Initialize a skip button with the appropriate values + + Parameters + ---------- + action: :class:`str` + Whether to go forward in the pagination process ("+") or backwards ("-") + + amount: :class:`int` + The amount of pages to skip. Must be >= 1. If value is <= 0, it is implicitly set to 2 + """ + def __init__(self, action: Literal['+', '-'], amount: int) -> None: + ... + + + + class Event: + """Set a button to be disabled or removed when it has been pressed a certain amount of times. If the button is a :class:`ReactionButton`, only the "remove" event is available + + Parameters + ---------- + event: :class:`str` + The action to take. Can either be "disable" or "remove" + + value: :class:`int` + The amount set for the specified event. Must be >= 1. If value is <= 0, it is implicitly set to 1""" + _DISABLE = ... + _REMOVE = ... + def __init__(self, event_type: Literal['disable', 'remove'], value: int) -> None: + ... + + + + + +class _BaseMenu(metaclass=abc.ABCMeta): + TypeEmbed: Final[_MenuType] = ... + TypeEmbedDynamic: Final[_MenuType] = ... + TypeText: Final[_MenuType] = ... + _sessions_limit_details = ... + _active_sessions: List[Self] + def __init__(self, method: Union[Context, discord.Interaction], /, menu_type: _MenuType, **kwargs) -> None: + ... + + @abc.abstractmethod + def remove_all_buttons(self): + ... + + @abc.abstractmethod + def get_button(self): + ... + + @abc.abstractmethod + def remove_button(self): + ... + + @abc.abstractmethod + def add_button(self): + ... + + @abc.abstractmethod + def add_buttons(self): + ... + + @abc.abstractmethod + def stop(self): + ... + + @abc.abstractmethod + async def start(self): + ... + + @abc.abstractmethod + async def quick_start(cls): + ... + + @staticmethod + def separate(values: Sequence[Any]) -> Tuple[List[discord.Embed], List[str]]: + """|static method| + + Sorts all embeds and strings into a single tuple + + Parameters + ---------- + values: Sequence[`Any`] + The values to separate + + Returns + ------- + Tuple[List[:class:`discord.Embed`], List[:class:`str`]] + + Example + ------- + >>> embeds, strings = .separate([...]) + """ + ... + + @staticmethod + def all_embeds(values: Sequence[Any]) -> bool: + """|static method| + + Tests to see if all items in the sequence are of type :class:`discord.Embed` + + Parameters + ---------- + values: Sequence[`Any`] + The values to test + + Returns + ------- + :class:`bool`: Can return `False` if the sequence is empty + """ + ... + + @staticmethod + def all_strings(values: Sequence[Any]) -> bool: + """|static method| + + Tests to see if all items in the sequence are of type :class:`str` + + Parameters + ---------- + values: Sequence[`Any`] + The values to test + + Returns + ------- + :class:`bool`: Can return `False` if the sequence is empty + """ + ... + + @classmethod + def remove_limit(cls) -> None: + """|class method| + + Remove the limits currently set for menu's + """ + ... + + @classmethod + def get_all_dm_sessions(cls) -> List[Self]: + """|class method| + + Retrieve all active DM menu sessions + + Returns + ------- + A :class:`list` of active DM menu sessions that are currently running. Can be an empty list if there are no active DM sessions + """ + ... + + @classmethod + def get_all_sessions(cls) -> List[Self]: + """|class method| + + Retrieve all active menu sessions + + Returns + ------- + A :class:`list` of menu sessions that are currently running. Can be an empty list if there are no active sessions + """ + ... + + @classmethod + def get_session(cls, name: str) -> List[Self]: + """|class method| + + Get a menu instance by it's name + + Parameters + ---------- + name: :class:`str` + The name of the menu to return + + Returns + ------- + A :class:`list` of menu sessions that are currently running that match the supplied :param:`name`. Can be an empty list if there are no active sessions that matched the :param:`name` + """ + ... + + @classmethod + def get_sessions_count(cls) -> int: + """|class method| + + Returns the number of active sessions + + Returns + ------- + :class:`int`: The amount of menu sessions that are active + """ + ... + + @classmethod + def set_sessions_limit(cls, limit: int, per: Literal['channel', 'guild', 'member'] = ..., message: str = ...) -> None: + """|class method| + + Sets the amount of menu sessions that can be active at the same time per guild, channel, or member. This applies to both :class:`ReactionMenu` & :class:`ViewMenu` + + Parameters + ---------- + limit: :class:`int` + The amount of menu sessions allowed + + per: :class:`str` + How menu sessions should be limited. Options: "channel", "guild", or "member" + + message: :class:`str` + Message that will be sent informing users about the menu limit when the limit is reached. Can be :class:`None` for no message + + Raises + ------ + - `IncorrectType`: The :param:`limit` parameter was not of type :class:`int` + - `MenuException`: The value of :param:`per` was not valid or the limit was not greater than or equal to one + """ + ... + + @classmethod + async def stop_session(cls, name: str, include_all: bool = ...) -> None: + """|coro class method| + + Stop a specific menu with the supplied name + + Parameters + ---------- + name: :class:`str` + The menus name + + include_all: :class:`bool` + If set to `True`, it stops all menu sessions with the supplied :param:`name`. If `False`, stops only the most recently started menu with the supplied :param:`name` + + Raises + ------ + - `MenuException`: The session with the supplied name was not found + """ + ... + + @classmethod + async def stop_all_sessions(cls) -> None: + """|coro class method| + + Stops all menu sessions that are currently running + """ + ... + + @classmethod + def get_menu_from_message(cls, message_id: int, /) -> Optional[Self]: + """|class method| + + Return the menu object associated with the message with the given ID + + Parameters + ---------- + message_id: :class:`int` + The `discord.Message.id` from the menu message + + Returns + ------- + The menu object. Can be :class:`None` if the menu was not found in the list of active menu sessions + """ + ... + + @property + def rows(self) -> Optional[List[str]]: + """ + Returns + ------- + Optional[List[:class:`str`]]: All rows that's been added to the menu. Can return `None` if the menu has not started or the `menu_type` is not `TypeEmbedDynamic` + + .. added: v3.1.0 + """ + ... + + @property + def menu_type(self) -> str: + """ + Returns + ------- + :class:`str`: The `menu_type` you set via the constructor. This will either be `TypeEmbed`, `TypeEmbedDynamic`, or `TypeText` + + .. added:: v3.1.0 + """ + ... + + @property + def last_viewed(self) -> Optional[Page]: + """ + Returns + ------- + Optional[:class:`Page`]: The last page that was viewed in the pagination process. Can be :class:`None` if the menu has not been started + """ + ... + + @property + def owner(self) -> Union[discord.Member, discord.User]: + """ + Returns + ------- + Union[:class:`discord.Member`, :class:`discord.User`]: The owner of the menu (the person that started the menu). If the menu was started in a DM, this will return :class:`discord.User` + """ + ... + + @property + def total_pages(self) -> int: + """ + Returns + ------- + :class:`int`: The amount of pages that have been added to the menu. If the `menu_type` is :attr:`TypeEmbedDynamic`, the amount of pages is not known until AFTER the menu has started. + If attempted to retrieve the value before a dynamic menu has started, this will return a value of -1 + """ + ... + + @property + def pages(self) -> Optional[List[Page]]: + """ + Returns + ------- + Optional[List[:class:`Page`]]: The pages currently applied to the menu. Can return :class:`None` if there are no pages + + Note: If the `menu_type` is :attr:`TypeEmbedDynamic`, the pages aren't known until after the menu has started + """ + ... + + @property + def message(self) -> Optional[Union[discord.Message, discord.InteractionMessage]]: + """ + Returns + ------- + Optional[Union[:class:`discord.Message`, :class:`discord.InteractionMessage`]]: The menu's message object. Can be :class:`None` if the menu has not been started + """ + ... + + @property + def is_running(self) -> bool: + """ + Returns + ------- + :class:`bool`: `True` if the menu is currently running, `False` otherwise + """ + ... + + @property + def in_dms(self) -> bool: + """ + Returns + ------- + :class:`bool`: If the menu was started in a DM + """ + ... + + def randomize_embed_colors(self) -> None: + """Randomize the color of all the embeds that have been added to the menu + + Raises + ------ + - `MenuException`: The `menu_type` was not of `TypeEmbed` + + .. added:: v3.1.0 + """ + ... + + def set_page_director_style(self, style_id: int, separator: str = ...) -> None: + """Set how the page numbers dictating what page you are on (in the footer of an embed/regular message) are displayed + + Parameters + ---------- + style_id: :class:`int` + Varying formats of how the page director can be presented. The following ID's are available: + + - `1` = Page 1/10 + - `2` = Page 1 out of 10 + - `3` = 1 out of 10 + - `4` = 1 • 10 + - `5` = 1 » 10 + - `6` = 1 | 10 + - `7` = 1 : 10 + - `8` = 1 - 10 + - `9` = 1 / 10 + - `10` = 1 🔹 10 + - `11` = 1 🔸 10 + + separator: :class:`str` + The separator between the page director and any text you may have in the embed footer. The default separator is ":". It should be noted that whichever separator you assign, + if you wish to have spacing between the page director and the separator, you must place the space inside the string yourself as such: " :" + + Raises + ------ + - `MenuException`: The :param:`style_id` value was not valid + """ + ... + + async def wait_until_closed(self) -> None: + """|coro| + + Waits until the menu session ends using `.stop()` or when the menu times out. This should not be used inside relays + + .. added:: v3.0.1 + """ + ... + + @ensure_not_primed + def add_from_messages(self, messages: Sequence[discord.Message]) -> None: + """Add pages to the menu using the message object itself + + Parameters + ---------- + messages: Sequence[:class:`discord.Message`] + A sequence of discord message objects + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The messages provided did not have the correct values. For example, the `menu_type` was set to `TypeEmbed`, but the messages you've provided only contains text. If the `menu_type` is `TypeEmbed`, only messages with embeds should be provided + - `IncorrectType`: All messages were not of type :class:`discord.Message` + """ + ... + + @ensure_not_primed + async def add_from_ids(self, messageable: discord.abc.Messageable, message_ids: Sequence[int]) -> None: + """|coro| + + Add pages to the menu using the IDs of messages. This only grabs embeds (if the `menu_type` is :attr:`TypeEmbed`) or the content (if the `menu_type` is :attr:`TypeText`) from the message + + Parameters + ---------- + messageable: :class:`discord.abc.Messageable` + A discord :class:`Messageable` object (:class:`discord.TextChannel`, :class:`commands.Context`, etc.) + + message_ids: Sequence[:class:`int`] + The messages to fetch + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The message IDs provided did not have the correct values when fetched. For example, the `menu_type` was set to `TypeEmbed`, but the messages you've provided for the library to fetch only contains text. If the `menu_type` is `TypeEmbed`, only messages with embeds should be provided + - `MenuException`: An error occurred when attempting to fetch a message or not all :param:`message_ids` were of type int + """ + ... + + @ensure_not_primed + def clear_all_row_data(self) -> None: + """Delete all the data thats been added using :meth:`add_row()` + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: This method was called but the menus `menu_type` was not :attr:`TypeEmbedDynamic` + """ + ... + + @ensure_not_primed + def add_row(self, data: str) -> None: + """Add text to the embed page by rows of data + + Parameters + ---------- + data: :class:`str` + The data to add + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: This method was called but the menus `menu_type` was not :attr:`TypeEmbedDynamic` + - `MissingSetting`: The kwarg "rows_requested" (int) has not been set for the menu + """ + ... + + @ensure_not_primed + def set_main_pages(self, *embeds: discord.Embed) -> None: + """On a menu with a `menu_type` of :attr:`TypeEmbedDynamic`, set the pages you would like to show first. These embeds will be shown before the embeds that contain your data + + Parameters + ---------- + *embeds: :class:`discord.Embed` + An argument list of :class:`discord.Embed` objects + + Raises + ------ + - `MenuSettingsMismatch`: Tried to use method on a menu that was not of `menu_type` :attr:`TypeEmbedDynamic` + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuException`: The "embeds" parameter was empty. At least one value is needed + - `IncorrectType`: All values in the argument list were not of type :class:`discord.Embed` + """ + ... + + @ensure_not_primed + def set_last_pages(self, *embeds: discord.Embed) -> None: + """On a menu with a `menu_type` of :attr:`TypeEmbedDynamic`, set the pages you would like to show last. These embeds will be shown after the embeds that contain your data + + Parameters + ---------- + *embeds: :class:`discord.Embed` + An argument list of :class:`discord.Embed` objects + + Raises + ------ + - `MenuSettingsMismatch`: Tried to use method on a menu that was not of `menu_type` :attr:`TypeEmbedDynamic` + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuException`: The "embeds" parameter was empty. At least one value is needed + - `IncorrectType`: All values in the argument list were not of type :class:`discord.Embed` + """ + ... + + @ensure_not_primed + def add_page(self, embed: Optional[discord.Embed] = ..., content: Optional[str] = ..., files: Optional[List[discord.File]] = ...) -> None: + """Add a page to the menu + + Parameters + ---------- + embed: Optional[:class:`discord.Embed`] + The embed of the page + + content: Optional[:class:`str`] + The text that appears above an embed in a message + + files: Optional[Sequence[:class:`discord.File`]] + Files you'd like to attach to the page + + Raises + ------ + - `MenuException`: Attempted to add a page with no parameters + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The page being added does not match the menus `menu_type` + + .. changes:: + v3.1.0 + Added parameter content + Added parameter embed + Added parameter files + Removed parameter "page" + """ + ... + + @overload + def add_pages(self, pages: Sequence[discord.Embed]) -> None: + ... + + @overload + def add_pages(self, pages: Sequence[str]) -> None: + ... + + @ensure_not_primed + def add_pages(self, pages: Sequence[Union[discord.Embed, str]]) -> None: + """Add multiple pages to the menu at once + + Parameters + ---------- + pages: Sequence[Union[:class:`discord.Embed`, :class:`str`]] + The pages to add. Can only be used when the menus `menu_type` is :attr:`TypeEmbed` (adding a :class:`discord.Embed`) + or :attr:`TypeText` (adding a :class:`str`) + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The page being added does not match the menus `menu_type` + """ + ... + + @ensure_not_primed + def remove_all_pages(self) -> None: + """Remove all pages from the menu + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + """ + ... + + @ensure_not_primed + def remove_page(self, page_number: int) -> None: + """Remove a page from the menu + + Parameters + ---------- + page_number: :class:`int` + The page to remove + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `InvalidPage`: The page associated with the given page number was not valid + """ + ... + + def set_on_timeout(self, func: Callable[[M], None]) -> None: + """Set the function to be called when the menu times out + + Parameters + ---------- + func: Callable[[:type:`M`]], :class:`None`] + The function object that will be called when the menu times out. The function should contain a single positional argument + and should not return anything. The argument passed to that function is an instance of the menu. + + Raises + ------ + - `IncorrectType`: Parameter "func" was not a callable object + """ + ... + + def remove_on_timeout(self) -> None: + """Remove the timeout call to the function you have set when the menu times out""" + ... + + def set_relay(self, func: Callable[[NamedTuple], None], *, only: Optional[List[GB]] = ...) -> None: + """Set a function to be called with a given set of information when a button is pressed on the menu. The information passed is `RelayPayload`, a named tuple. + The named tuple contains the following attributes: + + - `member`: The :class:`discord.Member` object of the person who pressed the button. Could be :class:`discord.User` if the menu was started in a DM + - `button`: Depending on the menu instance, the :class:`ReactionButton` or :class:`ViewButton` object of the button that was pressed + + Parameters + ---------- + func: Callable[[:class:`NamedTuple`], :class:`None`] + The function should only contain a single positional argument. Command functions (`@bot.command()`) not supported + + only: Optional[List[:generic:`GB`]] + A list of buttons (`GB`) associated with the current menu instance. If the menu instance is :class:`ReactionMenu`, this should be a list of :class:`ReactionButton` + and vice-versa for :class:`ViewMenu` instances. If this is :class:`None`, all buttons on the menu will be relayed. If set, only button presses from those specified buttons will be relayed + + Raises + ------ + - `IncorrectType`: The :param:`func` argument provided was not callable + """ + ... + + def remove_relay(self) -> None: + """Remove the relay that's been set""" + ... + + + diff --git a/stubs/reactionmenu/buttons.pyi b/stubs/reactionmenu/buttons.pyi new file mode 100644 index 0000000..a713cbb --- /dev/null +++ b/stubs/reactionmenu/buttons.pyi @@ -0,0 +1,549 @@ +""" +This type stub file was generated by pyright. +""" + +import discord +from typing import Any, Callable, Dict, Final, Iterable, List, Literal, NamedTuple, Optional, TYPE_CHECKING, Union +from . import ReactionButton, ReactionMenu, ViewMenu +from enum import Enum +from .abc import _BaseButton + +""" +MIT License + +Copyright (c) 2021-present @defxult + +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. +""" +if TYPE_CHECKING: + ... +class _Details(NamedTuple): + """Used for buttons with a `custom_id` of `ID_CALLER`""" + func: Callable[..., None] + args: Iterable[Any] + kwargs: Dict[str, Any] + ... + + +Details = _Details +class ViewButton(discord.ui.Button, _BaseButton): + """A helper class for :class:`ViewMenu`. Represents a UI button. + + Parameters + ---------- + style: :class:`discord.ButtonStyle` + The style of the button + + label: Optional[:class:`str`] + The button label, if any + + disabled: :class:`bool` + Whether the button is disabled or not + + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. If this button is for a URL, it does not have a custom ID + + url: Optional[:class:`str`] + The URL this button sends you to + + emoji: Optional[Union[:class:`str`, :class:`discord.PartialEmoji`]] + The emoji of the button, if available + + followup: Optional[:class:`ViewButton.Follow`] + Used with buttons with custom_id :attr:`ViewButton.ID_CALLER`, :attr:`ViewButton.ID_SEND_MESSAGE`, :attr:`ViewButton.ID_CUSTOM_EMBED` + + event: Optional[:class:`ViewButton.Event`] + Set the button to be disabled or removed when it has been pressed a certain amount of times + + Kwargs + ------ + name: :class:`str` + An optional name for the button. Can be set to retrieve it later via :meth:`ViewMenu.get_button()` + + skip: :class:`ViewButton.Skip` + Set the action and the amount of pages to skip when using a `custom_id` of `ViewButton.ID_SKIP` + + persist: :class:`bool` + Available only when using link buttons. This prevents link buttons from being disabled/removed when the menu times out or is stopped so they can remain clickable + + .. added v3.1.0 + :param:`persist` + """ + ID_NEXT_PAGE: Final[str] = ... + ID_PREVIOUS_PAGE: Final[str] = ... + ID_GO_TO_FIRST_PAGE: Final[str] = ... + ID_GO_TO_LAST_PAGE: Final[str] = ... + ID_GO_TO_PAGE: Final[str] = ... + ID_END_SESSION: Final[str] = ... + ID_CALLER: Final[str] = ... + ID_SEND_MESSAGE: Final[str] = ... + ID_CUSTOM_EMBED: Final[str] = ... + ID_SKIP: Final[str] = ... + _RE_IDs = ... + _RE_UNIQUE_ID_SET = ... + def __init__(self, *, style: discord.ButtonStyle = ..., label: Optional[str] = ..., disabled: bool = ..., custom_id: Optional[str] = ..., url: Optional[str] = ..., emoji: Optional[Union[str, discord.PartialEmoji]] = ..., followup: Optional[ViewButton.Followup] = ..., event: Optional[ViewButton.Event] = ..., **kwargs) -> None: + ... + + def __repr__(self): # -> str: + ... + + async def callback(self, interaction: discord.Interaction) -> None: + """*INTERNAL USE ONLY* - The callback function from the button interaction. This should not be manually called""" + ... + + class Followup: + """A class that represents the message sent using a :class:`ViewButton`. Contains parameters similar to method `discord.abc.Messageable.send`. Only to be used with :class:`ViewButton` kwarg "followup". + It is to be noted that this should not be used with :class:`ViewButton` with a "style" of `discord.ButtonStyle.link` because link buttons do not send interaction events. + + Parameters + ---------- + content: Optional[:class:`str`] + Message to send + + embed: Optional[:class:`discord.Embed`] + Embed to send. Can also bet set for buttons with a custom_id of :attr:`ViewButton.ID_CUSTOM_EMBED` + + file: Optional[:class:`discord.File`] + File to send. If the :class:`ViewButton` custom_id is :attr:`ViewButton.ID_SEND_MESSAGE`, the file will be ignored because of discord API limitations + + tts: :class:`bool` + If discord should read the message aloud. Not valid for `ephemeral` messages + + allowed_mentions: Optional[:class:`discord.AllowedMentions`] + Controls the mentions being processed in the menu message. Not valid for `ephemeral` messages + + delete_after: Optional[Union[:class:`int`, :class:`float`]] + Amount of time to wait before the message is deleted. Not valid for `ephemeral` messages + + ephemeral: :class:`bool` + If the message will be hidden from everyone except the person that pressed the button. This is only valid for a :class:`ViewButton` with custom_id :attr:`ViewButton.ID_SEND_MESSAGE` + + Kwargs + ------ + details: :meth:`ViewButton.Followup.set_caller_details()` + The information that will be used when a `ViewButton.ID_CALLER` button is pressed (defaults to :class:`None`) + """ + __slots__ = ... + def __repr__(self): # -> LiteralString: + ... + + def __init__(self, content: Optional[str] = ..., *, embed: Optional[discord.Embed] = ..., file: Optional[discord.File] = ..., tts: bool = ..., allowed_mentions: Optional[discord.AllowedMentions] = ..., delete_after: Optional[Union[int, float]] = ..., ephemeral: bool = ..., **kwargs) -> None: + ... + + @staticmethod + def set_caller_details(func: Callable[..., None], *args, **kwargs) -> Details: + """|static method| + + Set the parameters for the function you set for a :class:`ViewButton` with the custom_id :attr:`ViewButton.ID_CALLER` + + Parameters + ---------- + func: Callable[..., :class:`None`] + The function object that will be called when the associated button is pressed + + *args: `Any` + An argument list that represents the parameters of that function + + **kwargs: `Any` + An argument list that represents the kwarg parameters of that function + + Returns + ------- + :class:`Details`: The :class:`NamedTuple` containing the values needed to internally call the function you have set + + Raises + ------ + - `IncorrectType`: Parameter "func" was not a callable object + """ + ... + + + + @property + def menu(self) -> Optional[ViewMenu]: + """ + Returns + ------- + Optional[:class:`ViewMenu`]: The menu instance this button is attached to. Could be :class:`None` if the button is not attached to a menu + """ + ... + + @classmethod + def generate_skip(cls, label: str, action: Literal['+', '-'], amount: int) -> ViewButton: + """|class method| + + A factory method that returns a :class:`ViewButton` with the following parameters set: + + - style: `discord.ButtonStyle.gray` + - label: `