class ResultMenu(menus.MenuPages, inherit_buttons=False): def __init__(self, **kwargs): super().__init__( **kwargs, timeout=60, clear_reactions_after=True, delete_message_after=True, ) def _skip_double_triangle_buttons(self): return super()._skip_double_triangle_buttons() async def finalize(self, timed_out): if timed_out and self.delete_message_after: self.delete_message_after = False @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(1)) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0)) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages(self, payload) -> None: self.stop()
class MenuActions(menus.MenuPages, inherit_buttons=False): def reaction_check(self, payload): """The function that is used to check whether the payload should be processed. This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`. There should be no reason to override this function for most users. This is done this way in this cog to let a bot owner operate the menu along with the original command invoker. Parameters ------------ payload: :class:`discord.RawReactionActionEvent` The payload to check. Returns --------- :class:`bool` Whether the payload should be processed. """ if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons async def show_checked_page(self, page_number: int) -> None: # This is a custom impl of show_checked_page that allows looping around back to the # beginning of the page stack when at the end and using next, or looping to the end # when at the beginning page and using prev. max_pages = self._source.get_max_pages() try: if max_pages is None: await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) elif max_pages > page_number >= 0: await self.show_page(page_number) except IndexError: pass @menus.button("\N{UP-POINTING RED TRIANGLE}", position=menus.First(1)) async def prev(self, payload: discord.RawReactionActionEvent): await self.show_checked_page(self.current_page - 1) @menus.button("\N{DOWN-POINTING RED TRIANGLE}", position=menus.First(2)) async def next(self, payload: discord.RawReactionActionEvent): await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}", position=menus.Last(0)) async def close_menu(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class ButtonMenu(BaseButtonMenu, inherit_buttons=False): def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 @menus.button( LEFT_ARROW, position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, button: InteractionButton): await self.show_checked_page(self.current_page - 1, button) @menus.button( RIGHT_ARROW, position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, button: InteractionButton): await self.show_checked_page(self.current_page + 1, button) @menus.button( REWIND_ARROW, position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, button: InteractionButton): await self.show_checked_page(0, button) @menus.button( FORWARD_ARROW, position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, button: InteractionButton): await self.show_checked_page(self._source.get_max_pages() - 1, button) @menus.button(CLOSE_EMOJI) async def stop_pages(self, button: InteractionButton): await self.message.delete() # if self.clear_reactions_after: # await self.close_buttons(button) self.stop()
def __init__( self, source: menus.PageSource, cog: commands.Cog, user_token: tekore.Token, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) self.user_token = user_token self.cog = cog self.add_button( menus.Button(emoji_handler.get_emoji("next"), self.skip_next, position=menus.First(6)) ) self.add_button( menus.Button( emoji_handler.get_emoji("previous"), self.skip_previous, position=menus.First(0) ) ) self.add_button( menus.Button( emoji_handler.get_emoji("playpause"), self.play_pause, position=menus.First(2) ) ) self.add_button( menus.Button( emoji_handler.get_emoji("playall"), self.play_pause_all, position=menus.First(3), skip_if=self._skip_play_all, ) ) self.add_button( menus.Button(emoji_handler.get_emoji("like"), self.like_song, position=menus.First(4)) ) self.add_button( menus.Button( emoji_handler.get_emoji("back_left"), self.go_to_previous_page, position=menus.First(1), ) ) self.add_button( menus.Button( emoji_handler.get_emoji("play"), self.go_to_next_page, position=menus.First(5) ) )
def __init__(self, cog, game: Connect4Game): self.cog = cog self.game = game super().__init__( timeout=self.GAME_TIMEOUT_THRESHOLD, delete_message_after=False, clear_reactions_after=True, ) for index, digit in enumerate(self.DIGITS): self.add_button( menus.Button(digit, self.handle_digit_press, position=menus.First(index)) )
class BaseMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) self.__tasks = self._Menu__tasks async def update(self, payload): """|coro| Updates the menu after an event has been received. Parameters ----------- payload: :class:`discord.RawReactionActionEvent` The reaction event that triggered this update. """ button = self.buttons[payload.emoji] if not self._running: return try: if button.lock: async with self._lock: if self._running: await button(self, payload) else: await button(self, payload) except Exception as exc: log.debug("Ignored exception on reaction event", exc_info=exc) async def start(self, ctx, *, channel=None, wait=False, page: int = 0): """ Starts the interactive menu session. Parameters ----------- ctx: :class:`Context` The invocation context to use. channel: :class:`discord.abc.Messageable` The messageable to send the message to. If not given then it defaults to the channel in the context. wait: :class:`bool` Whether to wait until the menu is completed before returning back to the caller. Raises ------- MenuError An error happened when verifying permissions. discord.HTTPException Adding a reaction failed. """ # Clear the buttons cache and re-compute if possible. try: del self.buttons except AttributeError: pass self.bot = bot = ctx.bot self.ctx = ctx self._author_id = ctx.author.id channel = channel or ctx.channel is_guild = isinstance(channel, discord.abc.GuildChannel) me = ctx.guild.me if is_guild else ctx.bot.user permissions = channel.permissions_for(me) self.__me = discord.Object(id=me.id) self._verify_permissions(ctx, channel, permissions) self._event.clear() msg = self.message if msg is None: self.message = msg = await self.send_initial_message(ctx, channel, page=page) if self.should_add_reactions(): # Start the task first so we can listen to reactions before doing anything for task in self.__tasks: task.cancel() self.__tasks.clear() self._running = True self.__tasks.append(bot.loop.create_task(self._internal_loop())) if self.should_add_reactions(): async def add_reactions_task(): for emoji in self.buttons: await msg.add_reaction(emoji) self.__tasks.append(bot.loop.create_task(add_reactions_task())) if wait: await self._event.wait() async def send_initial_message(self, ctx: commands.Context, channel: discord.abc.Messageable, page: int = 0): """ The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ self.current_page = page page = await self._source.get_page(page) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def show_checked_page(self, page_number: int) -> None: max_pages = self._source.get_max_pages() try: if max_pages is None: # If it doesn't give maximum pages, it cannot be checked await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) elif max_pages > page_number >= 0: await self.show_page(page_number) except IndexError: # An error happened that can be handled, so ignore it. pass def reaction_check(self, payload): """Just extends the default reaction_check to use owner_ids""" if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" self.stop()
class GamesMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) async def update(self, payload): """|coro| Updates the menu after an event has been received. Parameters ----------- payload: :class:`discord.RawReactionActionEvent` The reaction event that triggered this update. """ button = self.buttons[payload.emoji] if not self._running: return try: if button.lock: async with self._lock: if self._running: await button(self, payload) else: await button(self, payload) except Exception as exc: log.debug("Ignored exception on reaction event", exc_info=exc) async def show_page(self, page_number, *, skip_next=False, skip_prev=False): try: page = await self._source.get_page(page_number, skip_next=skip_next, skip_prev=skip_prev) except NoSchedule: team = "" if self.source.team: team = _("for {teams} ").format( teams=humanize_list(self.source.team)) msg = _( "No schedule could be found {team}in dates between {last_searched}" ).format(team=team, last_searched=self.source._last_searched) await self.message.edit(content=msg, embed=None) return self.current_page = page_number kwargs = await self._get_kwargs_from_page(page) await self.message.edit(**kwargs) async def send_initial_message(self, ctx, channel): """|coro| The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ try: page = await self._source.get_page(0) except (IndexError, NoSchedule): return await channel.send(self.format_error()) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) def format_error(self): team = "" if self.source.team: team = _("for {teams} ").format( teams=humanize_list(self.source.team)) msg = _( "No schedule could be found {team}in dates between {last_searched}" ).format(team=team, last_searched=self.source._last_searched) return msg async def show_checked_page(self, page_number: int) -> None: try: await self.show_page(page_number) except IndexError: # An error happened that can be handled, so ignore it. pass def reaction_check(self, payload): """Just extends the default reaction_check to use owner_ids""" if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(1), ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(0), ) async def go_to_next_page(self, payload): """go to the next page""" # log.info(f"Moving to next page, {self.current_page + 1}") await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.First(0), ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0, skip_prev=True) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.Last(1), ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(0, skip_next=True) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" self.stop() await self.message.delete() @menus.button("\N{TEAR-OFF CALENDAR}") async def choose_date(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" send_msg = await self.ctx.send( _("Enter the date you would like to see `YYYY-MM-DD` format is accepted." )) def check(m: discord.Message): return m.author == self.ctx.author and DATE_RE.search( m.clean_content) try: msg = await self.ctx.bot.wait_for("message", check=check, timeout=30) except asyncio.TimeoutError: await send_msg.delete() return search = DATE_RE.search(msg.clean_content) if search: date_str = f"{search.group(1)}-{search.group(3)}-{search.group(4)}" date = datetime.strptime(date_str, "%Y-%m-%d") # log.debug(date) self.source.date = date try: await self.source.prepare() except NoSchedule: return await self.ctx.send(self.format_error()) await self.show_page(0) @menus.button("\N{FAMILY}") async def choose_teams(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" send_msg = await self.ctx.send( _("Enter the team you would like to filter for.")) def check(m: discord.Message): return m.author == self.ctx.author try: msg = await self.ctx.bot.wait_for("message", check=check, timeout=30) except asyncio.TimeoutError: await send_msg.delete() return potential_teams = msg.clean_content.split() teams: List[str] = [] for team, data in TEAMS.items(): if "Team" in teams: continue nick = data["nickname"] short = data["tri_code"] pattern = fr"{short}\b|" + r"|".join(fr"\b{i}\b" for i in team.split()) if nick: pattern += r"|" + r"|".join(fr"\b{i}\b" for i in nick) # log.debug(pattern) reg: Pattern = re.compile(fr"\b{pattern}", flags=re.I) for pot in potential_teams: find = reg.findall(pot) if find: teams.append(team) self.source.team = teams try: await self.source.prepare() except NoSchedule: return await self.ctx.send(self.format_error()) await self.show_page(0)
class GenericMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: Optional[commands.Cog] = None, ctx=None, clear_reactions_after: bool = True, delete_message_after: bool = False, add_reactions: bool = True, using_custom_emoji: bool = False, using_embeds: bool = False, keyword_to_reaction_mapping: Dict[str, str] = None, timeout: int = 180, message: discord.Message = None, **kwargs: Any, ) -> None: self.cog = cog self.ctx = ctx super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, check_embeds=using_embeds, timeout=timeout, message=message, **kwargs, ) def reaction_check(self, payload): """The function that is used to check whether the payload should be processed. This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`. There should be no reason to override this function for most users. Parameters ------------ payload: :class:`discord.RawReactionActionEvent` The payload to check. Returns --------- :class:`bool` Whether the payload should be processed. """ if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 # left @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}", position=menus.First(1), skip_if=_skip_single_arrows) async def prev(self, payload: discord.RawReactionActionEvent): if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages_default( self, payload: discord.RawReactionActionEvent) -> None: self.stop() with contextlib.suppress(discord.NotFound): await self.message.delete() @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}", position=menus.First(2), skip_if=_skip_single_arrows) async def next(self, payload: discord.RawReactionActionEvent): if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload: discord.RawReactionActionEvent): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload: discord.RawReactionActionEvent): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1)
class ReTriggerMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: Optional[commands.Cog] = None, page_start: Optional[int] = 0, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) self.cog = cog self.page_start = page_start async def send_initial_message(self, ctx, channel): """|coro| The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def update(self, payload): """|coro| Updates the menu after an event has been received. Parameters ----------- payload: :class:`discord.RawReactionActionEvent` The reaction event that triggered this update. """ button = self.buttons[payload.emoji] if not self._running: return try: if button.lock: async with self._lock: if self._running: await button(self, payload) else: await button(self, payload) except Exception as exc: log.debug("Ignored exception on reaction event", exc_info=exc) async def show_checked_page(self, page_number: int) -> None: max_pages = self._source.get_max_pages() try: if max_pages is None: # If it doesn't give maximum pages, it cannot be checked await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) elif max_pages > page_number >= 0: await self.show_page(page_number) except IndexError: # An error happened that can be handled, so ignore it. pass def reaction_check(self, payload): """Just extends the default reaction_check to use owner_ids""" if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" self.stop() await self.message.delete() @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}" ) async def toggle_trigger(self, payload: discord.RawReactionActionEvent) -> None: """Enables and disables triggers""" member = self.ctx.guild.get_member(payload.user_id) if await self.cog.can_edit(member, self.source.selection): self.source.selection.toggle() await self.show_checked_page(self.current_page) @menus.button("\N{NEGATIVE SQUARED CROSS MARK}") async def stop_trigger(self, payload: discord.RawReactionActionEvent) -> None: """Enables and disables triggers""" member = self.ctx.guild.get_member(payload.user_id) if await self.cog.can_edit(member, self.source.selection): self.source.selection.disable() await self.show_checked_page(self.current_page) @menus.button("\N{WHITE HEAVY CHECK MARK}") async def enable_trigger(self, payload: discord.RawReactionActionEvent) -> None: """Enables and disables triggers""" member = self.ctx.guild.get_member(payload.user_id) if await self.cog.can_edit(member, self.source.selection): self.source.selection.enable() await self.show_checked_page(self.current_page) @menus.button("\N{PUT LITTER IN ITS PLACE SYMBOL}") async def delete_trigger(self, payload: discord.RawReactionActionEvent) -> None: """Enables and disables triggers""" member = self.ctx.guild.get_member(payload.user_id) if await self.cog.can_edit(member, self.source.selection): msg = await self.ctx.send( _("Are you sure you want to delete trigger {name}?").format( name=self.source.selection.name)) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, self.ctx.author) await self.ctx.bot.wait_for("reaction_add", check=pred) if pred.result: await msg.delete() self.source.selection.disable() done = await self.cog.remove_trigger( payload.guild_id, self.source.selection.name) if done: page = await self._source.get_page(self.current_page) kwargs = await self._get_kwargs_from_page(page) await self.message.edit( content=_("This trigger has been deleted."), embed=kwargs["embed"]) for t in self.cog.triggers[self.ctx.guild.id]: if t.name == self.source.selection.name: self.cog.triggers[self.ctx.guild.id].remove(t)
class PokeListMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: Optional[commands.Cog] = None, ctx=None, user=None, clear_reactions_after: bool = True, delete_message_after: bool = False, add_reactions: bool = True, using_custom_emoji: bool = False, using_embeds: bool = False, keyword_to_reaction_mapping: Dict[str, str] = None, timeout: int = 180, message: discord.Message = None, **kwargs: Any, ) -> None: self.cog = cog self.ctx = ctx self.user = user self._search_lock = asyncio.Lock() self._search_task: asyncio.Task = None super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, check_embeds=using_embeds, timeout=timeout, message=message, **kwargs, ) async def finalize(self, timed_out): if not self._running: return await self.stop(do_super=False) async def stop(self, do_super: bool = True): if self._search_task is not None: self._search_task.cancel() if do_super: super().stop() async def _number_page_task(self): async def cleanup(messages: List[discord.Message]): with contextlib.suppress(discord.HTTPException): for msg in messages: await msg.delete() async with self._search_lock: prompt = await self.ctx.send( _("Please select the Pokémon ID number to jump to.")) try: pred = MessagePredicate.valid_int(self.ctx) msg = await self.bot.wait_for("message_without_command", check=pred, timeout=10.0) jump_page = int(msg.content) if jump_page > self._source.get_max_pages(): await self.ctx.send( _("Invalid Pokémon ID, jumping to the end."), delete_after=5) jump_page = self._source.get_max_pages() await self.show_checked_page(jump_page - 1) await cleanup([prompt, msg]) except (ValueError, asyncio.TimeoutError, asyncio.CancelledError): await cleanup([prompt]) self._search_task = None def reaction_check(self, payload): """The function that is used to check whether the payload should be processed. This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`. There should be no reason to override this function for most users. Parameters ------------ payload: :class:`discord.RawReactionActionEvent` The payload to check. Returns --------- :class:`bool` Whether the payload should be processed. """ if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _cant_select(self): return self.ctx.author != self.user @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}", position=menus.First(0)) async def prev(self, payload: discord.RawReactionActionEvent): if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{CROSS MARK}", position=menus.First(1)) async def stop_pages_default( self, payload: discord.RawReactionActionEvent) -> None: with contextlib.suppress(discord.NotFound): await self.message.delete() await self.stop() @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}", position=menus.First(2)) async def next(self, payload: discord.RawReactionActionEvent): if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button("\N{LEFT-POINTING MAGNIFYING GLASS}", position=menus.First(4)) async def number_page(self, payload: discord.RawReactionActionEvent): if self._search_lock.locked() and self._search_task is not None: return self._search_task = asyncio.get_running_loop().create_task( self._number_page_task()) @menus.button("\N{WHITE HEAVY CHECK MARK}", position=menus.First(3), skip_if=_cant_select) async def select(self, payload: discord.RawReactionActionEvent): command = self.ctx.bot.get_command("select") await self.ctx.invoke(command, _id=self.current_page + 1)
class Menu(menus.MenuPages, inherit_buttons=False): def __init__(self, source: Page): super().__init__(source) async def show_checked_page(self, page_number: int): max_pages = self.source.get_max_pages() try: if max_pages is None or max_pages > page_number >= 0: await self.show_page(page_number) elif max_pages <= page_number: await self.show_page(0) elif 0 > page_number: await self.show_page(max_pages - 1) except IndexError: pass async def send_initial_message( self, ctx: commands.Context, channel: discord.TextChannel) -> discord.Message: page = await self.source.get_page(0) self.current_page = 0 kwargs = await self._get_kwargs_from_page(page) return await ctx.send(**kwargs) def _skip_double_triangle_buttons(self) -> bool: max_pages = self.source.get_max_pages() if max_pages is None: return True return max_pages < 5 def _skip_single_triangle_buttons(self) -> bool: max_pages = self.source.get_max_pages() if max_pages is None: return True return max_pages == 1 @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): await self.show_page(self.source.get_max_pages() - 1) @menus.button( "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_single_triangle_buttons, ) async def go_to_previous_page(self, payload): await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_single_triangle_buttons, ) async def go_to_next_page(self, payload): await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload): self.stop() with suppress(discord.Forbidden): await self.message.delete()
channel: discord.abc.Messageable, page: int = 0): """ The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ page = await self._source.get_page(page) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) @_dpy_menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=_dpy_menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @_dpy_menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=_dpy_menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1)
class TopMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, timeout: int = 30, ): super().__init__( source, timeout=timeout, clear_reactions_after=True, delete_message_after=True, ) def _skip_double_triangle_buttons(self): return super()._skip_double_triangle_buttons() async def finalize(self, timed_out): """|coro| A coroutine that is called when the menu loop has completed its run. This is useful if some asynchronous clean-up is required after the fact. Parameters -------------- timed_out: :class:`bool` Whether the menu completed due to timing out. """ if timed_out and self.delete_message_after: self.delete_message_after = False @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(1)) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0)) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{NUMBER SIGN}\ufe0f\N{COMBINING ENCLOSING KEYCAP}", position=menus.Last(2)) async def number_page(self, payload): prompt = await self.ctx.send( "Send a number of page that you wish to see") try: pred = MessagePredicate.positive(self.ctx) msg = await self.bot.wait_for( "message_without_command", check=pred, timeout=10.0, ) if pred.result: jump_page = int(msg.content) if jump_page > self._source.get_max_pages(): jump_page = self._source.get_max_pages() await self.show_checked_page(jump_page - 1) if self.ctx.channel.permissions_for( self.ctx.me).manage_messages: with suppress(discord.HTTPException): await msg.delete() except asyncio.TimeoutError: pass finally: with suppress(discord.HTTPException): await prompt.delete() @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class ClientMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: Optional[commands.Cog] = None, ctx=None, user=None, clear_reactions_after: bool = True, delete_message_after: bool = True, add_reactions: bool = True, using_custom_emoji: bool = False, using_embeds: bool = False, keyword_to_reaction_mapping: Dict[str, str] = None, timeout: int = 180, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, check_embeds=using_embeds, timeout=timeout, message=message, **kwargs, ) def reaction_check(self, payload): """The function that is used to check whether the payload should be processed. This is passed to :meth:`discord.ext.commands.Bot.wait_for <Bot.wait_for>`. There should be no reason to override this function for most users. Parameters ------------ payload: :class:`discord.RawReactionActionEvent` The payload to check. Returns --------- :class:`bool` Whether the payload should be processed. """ if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}", position=menus.First(0)) async def prev(self, payload: discord.RawReactionActionEvent): if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{CROSS MARK}", position=menus.First(1)) async def stop_pages_default( self, payload: discord.RawReactionActionEvent) -> None: self.stop() @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}", position=menus.First(2)) async def next(self, payload: discord.RawReactionActionEvent): if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button("\N{WARNING SIGN}\N{VARIATION SELECTOR-16}", position=menus.First(3)) async def close_ws(self, payload: discord.RawReactionActionEvent): number = self.current_page msg = await self.ctx.send(( f"Are you sure you want to close RPC Client {number + 1}? " "This will prevent them from communicating and may raise errors if not handled properly." )) emojis = ReactionPredicate.YES_OR_NO_EMOJIS start_adding_reactions(msg, emojis) pred = ReactionPredicate.yes_or_no(msg, self.ctx.author) with contextlib.suppress(asyncio.TimeoutError): await self.ctx.bot.wait_for("reaction_add", check=pred, timeout=30) if pred.result: # Definitely do NOT do this at home self.ctx.bot.rpc._rpc.clients[number].ws._reader.set_exception( Exception) await self.ctx.send(f"Successfully closed RPC Client {number + 1}") else: await self.ctx.send("Cancelled.")
class BaseMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: commands.Cog, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, page_start: int = 0, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) self.cog = cog self.page_start = page_start async def send_initial_message(self, ctx, channel): """|coro| The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def show_checked_page(self, page_number: int) -> None: max_pages = self._source.get_max_pages() try: if max_pages is None: # If it doesn't give maximum pages, it cannot be checked await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) elif max_pages > page_number >= 0: await self.show_page(page_number) except IndexError: # An error happened that can be handled, so ignore it. pass def reaction_check(self, payload): """Just extends the default reaction_check to use owner_ids""" if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 def _skip_non_guild_buttons(self) -> bool: if self.ctx.author.id not in self.bot.owner_ids: return True if isinstance(self.source, GuildPages): return False return True @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" self.stop() await self.message.delete() @menus.button("\N{OUTBOX TRAY}", skip_if=_skip_non_guild_buttons) async def leave_guild_button(self, payload): await self.cog.confirm_leave_guild(self.ctx, self.source.guild) @menus.button("\N{INBOX TRAY}", skip_if=_skip_non_guild_buttons) async def make_guild_invite_button(self, payload): invite = await self.cog.get_guild_invite(self.source.guild) if invite: await self.ctx.send(str(invite)) else: await self.ctx.send( _("I cannot find or create an invite for `{guild}`").format( guild=self.source.guild.name))
class MjolnirMenu(menus.MenuPages, inherit_buttons=False): def __init__(self, source: menus.ListPageSource, page_start: int = 0, **kwargs): self.page_start = page_start super().__init__(source=source, **kwargs) async def send_initial_message(self, ctx, channel): self.current_page = self.page_start page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) await channel.send(**kwargs) async def show_checked_page(self, page_number: int): max_pages = self._source.get_max_pages() if max_pages is None or max_pages > page_number >= 0: await self.show_page(page_number) elif page_number > max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 4 @menus.button("\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", position=menus.First(0), skip_if=_skip_double_triangle_buttons) async def go_to_first_page(self, payload): await self.show_checked_page(0) @menus.button("\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons) async def go_to_last_page(self, payload): await self.show_checked_page(self._source.get_max_pages() - 1) @menus.button("\N{LEFTWARDS BLACK ARROW}", position=menus.First(1), skip_if=_skip_single_arrows) async def go_to_previous_page(self, payload): await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHTWARDS ARROW}", position=menus.Last(0), skip_if=_skip_single_arrows) async def go_to_next_page(self, payload): await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload): self.stop() await self.message.delete()
class ChannelsMenu(menus.MenuPages, inherit_buttons=False): def __init__(self, sources: dict, channel_type: str, total_channels: int, timeout: int = 30): super().__init__( sources[next(iter(sources))], timeout=timeout, clear_reactions_after=True, delete_message_after=True, ) self.sources = sources self.channel_type = channel_type self.total_channels = total_channels async def set_source(self, channel_type): self.channel_type = channel_type await self.change_source(self.sources[channel_type]) def should_add_reactions(self): return True @menus.button("\N{BOOKMARK TABS}", position=menus.First(0), skip_if=check_channels("category")) async def switch_category(self, payload): await self.set_source("category") @menus.button("\N{SPEECH BALLOON}", position=menus.First(1), skip_if=check_channels("text")) async def switch_text(self, payload): await self.set_source("text") @menus.button("\N{SPEAKER}", position=menus.First(2), skip_if=check_channels("voice")) async def switch_voice(self, payload): await self.set_source("voice") @menus.button( "\N{SATELLITE ANTENNA}", position=menus.First(3), skip_if=check_channels("stage"), ) async def switch_stage(self, payload): await self.set_source("stage") @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(3), ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(4)) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0)) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}", position=menus.First(5)) async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class JojoMenu(menus.MenuPages, inherit_buttons=False): def __init__(self, source: menus.ListPageSource, timeout: int = 30, delete_message_after: bool = False, clear_reactions_after: bool = True, message: discord.Message = None, page_start: int = 0, **kwargs: typing.Any): super().__init__(source=source, delete_message_after=delete_message_after, timeout=timeout, clear_reactions_after=clear_reactions_after, message=message, **kwargs) self.page_start = page_start async def send_initial_message(self, ctx: commands.Context, channel: discord.TextChannel): self.current_page = self.page_start page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def show_checked_page(self, page_number: int): max_pages = self._source.get_max_pages() if max_pages is None or max_pages > page_number >= 0: await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 4 @menus.button("\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", position=menus.First(0), skip_if=_skip_double_triangle_buttons) async def go_to_first_page(self, payload): await self.show_checked_page(0) @menus.button("\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons) async def go_to_last_page(self, payload): await self.show_checked_page(page_number=self._source.max_pages() - 1) @menus.button("\N{LEFTWARDS BLACK ARROW}", position=menus.First(1), skip_if=_skip_single_arrows) async def go_to_previous_page(self, payload): await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHTWARDS ARROW}", position=menus.Last(0), skip_if=_skip_single_arrows) async def go_to_next_page(self, payload): await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload): self.stop() await self.message.delete()
class WSStatsMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, header: str, timeout: int = 30, image: BytesIO = None, ): super().__init__( source, timeout=timeout, clear_reactions_after=True, delete_message_after=True, ) self.header = header self.image = image def should_add_reactions(self): return True def not_paginating(self): return not self._source.is_paginating() async def send_initial_message(self, ctx, channel): page = await self._source.get_page(0) kwargs = await self._get_kwargs_from_page(page) msg = await channel.send( **kwargs, file=discord.File(self.image, filename="chart.png") if self.image else None, ) if self.image: self.image.close() return msg async def finalize(self, timed_out): """|coro| A coroutine that is called when the menu loop has completed its run. This is useful if some asynchronous clean-up is required after the fact. Parameters -------------- timed_out: :class:`bool` Whether the menu completed due to timing out. """ if timed_out and self.delete_message_after: self.delete_message_after = False async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(1), skip_if=not_paginating, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0), skip_if=not_paginating, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class BadgeMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, timeout: int = 30, can_buy=False, ): super().__init__( source, timeout=timeout, clear_reactions_after=True, delete_message_after=True, ) self.can_buy = can_buy async def start(self, ctx, *, channel=None, wait=False): if self.can_buy: self.can_buy = await ctx.cog.buy_badge.can_run( ctx, check_all_parents=True) await super().start(ctx, channel=channel, wait=wait) def should_add_reactions(self): return True def _no_pages(self): return not self._source.is_paginating() def _skip_double_triangle_buttons(self): return (not self._source.is_paginating() ) or super()._skip_double_triangle_buttons() async def finalize(self, timed_out): """|coro| A coroutine that is called when the menu loop has completed its run. This is useful if some asynchronous clean-up is required after the fact. Parameters -------------- timed_out: :class:`bool` Whether the menu completed due to timing out. """ if timed_out and self.delete_message_after: self.delete_message_after = False def cant_buy_check(self): return not self.can_buy @menus.button("\N{BANKNOTE WITH DOLLAR SIGN}", position=menus.First(0), skip_if=cant_buy_check) async def buy_badge(self, payload): page = await self.source.get_page(self.current_page) await self.ctx.invoke( self.ctx.cog.buy_badge, is_global=True if page["server_id"] == "global" else False, name=page["badge_name"], ) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(1), skip_if=_no_pages) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0), skip_if=_no_pages) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class BackgroundMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, sources: dict, bg_type: str, timeout: int = 30, ): super().__init__( sources[bg_type], timeout=timeout, clear_reactions_after=True, delete_message_after=True, ) self.sources = sources self.bg_type = bg_type async def finalize(self, timed_out): """|coro| A coroutine that is called when the menu loop has completed its run. This is useful if some asynchronous clean-up is required after the fact. Parameters -------------- timed_out: :class:`bool` Whether the menu completed due to timing out. """ if timed_out and self.delete_message_after: self.delete_message_after = False def should_add_reactions(self): return True async def set_source(self, bg_type): self.bg_type = bg_type await self.change_source(self.sources[bg_type]) @menus.button("\N{RECEIPT}", position=menus.First(0)) async def switch_profile(self, payload): await self.set_source("profile") @menus.button("\N{CARD INDEX}", position=menus.First(1)) async def switch_rank(self, payload): await self.set_source("rank") @menus.button("\N{SQUARED UP WITH EXCLAMATION MARK}", position=menus.First(2)) async def switch_levelup(self, payload): await self.set_source("levelup") @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(3), ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(4)) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0)) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}", position=menus.First(5)) async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: self.stop()
class Menu(menus.MenuPages, inherit_buttons=False): # type:ignore message: discord.Message def __init__(self, source: Pages): super().__init__( source, timeout=30.0, delete_message_after=False, clear_reactions_after=True, message=None, ) @property def source(self) -> Pages: return self._source async def send_initial_message(self, ctx, channel) -> discord.Message: page = await self.source.get_page(0) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def show_checked_page(self, page_number: int) -> None: max_pages = self.source.get_max_pages() try: if max_pages is None or max_pages > page_number >= 0: await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) except IndexError: pass def _skip_triangle_buttons(self) -> bool: max_pages = self.source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self) -> bool: max_pages = self.source.get_max_pages() if max_pages is None: return True return max_pages <= 5 @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): await self.show_page(self.source.get_max_pages() - 1) @menus.button( "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_triangle_buttons, ) async def go_to_previous_page(self, payload): await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_triangle_buttons, ) async def go_to_next_page(self, payload): await self.show_checked_page(self.current_page + 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload): self.stop() with contextlib.suppress(discord.Forbidden): await self.message.delete()
class ResultMenu(menus.MenuPages, inherit_buttons=False): def __init__(self, **kwargs): super().__init__( **kwargs, timeout=60, clear_reactions_after=True, delete_message_after=True, ) def _skip_double_triangle_buttons(self): return super()._skip_double_triangle_buttons() async def finalize(self, timed_out): """|coro| A coroutine that is called when the menu loop has completed its run. This is useful if some asynchronous clean-up is required after the fact. Parameters -------------- timed_out: :class:`bool` Whether the menu completed due to timing out. """ if timed_out and self.delete_message_after: self.delete_message_after = False @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button("\N{BLACK LEFT-POINTING TRIANGLE}\ufe0f", position=menus.First(1)) async def go_to_previous_page(self, payload): """go to the previous page""" if self.current_page == 0: await self.show_page(self._source.get_max_pages() - 1) else: await self.show_checked_page(self.current_page - 1) @menus.button("\N{BLACK RIGHT-POINTING TRIANGLE}\ufe0f", position=menus.Last(0)) async def go_to_next_page(self, payload): """go to the next page""" if self.current_page == self._source.get_max_pages() - 1: await self.show_page(0) else: await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}", position=menus.First(2)) async def stop_pages(self, payload) -> None: self.stop()
class BaseMenu(menus.MenuPages, inherit_buttons=False): def __init__( self, source: menus.PageSource, cog: Optional[commands.Cog] = None, page_start: Optional[int] = 0, clear_reactions_after: bool = True, delete_message_after: bool = False, timeout: int = 60, message: discord.Message = None, **kwargs: Any, ) -> None: super().__init__( source, clear_reactions_after=clear_reactions_after, delete_message_after=delete_message_after, timeout=timeout, message=message, **kwargs, ) self.cog = cog self.page_start = page_start async def send_initial_message(self, ctx, channel): """|coro| The default implementation of :meth:`Menu.send_initial_message` for the interactive pagination session. This implementation shows the first page of the source. """ self.current_page = self.page_start page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def update(self, payload): """|coro| Updates the menu after an event has been received. Parameters ----------- payload: :class:`discord.RawReactionActionEvent` The reaction event that triggered this update. """ button = self.buttons[payload.emoji] if not self._running: return try: if button.lock: async with self._lock: if self._running: await button(self, payload) else: await button(self, payload) except Exception as exc: log.debug("Ignored exception on reaction event", exc_info=exc) async def show_checked_page(self, page_number: int) -> None: max_pages = self._source.get_max_pages() try: if max_pages is None: # If it doesn't give maximum pages, it cannot be checked await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) elif page_number < 0: await self.show_page(max_pages - 1) elif max_pages > page_number >= 0: await self.show_page(page_number) except IndexError: # An error happened that can be handled, so ignore it. pass def reaction_check(self, payload): """Just extends the default reaction_check to use owner_ids""" if payload.message_id != self.message.id: return False if payload.user_id not in (*self.bot.owner_ids, self._author_id): return False return payload.emoji in self.buttons def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 2 @menus.button( "\N{BLACK LEFT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): """go to the previous page""" await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING TRIANGLE}\N{VARIATION SELECTOR-16}", position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): """go to the next page""" await self.show_checked_page(self.current_page + 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): """go to the first page""" await self.show_page(0) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): """go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(self._source.get_max_pages() - 1) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload: discord.RawReactionActionEvent) -> None: """stops the pagination session.""" self.stop() await self.message.delete()
class Menu(menus.MenuPages, inherit_buttons=False): """A menus class for discord If you would like to change the buttons subclass this! """ async def send_initial_message(self, ctx: commands.Context, channel: discord.TextChannel): self.current_page = self.page_start page = await self._source.get_page(self.page_start) kwargs = await self._get_kwargs_from_page(page) return await channel.send(**kwargs) async def show_checked_page(self, page_number: int): max_pages = self._source.get_max_pages() try: if max_pages is None or max_pages > page_number >= 0: await self.show_page(page_number) elif page_number >= max_pages: await self.show_page(0) else: await self.show_page(max_pages - 1) except IndexError: pass def _skip_double_triangle_buttons(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages <= 4 def _skip_single_arrows(self): max_pages = self._source.get_max_pages() if max_pages is None: return True return max_pages == 1 @menus.button( "\N{BLACK RIGHTWARDS ARROW}", position=menus.Last(0), skip_if=_skip_single_arrows, ) async def go_to_next_page(self, payload): await self.show_checked_page(self.current_page + 1) @menus.button( "\N{LEFTWARDS BLACK ARROW}", position=menus.First(1), skip_if=_skip_single_arrows, ) async def go_to_previous_page(self, payload): await self.show_checked_page(self.current_page - 1) @menus.button( "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}", position=menus.Last(1), skip_if=_skip_double_triangle_buttons, ) async def go_to_last_page(self, payload): await self.show_checked_page(self._source.get_max_pages - 1) @menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}", position=menus.First(0), skip_if=_skip_double_triangle_buttons, ) async def go_to_first_page(self, payload): await self.show_checked_page(0) @menus.button("\N{CROSS MARK}") async def stop_pages(self, payload): self.stop() await self.message.delete()