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 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()
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 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 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 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()
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 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 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 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 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 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()
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) @_dpy_menus.button( "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\ufe0f", position=_dpy_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)
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 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 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 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 Connect4Menu(menus.Menu): CANCEL_GAME_EMOJI = "🚫" DIGITS = [ str(digit) + "\N{combining enclosing keycap}" for digit in range(1, 8) ] GAME_TIMEOUT_THRESHOLD = 2 * 60 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))) def reaction_check(self, payload: discord.RawReactionActionEvent): if payload.message_id != self.message.id: return False if payload.user_id != self.game.current_player.id: return False return payload.emoji in self.buttons async def send_initial_message( self, ctx: commands.Context, channel: discord.TextChannel) -> discord.Message: return await channel.send(self.game) def get_emoji_from_payload(self, payload: discord.RawReactionActionEvent) -> str: return str(payload.emoji) async def handle_digit_press(self, payload: discord.RawReactionActionEvent): try: # convert the reaction to a 0-indexed int and move in that column self.game.move( self.DIGITS.index(self.get_emoji_from_payload(payload))) except ValueError: pass # the column may be full await self.edit(payload, content=self.game, refresh_components=True) if self.game.whomst_won() != self.game.NO_WINNER: await self.end() @menus.button(CANCEL_GAME_EMOJI, position=menus.Last(0)) async def close_menu(self, payload: discord.RawReactionActionEvent): self.game.forfeit() await self.end() async def end(self): self.stop() async def edit(self, payload: discord.RawReactionActionEvent, *, respond: bool = True, **kwargs): refresh_components = kwargs.pop("refresh_components", False) try: await self.message.edit(**kwargs) except discord.NotFound: await self.cancel( "Connect4 game cancelled since the message was deleted." if respond else None) except discord.Forbidden: await self.cancel(None) async def cancel(self, message: str = "Connect4 game cancelled."): if message: await self.ctx.send(message) self.stop() async def finalize(self, timed_out: bool): if timed_out: await self.ctx.send(content="Connect4 game timed out.") gameboard = str(self.game) if self.message.content != gameboard: await self.edit(None, content=gameboard, respond=False, components=[]) await self.store_stats() @staticmethod def add_stat(stats: dict, key: str, user_id: str): if user_id in stats[key]: stats[key][user_id] += 1 else: stats[key][user_id] = 1 async def store_stats(self): winnernum = self.game.whomst_won() if winnernum in (Connect4Game.FORFEIT, Connect4Game.NO_WINNER): return player1_id = str(self.game.player1.id) player2_id = str(self.game.player2.id) async with self.cog.config.guild(self.message.guild).stats() as stats: stats["played"] += 1 if winnernum == Connect4Game.TIE: stats["ties"] += 1 self.add_stat(stats, "draw", player1_id) self.add_stat(stats, "draw", player2_id) else: winner, loser = ((player1_id, player2_id) if winnernum == 1 else (player2_id, player1_id)) self.add_stat(stats, "wins", winner) self.add_stat(stats, "losses", loser)
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()