From 547f129d3c98bae5d569c0a30569a69233717284 Mon Sep 17 00:00:00 2001 From: teilorr Date: Thu, 29 Aug 2024 23:59:20 -0300 Subject: [PATCH 1/2] fix(tux/cogs/info/info.py): fixes embed lenght bug for roles and emotes --- tux/cogs/info/info.py | 87 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 04796c30a..31995eb80 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -1,5 +1,8 @@ +from collections.abc import Generator, Iterable, Iterator + import discord from discord.ext import commands +from reactionmenu import ViewButton, ViewMenu class Info(commands.Cog): @@ -127,13 +130,26 @@ async def roles(self, ctx: commands.Context[commands.Bot]) -> None: embed = discord.Embed( title="Server Roles", - description=f"Role list for {guild.name}", color=discord.Color.blurple(), ) - embed.add_field(name="Roles", value=", ".join(roles), inline=False) + chunk_size = 32 + if not len(roles) > chunk_size: + embed.description = self._create_embed_desc("roles", guild.name, roles) + await ctx.send(embed=embed) + return - await ctx.send(embed=embed) + chunks = self._chunks(iter(roles), chunk_size) + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) + + for chunk in chunks: + embed = embed.copy() + + embed.description = self._create_embed_desc("roles", guild.name, chunk) + menu.add_page(embed) + + menu = self._add_buttons_to_menu(menu) + await menu.start() @info.command( name="emotes", @@ -153,17 +169,74 @@ async def emotes(self, ctx: commands.Context[commands.Bot]) -> None: return guild = ctx.guild - emotes = [str(emote) for emote in guild.emojis] + emotes: list[str] = [str(emote) for emote in guild.emojis] embed = discord.Embed( title="Server Emotes", - description=f"Emote list for {guild.name}", color=discord.Color.blurple(), ) - embed.add_field(name="Emotes", value=" ".join(emotes) if emotes else "No emotes available", inline=False) + chunk_size = 128 + if not len(emotes) > chunk_size: + embed.description = self._create_embed_desc("emotes", guild.name, emotes) + await ctx.send(embed=embed) + return - await ctx.send(embed=embed) + chunks = self._chunks(iter(emotes), chunk_size) + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) + + for chunk in chunks: + embed = embed.copy() + + embed.description = self._create_embed_desc("emotes", guild.name, chunk) + menu.add_page(embed) + + menu = self._add_buttons_to_menu(menu) + await menu.start() + + def _chunks[T](self, it: Iterator[T], size: int) -> Generator[list[T], None, None]: + """ + Split an iterator into chunks of a specified size. + + This function takes an iterator and divides it into chunks of a given size. + Any remaining elements that do not fill a complete chunk are included in the + final chunk, which may be smaller than the specified size. + + Parameters + ---------- + it : Iterator[T] + The input iterator to be split into chunks. + size : int + The size of each chunk. + + Yields + ------- + list[T] + A list containing a chunk of elements from the input iterator. The last + list may contain fewer elements if there are not enough remaining to fill + a complete chunk. + """ + chunk: list[T] = [] + for item in it: + chunk.append(item) + if len(chunk) == size: + yield chunk + chunk = [] + if chunk: # if last chunk has any items + yield chunk + + def _create_embed_desc(self, list_type: str, guild_name: str, items: Iterable[str]) -> str: + return ( + f"{list_type.capitalize()} list for {guild_name}:\n {" ".join(items) if items else "No items available."}" + ) + + def _add_buttons_to_menu[T: ViewMenu](self, menu: T) -> T: + menu.add_button(ViewButton.go_to_first_page()) + menu.add_button(ViewButton.back()) + menu.add_button(ViewButton.next()) + menu.add_button(ViewButton.go_to_last_page()) + menu.add_button(ViewButton.end_session()) + return menu async def setup(bot: commands.Bot) -> None: From ab014556749cdcb3f47550307b156eda26dc90b2 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 31 Aug 2024 18:04:36 +0000 Subject: [PATCH 2/2] refactor(info.py): remove unnecessary line of code to improve readability and maintainability refactor(info.py): simplify command decorators for readability refactor(info.py): change ctx type from commands.Context[commands.Bot] to commands.Context for simplicity feat(info.py): add paginated_embed method to handle paginated embeds in a more reusable way fix(info.py): check if guild exists before proceeding with commands to prevent errors feat(info.py): add more detailed server and member information to embeds refactor(info.py): streamline embed creation for cleaner code refactor(info.py): remove unused _create_embed_desc method refactor(info.py): update _chunks method to return list of strings for consistency refactor(info.py): update _add_buttons_to_menu method to return ViewMenu for consistency refactor: replace individual button addition with loop to improve code readability and maintainability --- tux/cogs/info/info.py | 275 +++++++++++++++++++++--------------------- 1 file changed, 137 insertions(+), 138 deletions(-) diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 31995eb80..45403ab9d 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -9,233 +9,232 @@ class Info(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - @commands.hybrid_group( - name="info", - aliases=["i"], - usage="info ", - ) + @commands.hybrid_group(name="info", aliases=["i"], usage="info ") async def info(self, ctx: commands.Context[commands.Bot]) -> None: """ Information commands. Parameters ---------- - ctx : commands.Context[commands.Bot] - The discord context object. + ctx : commands.Context + The context object associated with the command. """ + if ctx.invoked_subcommand is None: await ctx.send_help("info") - @info.command( - name="server", - aliases=["s"], - usage="info server", - ) + @info.command(name="server", aliases=["s"], usage="info server") async def server(self, ctx: commands.Context[commands.Bot]) -> None: """ Show information about the server. Parameters ---------- - ctx : commands.Context[commands.Bot] - The discord context object. + ctx : commands.Context + The context object associated with the command. """ - - if not ctx.guild: - return guild = ctx.guild + if not guild: + return - embed = discord.Embed( - title=ctx.guild.name, - description=guild.description or "No description available.", - color=discord.Color.blurple(), + embed: discord.Embed = ( + discord.Embed( + title=guild.name, + description=guild.description or "No description available.", + color=discord.Color.blurple(), + ) + .set_author(name="Server Information", icon_url=guild.icon) + .add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown") + .add_field(name="Vanity URL", value=guild.vanity_url_code or "None") + .add_field(name="Boosts", value=guild.premium_subscription_count) + .add_field(name="Text Channels", value=len(guild.text_channels)) + .add_field(name="Voice Channels", value=len(guild.voice_channels)) + .add_field(name="Forum Channels", value=len(guild.forums)) + .add_field(name="Emojis", value=f"{len(guild.emojis)}/{guild.emoji_limit}") + .add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}") + .add_field(name="Roles", value=len(guild.roles)) + .add_field(name="Humans", value=sum(not member.bot for member in guild.members)) + .add_field(name="Bots", value=sum(member.bot for member in guild.members)) + .add_field(name="Bans", value=len([entry async for entry in guild.bans(limit=2000)])) + .set_footer(text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}") ) - embed.set_author(name="Server Information", icon_url=guild.icon) - embed.add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown") - embed.add_field(name="Vanity URL", value=guild.vanity_url_code or "None") - embed.add_field(name="Boosts", value=guild.premium_subscription_count) - embed.add_field(name="Text Channels", value=len(guild.text_channels)) - embed.add_field(name="Voice Channels", value=len(guild.voice_channels)) - embed.add_field(name="Forum Channels", value=len(guild.forums)) - embed.add_field(name="Emojis", value=f"{len(guild.emojis)}/{guild.emoji_limit}") - embed.add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}") - embed.add_field(name="Roles", value=len(guild.roles)) - embed.add_field(name="Humans", value=sum(not member.bot for member in guild.members)) - embed.add_field(name="Bots", value=sum(member.bot for member in guild.members)) - embed.add_field(name="Bans", value=len([entry async for entry in guild.bans(limit=2000)])) - embed.set_footer(text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}") - await ctx.send(embed=embed) - @info.command( - name="member", - aliases=["m", "user", "u"], - usage="info member [member]", - ) + @info.command(name="member", aliases=["m", "user", "u"], usage="info member [member]") async def member(self, ctx: commands.Context[commands.Bot], member: discord.Member) -> None: """ Show information about a member. Parameters ---------- - ctx : commands.Context[commands.Bot] - The discord context object. + ctx : commands.Context + The context object associated with the command. member : discord.Member The member to get information about. """ - - bot_status = "✅" if member.bot else "❌" - joined = discord.utils.format_dt(member.joined_at, "R") if member.joined_at else "Unknown" - created = discord.utils.format_dt(member.created_at, "R") if member.created_at else "Unknown" - roles = ", ".join(role.mention for role in member.roles[1:]) if member.roles[1:] else "No roles" - fetched_member = await self.bot.fetch_user(member.id) - - embed = discord.Embed( - title=member.display_name, - description="Here is some information about the member.", - color=discord.Color.blurple(), + embed: discord.Embed = ( + discord.Embed( + title=member.display_name, + description="Here is some information about the member.", + color=discord.Color.blurple(), + ) + .set_thumbnail(url=member.display_avatar.url) + .set_image( + url=(await self.bot.fetch_user(member.id)).banner, # Fetched member's banner + ) + .add_field(name="Bot?", value="✅" if member.bot else "❌", inline=False) + .add_field(name="Username", value=member.name, inline=False) + .add_field(name="ID", value=str(member.id), inline=False) + .add_field( + name="Joined", + value=discord.utils.format_dt(member.joined_at, "R") if member.joined_at else "Unknown", + inline=False, + ) + .add_field( + name="Registered", + value=discord.utils.format_dt(member.created_at, "R") if member.created_at else "Unknown", + inline=False, + ) + .add_field( + name="Roles", + value=", ".join(role.mention for role in member.roles[1:]) if member.roles[1:] else "No roles", + inline=False, + ) ) - embed.set_thumbnail(url=member.display_avatar.url) - embed.set_image(url=fetched_member.banner) - embed.add_field(name="Bot?", value=bot_status, inline=False) - embed.add_field(name="Username", value=member.name, inline=False) - embed.add_field(name="ID", value=str(member.id), inline=False) - embed.add_field(name="Joined", value=joined, inline=False) - embed.add_field(name="Registered", value=created, inline=False) - embed.add_field(name="Roles", value=roles, inline=False) - await ctx.send(embed=embed) - @info.command( - name="roles", - aliases=["r"], - usage="info roles", - ) + @info.command(name="roles", aliases=["r"], usage="info roles") async def roles(self, ctx: commands.Context[commands.Bot]) -> None: """ List all roles in the server. Parameters ---------- - ctx : commands.Context[commands.Bot] - The discord context object. + ctx : commands.Context + The context object associated with the command. """ - if not ctx.guild: - return - guild = ctx.guild - roles = [role.mention for role in guild.roles] - - embed = discord.Embed( - title="Server Roles", - color=discord.Color.blurple(), - ) - - chunk_size = 32 - if not len(roles) > chunk_size: - embed.description = self._create_embed_desc("roles", guild.name, roles) - await ctx.send(embed=embed) + if not guild: return - chunks = self._chunks(iter(roles), chunk_size) - menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) + roles: list[str] = [role.mention for role in guild.roles] - for chunk in chunks: - embed = embed.copy() - - embed.description = self._create_embed_desc("roles", guild.name, chunk) - menu.add_page(embed) - - menu = self._add_buttons_to_menu(menu) - await menu.start() + await self.paginated_embed(ctx, "Server Roles", "roles", guild.name, roles, 32) - @info.command( - name="emotes", - aliases=["e"], - usage="info emotes", - ) + @info.command(name="emotes", aliases=["e"], usage="info emotes") async def emotes(self, ctx: commands.Context[commands.Bot]) -> None: """ List all emotes in the server. Parameters ---------- - ctx : commands.Context[commands.Bot] - The discord context object. + ctx : commands.Context + The context object associated with the command. """ - if not ctx.guild: + guild = ctx.guild + if not guild: return - guild = ctx.guild emotes: list[str] = [str(emote) for emote in guild.emojis] + await self.paginated_embed(ctx, "Server Emotes", "emotes", guild.name, emotes, 128) + + async def paginated_embed( + self, + ctx: commands.Context[commands.Bot], + title: str, + list_type: str, + guild_name: str, + items: Iterable[str], + chunk_size: int, + ) -> None: + """ + Send a paginated embed. - embed = discord.Embed( - title="Server Emotes", - color=discord.Color.blurple(), - ) + Parameters + ---------- + ctx : commands.Context + The context object associated with the command. + title : str + The title of the embed. + list_type : str + The type of list (e.g., roles, emotes). + guild_name : str + The name of the guild. + items : Iterable[str] + The items to display in the embed. + chunk_size : int + The size of each chunk for pagination. + """ + embed: discord.Embed = discord.Embed(title=title, color=discord.Color.blurple()) + chunks: list[list[str]] = list(self._chunks(iter(items), chunk_size)) - chunk_size = 128 - if not len(emotes) > chunk_size: - embed.description = self._create_embed_desc("emotes", guild.name, emotes) + if not chunks: + embed.description = "No items available." await ctx.send(embed=embed) return - chunks = self._chunks(iter(emotes), chunk_size) - menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) - + menu: ViewMenu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) for chunk in chunks: - embed = embed.copy() - - embed.description = self._create_embed_desc("emotes", guild.name, chunk) - menu.add_page(embed) + page_embed: discord.Embed = embed.copy() + page_embed.description = f"{list_type.capitalize()} list for {guild_name}:\n{' '.join(chunk)}" + menu.add_page(page_embed) - menu = self._add_buttons_to_menu(menu) + self._add_buttons_to_menu(menu) await menu.start() - def _chunks[T](self, it: Iterator[T], size: int) -> Generator[list[T], None, None]: + def _chunks(self, it: Iterator[str], size: int) -> Generator[list[str], None, None]: """ Split an iterator into chunks of a specified size. - This function takes an iterator and divides it into chunks of a given size. - Any remaining elements that do not fill a complete chunk are included in the - final chunk, which may be smaller than the specified size. - Parameters ---------- - it : Iterator[T] + it : Iterator[str] The input iterator to be split into chunks. size : int The size of each chunk. Yields - ------- - list[T] + ------ + List[str] A list containing a chunk of elements from the input iterator. The last list may contain fewer elements if there are not enough remaining to fill a complete chunk. """ - chunk: list[T] = [] + chunk: list[str] = [] for item in it: chunk.append(item) if len(chunk) == size: yield chunk chunk = [] - if chunk: # if last chunk has any items + if chunk: yield chunk - def _create_embed_desc(self, list_type: str, guild_name: str, items: Iterable[str]) -> str: - return ( - f"{list_type.capitalize()} list for {guild_name}:\n {" ".join(items) if items else "No items available."}" - ) + def _add_buttons_to_menu(self, menu: ViewMenu) -> ViewMenu: + """ + Add buttons to the menu. - def _add_buttons_to_menu[T: ViewMenu](self, menu: T) -> T: - menu.add_button(ViewButton.go_to_first_page()) - menu.add_button(ViewButton.back()) - menu.add_button(ViewButton.next()) - menu.add_button(ViewButton.go_to_last_page()) - menu.add_button(ViewButton.end_session()) + Parameters + ---------- + menu : ViewMenu + The menu to add buttons to. + + Returns + ------- + ViewMenu + The menu with buttons added. + """ + buttons = [ + ViewButton.go_to_first_page(), + ViewButton.back(), + ViewButton.next(), + ViewButton.go_to_last_page(), + ViewButton.end_session(), + ] + + for button in buttons: + menu.add_button(button) return menu