From e34271984971e114a0717436321b4c480a0be61e Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:28:03 +0100 Subject: [PATCH 01/25] Update emojilink.py --- emojilink/emojilink.py | 198 +++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 117 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index e8e53a7..0b3bf99 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -4,8 +4,12 @@ import random import typing import aiohttp +from PIL import Image +import io class EmojiLink(commands.Cog): + """Emoji management commands with automatic background removal.""" + def __init__(self, bot: Red): self.bot = bot @@ -16,63 +20,46 @@ async def emojilink(self, ctx: commands.Context): @emojilink.command(name="getlink") async def get_emoji_link(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): - """ - Get the link for a Discord emoji. - - Parameters: - - emoji: The Discord emoji (custom emoji or Unicode emoji). - """ - # Determine if the provided emoji is a custom emoji or a Unicode emoji + """Get the link for a Discord emoji.""" if isinstance(emoji, discord.PartialEmoji): emoji_str = str(emoji) - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{emoji.animated and 'gif' or 'png'}" + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" elif isinstance(emoji, str): emoji_str = emoji - # Generate a link using Emojipedia (replace '+' with the Unicode emoji) emoji_url = f"https://emojipedia.org/{'+'.join(emoji.encode('unicode-escape').decode('utf-8').split())}/" else: raise commands.BadArgument("Invalid emoji provided.") - - # Send the emoji and the emoji link await ctx.send(f"Emoji: {emoji_str}") await ctx.send(f"Emoji link: {emoji_url}") @emojilink.command(name="list", aliases=["all"]) async def list_emojis(self, ctx: commands.Context): - """List all custom emojis in the server along with their names and links.""" + """List all custom emojis in the server with names and links.""" if not ctx.guild.emojis: await ctx.send("No custom emojis found in this server.") return - # Create embed pages with 3 emojis per page emojis = ctx.guild.emojis pages = [] for i in range(0, len(emojis), 3): embed = discord.Embed(title="Server Emojis", color=discord.Color.blue()) chunk = emojis[i:i + 3] - - # Add each emoji as a large image for emoji in chunk: - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{emoji.animated and 'gif' or 'png'}" - # Create a clickable image with name and download text underneath + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" embed.add_field( name=f":{emoji.name}:", value=f"[⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀]({emoji_url})\n{emoji} • [Download]({emoji_url})", inline=True ) - - # Add empty fields to maintain grid layout if needed remaining = 3 - len(chunk) for _ in range(remaining): embed.add_field(name="⠀", value="⠀", inline=True) - embed.set_footer(text=f"Page {i//3 + 1}/{-(-len(emojis)//3)} • Total emojis: {len(emojis)}") pages.append(embed) if not pages: return await ctx.send("No emojis to display.") - # Create custom view with buttons class PaginationView(discord.ui.View): def __init__(self): super().__init__(timeout=60) @@ -82,24 +69,18 @@ def __init__(self): async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user != ctx.author: return await interaction.response.send_message("You cannot use these buttons.", ephemeral=True) - self.current_page -= 1 - # Update button states button.disabled = self.current_page == 0 self.children[1].disabled = self.current_page == len(pages) - 1 - await interaction.response.edit_message(embed=pages[self.current_page], view=self) @discord.ui.button(label="Next ▶️", style=discord.ButtonStyle.secondary) async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user != ctx.author: return await interaction.response.send_message("You cannot use these buttons.", ephemeral=True) - self.current_page += 1 - # Update button states self.children[0].disabled = self.current_page == 0 button.disabled = self.current_page == len(pages) - 1 - await interaction.response.edit_message(embed=pages[self.current_page], view=self) async def on_timeout(self): @@ -110,35 +91,28 @@ async def on_timeout(self): except: pass - # Send the initial message with the view view = PaginationView() view.message = await ctx.send(embed=pages[0], view=view) @emojilink.command(name="info") async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): - """ - Get information about a specific custom emoji, including its name, ID, and creation date. - - Parameters: - - emoji: The Discord emoji (custom emoji or Unicode emoji). - """ - # Determine if the provided emoji is a custom emoji or a Unicode emoji + """Get information about a specific emoji.""" if isinstance(emoji, discord.PartialEmoji): emoji_str = str(emoji) - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{emoji.animated and 'gif' or 'png'}" + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" emoji_name = emoji.name emoji_id = emoji.id emoji_created_at = emoji.created_at elif isinstance(emoji, str): emoji_str = emoji - emoji_url = None # Unicode emojis don't have a direct image link - emoji_name = None # Unicode emojis don't have a name - emoji_id = None # Unicode emojis don't have an ID - emoji_created_at = None # Unicode emojis don't have a creation date + emoji_url = None + emoji_name = None + emoji_id = None + emoji_created_at = None else: raise commands.BadArgument("Invalid emoji provided.") - if emoji_name is not None: + if emoji_name: await ctx.send(f"Emoji: {emoji_str}\nName: {emoji_name}\nID: {emoji_id}\nCreation Date: {emoji_created_at}") else: await ctx.send(f"Emoji: {emoji_str}") @@ -148,14 +122,11 @@ async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.Pa @emojilink.command(name="random") async def random_emoji(self, ctx: commands.Context): - """ - Get a link for a random custom emoji in the server. - """ + """Get a random custom emoji.""" emojis = ctx.guild.emojis if emojis: random_emoji = random.choice(emojis) - emoji_url = f"https://cdn.discordapp.com/emojis/{random_emoji.id}.{random_emoji.animated and 'gif' or 'png'}" - # Send the emoji and the emoji link + emoji_url = f"https://cdn.discordapp.com/emojis/{random_emoji.id}.{ 'gif' if random_emoji.animated else 'png'}" await ctx.send(f"Random Emoji: {random_emoji}") await ctx.send(f"Emoji link: {emoji_url}") else: @@ -163,73 +134,73 @@ async def random_emoji(self, ctx: commands.Context): @emojilink.command(name="search") async def emoji_search(self, ctx: commands.Context, keyword: str): - """ - Search for custom emojis based on their names or keywords. - - Parameters: - - keyword: The search keyword. - """ + """Search custom emojis by name.""" matching_emojis = [ - f"{emoji}: [Link]({emoji_url})" - for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) - if keyword.lower() in emoji.name.lower() # Compare against emoji name + f"{emoji}: [Link]({emoji_url})" + for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) + if keyword.lower() in emoji.name.lower() if hasattr(emoji, "name") else False ] if matching_emojis: await ctx.send("\n".join(matching_emojis)) else: - await ctx.send(f"No custom emojis found matching the keyword '{keyword}'.") + await ctx.send(f"No custom emojis found matching '{keyword}'.") def get_all_emojis(self, emojis): - """ - Helper function to extract all emojis and their URLs from a list of emojis. - """ all_emojis = [] for emoji in emojis: if isinstance(emoji, discord.PartialEmoji): - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{emoji.animated and 'gif' or 'png'}" - all_emojis.append((str(emoji), emoji_url)) + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" + all_emojis.append((emoji, emoji_url)) elif isinstance(emoji, str): - # Unicode emojis don't have a direct image link all_emojis.append((emoji, None)) return all_emojis @emojilink.command(name="add", aliases=["create"]) @commands.has_permissions(manage_emojis=True) async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union[discord.PartialEmoji, str] = None): - """Add a custom emoji to the server from a URL, attachment, or existing emoji.""" - # Validate emoji name - if not name.isalnum() and not '_' in name: + """Add a custom emoji with automatic background removal.""" + if not name.isalnum() and "_" not in name: return await ctx.send("Emoji name must contain only letters, numbers, and underscores.") - if len(name) < 2 or len(name) > 32: return await ctx.send("Emoji name must be between 2 and 32 characters long.") - - # Check for attachment if no source is provided if not source and not ctx.message.attachments: - return await ctx.send("Please provide either an existing emoji, URL, or attach an image file.") - + return await ctx.send("Provide an existing emoji, URL, or attach an image file.") + try: async with ctx.typing(): async with aiohttp.ClientSession() as session: - # Handle different source types if isinstance(source, discord.PartialEmoji): - # Copy existing emoji - url = f"https://cdn.discordapp.com/emojis/{source.id}.{source.animated and 'gif' or 'png'}" + url = f"https://cdn.discordapp.com/emojis/{source.id}.{ 'gif' if source.animated else 'png'}" elif isinstance(source, str) and source.startswith(('http://', 'https://')): - # Direct URL url = source elif ctx.message.attachments: - # Attachment url = ctx.message.attachments[0].url else: - return await ctx.send("Invalid source. Please provide an existing emoji, valid URL, or attach an image file.") - + return await ctx.send("Invalid source.") + async with session.get(url) as response: if response.status != 200: return await ctx.send("Failed to fetch image.") image_data = await response.read() - await ctx.guild.create_custom_emoji(name=name, image=image_data) - await ctx.send(f"Emoji '{name}' added successfully.") + + # === Background removal === + image = Image.open(io.BytesIO(image_data)).convert("RGBA") + datas = image.getdata() + new_data = [] + for item in datas: + if item[0] > 240 and item[1] > 240 and item[2] > 240: + new_data.append((255, 255, 255, 0)) + else: + new_data.append(item) + image.putdata(new_data) + buffered = io.BytesIO() + image.save(buffered, format="PNG") + buffered.seek(0) + image_data = buffered.read() + # === End background removal === + + await ctx.guild.create_custom_emoji(name=name, image=image_data) + await ctx.send(f"Emoji '{name}' added successfully.") except discord.HTTPException as e: await ctx.send(f"Failed to add emoji: {e.text}") except discord.Forbidden: @@ -240,29 +211,39 @@ async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union @emojilink.command(name="copy", require_var_positional=True) @commands.has_permissions(manage_emojis=True) async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): - """ - Copy a custom emoji from one server to another. - - Parameters: - - emoji: The custom emoji to copy. - """ - # Check if the user has permission to manage emojis + """Copy a custom emoji with automatic background removal.""" if not ctx.author.guild_permissions.manage_emojis: return await ctx.send("You do not have permission to manage emojis.") - if not ctx.guild.me.guild_permissions.manage_emojis: return await ctx.send("I do not have permissions to manage emojis in this server.") try: async with ctx.typing(): - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{emoji.animated and 'gif' or 'png'}" + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" async with aiohttp.ClientSession() as session: async with session.get(emoji_url) as response: if response.status != 200: return await ctx.send("Failed to fetch emoji image.") image_data = await response.read() - await ctx.guild.create_custom_emoji(name=emoji.name, image=image_data) - await ctx.send(f"Emoji '{emoji.name}' copied successfully.") + + # === Background removal === + image = Image.open(io.BytesIO(image_data)).convert("RGBA") + datas = image.getdata() + new_data = [] + for item in datas: + if item[0] > 240 and item[1] > 240 and item[2] > 240: + new_data.append((255, 255, 255, 0)) + else: + new_data.append(item) + image.putdata(new_data) + buffered = io.BytesIO() + image.save(buffered, format="PNG") + buffered.seek(0) + image_data = buffered.read() + # === End background removal === + + await ctx.guild.create_custom_emoji(name=emoji.name, image=image_data) + await ctx.send(f"Emoji '{emoji.name}' copied successfully.") except discord.HTTPException as e: await ctx.send(f"Failed to copy emoji: {e.text}") except discord.Forbidden: @@ -273,51 +254,37 @@ async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): @emojilink.command(name="delete", require_var_positional=True) @commands.has_permissions(manage_emojis=True) async def delete_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): - """ - Delete a custom emoji from the server. - - Parameters: - - emoji: The custom emoji to delete. - """ + """Delete a custom emoji from the server.""" try: - # Find the emoji in the guild's emoji list guild_emoji = discord.utils.get(ctx.guild.emojis, id=emoji.id) if guild_emoji is None: return await ctx.send("This emoji doesn't exist in this server.") - - # Create confirmation view + view = discord.ui.View(timeout=30) confirm_button = discord.ui.Button(label="Confirm", style=discord.ButtonStyle.danger) cancel_button = discord.ui.Button(label="Cancel", style=discord.ButtonStyle.secondary) - + async def confirm_callback(interaction: discord.Interaction): if interaction.user != ctx.author: return await interaction.response.send_message("You cannot use this button.", ephemeral=True) - try: await guild_emoji.delete() await interaction.response.edit_message(content=f"Emoji '{emoji.name}' deleted successfully.", view=None) except Exception as e: await interaction.response.edit_message(content=f"Failed to delete emoji: {e}", view=None) - + async def cancel_callback(interaction: discord.Interaction): if interaction.user != ctx.author: return await interaction.response.send_message("You cannot use this button.", ephemeral=True) - await interaction.response.edit_message(content="Emoji deletion cancelled.", view=None) - + confirm_button.callback = confirm_callback cancel_button.callback = cancel_callback - + view.add_item(confirm_button) view.add_item(cancel_button) - - # Send confirmation message - await ctx.send( - f"Are you sure you want to delete the emoji {emoji}?", - view=view - ) - + + await ctx.send(f"Are you sure you want to delete the emoji {emoji}?", view=view) except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") @@ -325,14 +292,11 @@ async def cancel_callback(interaction: discord.Interaction): @commands.has_permissions(manage_emojis=True) async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, new_name: str): """Rename a custom emoji in the server.""" - # Validate new name - if not new_name.isalnum() and not '_' in new_name: + if not new_name.isalnum() and "_" not in new_name: return await ctx.send("Emoji name must contain only letters, numbers, and underscores.") - if len(new_name) < 2 or len(new_name) > 32: return await ctx.send("Emoji name must be between 2 and 32 characters long.") - # Find the emoji in the guild's emoji list guild_emoji = discord.utils.get(ctx.guild.emojis, id=emoji.id) if guild_emoji is None: return await ctx.send("This emoji doesn't exist in this server.") @@ -345,4 +309,4 @@ async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, except discord.Forbidden: await ctx.send("I do not have permissions to rename emojis.") except Exception as e: - await ctx.send(f"An unexpected error occurred: {e}") \ No newline at end of file + await ctx.send(f"An unexpected error occurred: {e}") From a201e9222113101f1b98caba570c227f6b815377 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:29:41 +0100 Subject: [PATCH 02/25] Refactor emoji search command for clarity --- emojilink/emojilink.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index 0b3bf99..77d5aa7 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -132,18 +132,19 @@ async def random_emoji(self, ctx: commands.Context): else: await ctx.send("No custom emojis found in this server.") - @emojilink.command(name="search") - async def emoji_search(self, ctx: commands.Context, keyword: str): - """Search custom emojis by name.""" - matching_emojis = [ - f"{emoji}: [Link]({emoji_url})" - for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) - if keyword.lower() in emoji.name.lower() if hasattr(emoji, "name") else False - ] - if matching_emojis: - await ctx.send("\n".join(matching_emojis)) - else: - await ctx.send(f"No custom emojis found matching '{keyword}'.") +@emojilink.command(name="search") +async def emoji_search(self, ctx: commands.Context, keyword: str): + """Search custom emojis by name.""" + matching_emojis = [ + f"{emoji}: [Link]({emoji_url})" + for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) + if hasattr(emoji, "name") and keyword.lower() in emoji.name.lower() + ] + if matching_emojis: + await ctx.send("\n".join(matching_emojis)) + else: + await ctx.send(f"No custom emojis found matching '{keyword}'.") + def get_all_emojis(self, emojis): all_emojis = [] From f0c393d93fff2f51f466afa462d031f80a458d4a Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:31:49 +0100 Subject: [PATCH 03/25] Refactor emoji commands to use commands.command --- emojilink/emojilink.py | 62 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index 77d5aa7..a70c0ca 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -18,7 +18,11 @@ async def emojilink(self, ctx: commands.Context): """Emoji related commands.""" await ctx.send_help(str(ctx.command)) - @emojilink.command(name="getlink") + # ----------------------------- + # Subcommands + # ----------------------------- + + @commands.command(name="getlink") async def get_emoji_link(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): """Get the link for a Discord emoji.""" if isinstance(emoji, discord.PartialEmoji): @@ -31,8 +35,9 @@ async def get_emoji_link(self, ctx: commands.Context, emoji: typing.Union[discor raise commands.BadArgument("Invalid emoji provided.") await ctx.send(f"Emoji: {emoji_str}") await ctx.send(f"Emoji link: {emoji_url}") + emojilink.add_command(get_emoji_link) - @emojilink.command(name="list", aliases=["all"]) + @commands.command(name="list", aliases=["all"]) async def list_emojis(self, ctx: commands.Context): """List all custom emojis in the server with names and links.""" if not ctx.guild.emojis: @@ -93,8 +98,9 @@ async def on_timeout(self): view = PaginationView() view.message = await ctx.send(embed=pages[0], view=view) + emojilink.add_command(list_emojis) - @emojilink.command(name="info") + @commands.command(name="info") async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): """Get information about a specific emoji.""" if isinstance(emoji, discord.PartialEmoji): @@ -119,8 +125,9 @@ async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.Pa if emoji_url: await ctx.send(f"Emoji link: {emoji_url}") + emojilink.add_command(emoji_info) - @emojilink.command(name="random") + @commands.command(name="random") async def random_emoji(self, ctx: commands.Context): """Get a random custom emoji.""" emojis = ctx.guild.emojis @@ -131,20 +138,25 @@ async def random_emoji(self, ctx: commands.Context): await ctx.send(f"Emoji link: {emoji_url}") else: await ctx.send("No custom emojis found in this server.") + emojilink.add_command(random_emoji) + + @commands.command(name="search") + async def emoji_search(self, ctx: commands.Context, keyword: str): + """Search custom emojis by name.""" + matching_emojis = [ + f"{emoji}: [Link]({emoji_url})" + for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) + if hasattr(emoji, "name") and keyword.lower() in emoji.name.lower() + ] + if matching_emojis: + await ctx.send("\n".join(matching_emojis)) + else: + await ctx.send(f"No custom emojis found matching '{keyword}'.") + emojilink.add_command(emoji_search) -@emojilink.command(name="search") -async def emoji_search(self, ctx: commands.Context, keyword: str): - """Search custom emojis by name.""" - matching_emojis = [ - f"{emoji}: [Link]({emoji_url})" - for emoji, emoji_url in self.get_all_emojis(ctx.guild.emojis) - if hasattr(emoji, "name") and keyword.lower() in emoji.name.lower() - ] - if matching_emojis: - await ctx.send("\n".join(matching_emojis)) - else: - await ctx.send(f"No custom emojis found matching '{keyword}'.") - + # ----------------------------- + # Helpers + # ----------------------------- def get_all_emojis(self, emojis): all_emojis = [] @@ -156,7 +168,11 @@ def get_all_emojis(self, emojis): all_emojis.append((emoji, None)) return all_emojis - @emojilink.command(name="add", aliases=["create"]) + # ----------------------------- + # Add / Copy / Delete / Rename + # ----------------------------- + + @commands.command(name="add", aliases=["create"]) @commands.has_permissions(manage_emojis=True) async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union[discord.PartialEmoji, str] = None): """Add a custom emoji with automatic background removal.""" @@ -208,8 +224,9 @@ async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union await ctx.send("I do not have permissions to add emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") + emojilink.add_command(add_emoji) - @emojilink.command(name="copy", require_var_positional=True) + @commands.command(name="copy") @commands.has_permissions(manage_emojis=True) async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): """Copy a custom emoji with automatic background removal.""" @@ -251,8 +268,9 @@ async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): await ctx.send("I do not have permissions to add emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") + emojilink.add_command(copy_emoji) - @emojilink.command(name="delete", require_var_positional=True) + @commands.command(name="delete") @commands.has_permissions(manage_emojis=True) async def delete_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): """Delete a custom emoji from the server.""" @@ -288,8 +306,9 @@ async def cancel_callback(interaction: discord.Interaction): await ctx.send(f"Are you sure you want to delete the emoji {emoji}?", view=view) except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") + emojilink.add_command(delete_emoji) - @emojilink.command(name="rename", aliases=["edit"]) + @commands.command(name="rename", aliases=["edit"]) @commands.has_permissions(manage_emojis=True) async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, new_name: str): """Rename a custom emoji in the server.""" @@ -311,3 +330,4 @@ async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, await ctx.send("I do not have permissions to rename emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") + emojilink.add_command(rename_emoji) From 630b27709b437df53d3a66227679c87d1f772d73 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:33:36 +0100 Subject: [PATCH 04/25] Update emojilink.py --- emojilink/emojilink.py | 49 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index a70c0ca..3252e89 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -100,32 +100,31 @@ async def on_timeout(self): view.message = await ctx.send(embed=pages[0], view=view) emojilink.add_command(list_emojis) - @commands.command(name="info") - async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): - """Get information about a specific emoji.""" - if isinstance(emoji, discord.PartialEmoji): - emoji_str = str(emoji) - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" - emoji_name = emoji.name - emoji_id = emoji.id - emoji_created_at = emoji.created_at - elif isinstance(emoji, str): - emoji_str = emoji - emoji_url = None - emoji_name = None - emoji_id = None - emoji_created_at = None - else: - raise commands.BadArgument("Invalid emoji provided.") - - if emoji_name: - await ctx.send(f"Emoji: {emoji_str}\nName: {emoji_name}\nID: {emoji_id}\nCreation Date: {emoji_created_at}") - else: - await ctx.send(f"Emoji: {emoji_str}") +@emojilink.command(name="info") +async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): + """Get information about a specific emoji.""" + if isinstance(emoji, discord.PartialEmoji): + emoji_str = str(emoji) + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" + emoji_name = emoji.name + emoji_id = emoji.id + emoji_created_at = emoji.created_at + elif isinstance(emoji, str): + emoji_str = emoji + emoji_url = None + emoji_name = None + emoji_id = None + emoji_created_at = None + else: + raise commands.BadArgument("Invalid emoji provided.") + + if emoji_name: + await ctx.send(f"Emoji: {emoji_str}\nName: {emoji_name}\nID: {emoji_id}\nCreation Date: {emoji_created_at}") + else: + await ctx.send(f"Emoji: {emoji_str}") - if emoji_url: - await ctx.send(f"Emoji link: {emoji_url}") - emojilink.add_command(emoji_info) + if emoji_url: + await ctx.send(f"Emoji link: {emoji_url}") @commands.command(name="random") async def random_emoji(self, ctx: commands.Context): From 1aa94a1e316a295e9a0822df98261ef34982b141 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:34:41 +0100 Subject: [PATCH 05/25] Update emojilink.py --- emojilink/emojilink.py | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index 3252e89..28f7fcd 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -100,31 +100,31 @@ async def on_timeout(self): view.message = await ctx.send(embed=pages[0], view=view) emojilink.add_command(list_emojis) -@emojilink.command(name="info") -async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): - """Get information about a specific emoji.""" - if isinstance(emoji, discord.PartialEmoji): - emoji_str = str(emoji) - emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" - emoji_name = emoji.name - emoji_id = emoji.id - emoji_created_at = emoji.created_at - elif isinstance(emoji, str): - emoji_str = emoji - emoji_url = None - emoji_name = None - emoji_id = None - emoji_created_at = None - else: - raise commands.BadArgument("Invalid emoji provided.") - - if emoji_name: - await ctx.send(f"Emoji: {emoji_str}\nName: {emoji_name}\nID: {emoji_id}\nCreation Date: {emoji_created_at}") - else: - await ctx.send(f"Emoji: {emoji_str}") + @emojilink.command(name="info") + async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): + """Get information about a specific emoji.""" + if isinstance(emoji, discord.PartialEmoji): + emoji_str = str(emoji) + emoji_url = f"https://cdn.discordapp.com/emojis/{emoji.id}.{ 'gif' if emoji.animated else 'png'}" + emoji_name = emoji.name + emoji_id = emoji.id + emoji_created_at = emoji.created_at + elif isinstance(emoji, str): + emoji_str = emoji + emoji_url = None + emoji_name = None + emoji_id = None + emoji_created_at = None + else: + raise commands.BadArgument("Invalid emoji provided.") - if emoji_url: - await ctx.send(f"Emoji link: {emoji_url}") + if emoji_name: + await ctx.send(f"Emoji: {emoji_str}\nName: {emoji_name}\nID: {emoji_id}\nCreation Date: {emoji_created_at}") + else: + await ctx.send(f"Emoji: {emoji_str}") + + if emoji_url: + await ctx.send(f"Emoji link: {emoji_url}") @commands.command(name="random") async def random_emoji(self, ctx: commands.Context): From 956251f4d088122c72b8e7ff88f95a1a91e0c675 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:36:15 +0100 Subject: [PATCH 06/25] Refactor command registration to use emojilink --- emojilink/emojilink.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/emojilink/emojilink.py b/emojilink/emojilink.py index 28f7fcd..8988a53 100644 --- a/emojilink/emojilink.py +++ b/emojilink/emojilink.py @@ -22,7 +22,7 @@ async def emojilink(self, ctx: commands.Context): # Subcommands # ----------------------------- - @commands.command(name="getlink") + @emojilink.command(name="getlink") async def get_emoji_link(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): """Get the link for a Discord emoji.""" if isinstance(emoji, discord.PartialEmoji): @@ -35,9 +35,8 @@ async def get_emoji_link(self, ctx: commands.Context, emoji: typing.Union[discor raise commands.BadArgument("Invalid emoji provided.") await ctx.send(f"Emoji: {emoji_str}") await ctx.send(f"Emoji link: {emoji_url}") - emojilink.add_command(get_emoji_link) - @commands.command(name="list", aliases=["all"]) + @emojilink.command(name="list", aliases=["all"]) async def list_emojis(self, ctx: commands.Context): """List all custom emojis in the server with names and links.""" if not ctx.guild.emojis: @@ -98,7 +97,6 @@ async def on_timeout(self): view = PaginationView() view.message = await ctx.send(embed=pages[0], view=view) - emojilink.add_command(list_emojis) @emojilink.command(name="info") async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.PartialEmoji, str]): @@ -126,7 +124,7 @@ async def emoji_info(self, ctx: commands.Context, emoji: typing.Union[discord.Pa if emoji_url: await ctx.send(f"Emoji link: {emoji_url}") - @commands.command(name="random") + @emojilink.command(name="random") async def random_emoji(self, ctx: commands.Context): """Get a random custom emoji.""" emojis = ctx.guild.emojis @@ -137,9 +135,8 @@ async def random_emoji(self, ctx: commands.Context): await ctx.send(f"Emoji link: {emoji_url}") else: await ctx.send("No custom emojis found in this server.") - emojilink.add_command(random_emoji) - @commands.command(name="search") + @emojilink.command(name="search") async def emoji_search(self, ctx: commands.Context, keyword: str): """Search custom emojis by name.""" matching_emojis = [ @@ -151,7 +148,6 @@ async def emoji_search(self, ctx: commands.Context, keyword: str): await ctx.send("\n".join(matching_emojis)) else: await ctx.send(f"No custom emojis found matching '{keyword}'.") - emojilink.add_command(emoji_search) # ----------------------------- # Helpers @@ -171,7 +167,7 @@ def get_all_emojis(self, emojis): # Add / Copy / Delete / Rename # ----------------------------- - @commands.command(name="add", aliases=["create"]) + @emojilink.command(name="add", aliases=["create"]) @commands.has_permissions(manage_emojis=True) async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union[discord.PartialEmoji, str] = None): """Add a custom emoji with automatic background removal.""" @@ -223,9 +219,8 @@ async def add_emoji(self, ctx: commands.Context, name: str, source: typing.Union await ctx.send("I do not have permissions to add emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") - emojilink.add_command(add_emoji) - @commands.command(name="copy") + @emojilink.command(name="copy") @commands.has_permissions(manage_emojis=True) async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): """Copy a custom emoji with automatic background removal.""" @@ -267,9 +262,8 @@ async def copy_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): await ctx.send("I do not have permissions to add emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") - emojilink.add_command(copy_emoji) - @commands.command(name="delete") + @emojilink.command(name="delete") @commands.has_permissions(manage_emojis=True) async def delete_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji): """Delete a custom emoji from the server.""" @@ -305,9 +299,8 @@ async def cancel_callback(interaction: discord.Interaction): await ctx.send(f"Are you sure you want to delete the emoji {emoji}?", view=view) except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") - emojilink.add_command(delete_emoji) - @commands.command(name="rename", aliases=["edit"]) + @emojilink.command(name="rename", aliases=["edit"]) @commands.has_permissions(manage_emojis=True) async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, new_name: str): """Rename a custom emoji in the server.""" @@ -329,4 +322,3 @@ async def rename_emoji(self, ctx: commands.Context, emoji: discord.PartialEmoji, await ctx.send("I do not have permissions to rename emojis.") except Exception as e: await ctx.send(f"An unexpected error occurred: {e}") - emojilink.add_command(rename_emoji) From 09372eb037456f28726714180475843a116a2902 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sat, 18 Oct 2025 21:41:19 +0100 Subject: [PATCH 07/25] Update stats.py --- skysearch/utils/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skysearch/utils/stats.py b/skysearch/utils/stats.py index 5936256..0b5400b 100644 --- a/skysearch/utils/stats.py +++ b/skysearch/utils/stats.py @@ -68,7 +68,7 @@ def _(text): return text name=_("⚡ Performance"), value=( _("**Avg Response:** {avg:.3f}s\n**Last 24h:** {requests:,} requests").format( - avg=api_stats['avg_response_time'], + avg=api_stats['avg_response_time'] * 1000, requests=api_stats['requests_last_24h'] ) ), From 0577e36f272ed18be296074cf5a148d8c318a764 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sat, 18 Oct 2025 21:42:38 +0100 Subject: [PATCH 08/25] Revert "Update stats.py" This reverts commit 09372eb037456f28726714180475843a116a2902. --- skysearch/utils/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skysearch/utils/stats.py b/skysearch/utils/stats.py index 0b5400b..5936256 100644 --- a/skysearch/utils/stats.py +++ b/skysearch/utils/stats.py @@ -68,7 +68,7 @@ def _(text): return text name=_("⚡ Performance"), value=( _("**Avg Response:** {avg:.3f}s\n**Last 24h:** {requests:,} requests").format( - avg=api_stats['avg_response_time'] * 1000, + avg=api_stats['avg_response_time'], requests=api_stats['requests_last_24h'] ) ), From b1b22b931c99f2bb5252eda79b10990d02674615 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 17:17:06 +0100 Subject: [PATCH 09/25] Fix timezone handling for next_iteration datetime Added a check to ensure next_iteration is timezone-aware by coercing naive datetimes to UTC. This prevents errors in datetime arithmetic when calculating time remaining. --- skysearch/commands/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skysearch/commands/admin.py b/skysearch/commands/admin.py index c74a075..955f113 100644 --- a/skysearch/commands/admin.py +++ b/skysearch/commands/admin.py @@ -160,6 +160,9 @@ async def list_alert_channels(self, ctx): next_iteration = self.cog.check_emergency_squawks.next_iteration now = datetime.datetime.now(datetime.timezone.utc) if next_iteration: + # Ensure timezone-aware arithmetic by coercing naive datetime to UTC + if next_iteration.tzinfo is None: + next_iteration = next_iteration.replace(tzinfo=datetime.timezone.utc) time_remaining = (next_iteration - now).total_seconds() if time_remaining > 0: time_remaining_formatted = f"" From 9e8062f2610c80d64bff56b96c916b2f9e55bfce Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 17:20:36 +0100 Subject: [PATCH 10/25] Add custom role support for alerts Custom alerts can now specify a role to mention, both via Discord commands and the dashboard. The role is stored per alert and displayed in alert listings. Alert messages will mention the custom role if set, otherwise default to the guild's alert role. --- skysearch/commands/admin.py | 16 ++++++++-- skysearch/dashboard/dashboard_integration.py | 32 ++++++++++++++++++-- skysearch/skysearch.py | 21 ++++++++++--- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/skysearch/commands/admin.py b/skysearch/commands/admin.py index 955f113..e8b0caf 100644 --- a/skysearch/commands/admin.py +++ b/skysearch/commands/admin.py @@ -610,7 +610,7 @@ async def apistats_debug(self, ctx): ) await ctx.send(embed=embed) - async def add_custom_alert(self, ctx, alert_type: str, value: str, cooldown: int = 5, channel: discord.TextChannel = None): + async def add_custom_alert(self, ctx, alert_type: str, value: str, cooldown: int = 5, channel: discord.TextChannel = None, role: discord.Role = None): """Add a custom alert for specific aircraft or squawks. Alert types: @@ -660,6 +660,7 @@ async def add_custom_alert(self, ctx, alert_type: str, value: str, cooldown: int 'value': value, 'cooldown': cooldown, 'custom_channel': channel.id if channel else None, + 'custom_role': role.id if role else None, 'created_by': ctx.author.id, 'created_at': datetime.datetime.utcnow().isoformat(), 'last_triggered': None @@ -668,9 +669,10 @@ async def add_custom_alert(self, ctx, alert_type: str, value: str, cooldown: int await guild_config.custom_alerts.set(custom_alerts) channel_info = f" to {channel.mention}" if channel else " to default alert channel" + role_info = f" and will mention {role.mention}" if role else "" embed = discord.Embed( title="✅ Custom Alert Added", - description=f"Added alert for {alert_type} '{value}' with {cooldown} minute cooldown{channel_info}", + description=f"Added alert for {alert_type} '{value}' with {cooldown} minute cooldown{channel_info}{role_info}", color=0x00ff00 ) await ctx.send(embed=embed) @@ -744,7 +746,7 @@ async def list_custom_alerts(self, ctx): if alert_info['last_triggered']: last_triggered = datetime.datetime.fromisoformat(alert_info['last_triggered']).strftime("%Y-%m-%d %H:%M UTC") - # Get channel information + # Get channel and role information custom_channel_id = alert_info.get('custom_channel') channel_info = "" if custom_channel_id: @@ -753,6 +755,13 @@ async def list_custom_alerts(self, ctx): channel_info = f"**Channel:** {channel_name}\n" else: channel_info = "**Channel:** Default\n" + + custom_role_id = alert_info.get('custom_role') + role_info = "" + if custom_role_id: + role_obj = ctx.guild.get_role(custom_role_id) + role_name = role_obj.mention if role_obj else f"<@&{custom_role_id}>" + role_info = f"**Role:** {role_name}\n" embed.add_field( name=f"🔔 {alert_id}", @@ -760,6 +769,7 @@ async def list_custom_alerts(self, ctx): f"**Value:** {alert_info['value']}\n" f"**Cooldown:** {alert_info['cooldown']} minutes\n" f"{channel_info}" + f"{role_info}" f"**Created:** {created_at.strftime('%Y-%m-%d %H:%M UTC')}\n" f"**Last Triggered:** {last_triggered}", inline=False diff --git a/skysearch/dashboard/dashboard_integration.py b/skysearch/dashboard/dashboard_integration.py index 5157eff..7a6fd6a 100644 --- a/skysearch/dashboard/dashboard_integration.py +++ b/skysearch/dashboard/dashboard_integration.py @@ -752,6 +752,7 @@ def __init__(self): alert_value = wtforms.StringField("Alert Value", render_kw={"class": "form-field", "placeholder": "Enter value to monitor..."}) cooldown = wtforms.IntegerField("Cooldown (minutes)", render_kw={"class": "form-field", "placeholder": "5", "min": "1", "max": "1440"}) custom_channel = wtforms.StringField("Custom Channel ID (optional)", render_kw={"class": "form-field", "placeholder": "Leave empty to use default alert channel"}) + custom_role = wtforms.StringField("Custom Role ID (optional)", render_kw={"class": "form-field", "placeholder": "Leave empty to use default alert role"}) submit_alert = wtforms.SubmitField("Add Alert", render_kw={"class": "form-submit"}) # Lightweight CSRF-only form for remove action @@ -816,6 +817,7 @@ def __init__(self): alert_value = alert_form.alert_value.data.strip() cooldown = alert_form.cooldown.data or 5 custom_channel_id = alert_form.custom_channel.data.strip() if alert_form.custom_channel.data else None + custom_role_id = alert_form.custom_role.data.strip() if alert_form.custom_role.data else None if not alert_value: result_html = ''' @@ -841,6 +843,18 @@ def __init__(self): Error: Custom channel not found. Please enter a valid channel ID. ''' + elif custom_role_id and not custom_role_id.isdigit(): + result_html = ''' +
+ Error: Custom role ID must be a valid numeric ID. +
+ ''' + elif custom_role_id and not guild.get_role(int(custom_role_id)): + result_html = ''' +
+ Error: Custom role not found. Please enter a valid role ID. +
+ ''' else: alert_id = f"{alert_type}_{alert_value.lower()}" @@ -856,6 +870,7 @@ def __init__(self): 'value': alert_value, 'cooldown': cooldown, 'custom_channel': int(custom_channel_id) if custom_channel_id else None, + 'custom_role': int(custom_role_id) if custom_role_id else None, 'created_by': 'dashboard_user', 'created_at': datetime.datetime.utcnow().isoformat(), 'last_triggered': None @@ -869,10 +884,15 @@ def __init__(self): channel_info = f" to #{channel_name}" else: channel_info = " to default alert channel" + role_info = "" + if custom_role_id: + role_obj = guild.get_role(int(custom_role_id)) + role_name = f"@{role_obj.name}" if role_obj else f"<@&{custom_role_id}>" + role_info = f" and will mention {role_name}" result_html = f'''
- Success: Added alert for {alert_type} '{alert_value}' with {cooldown} minute cooldown{channel_info}. + Success: Added alert for {alert_type} '{alert_value}' with {cooldown} minute cooldown{channel_info}{role_info}.
''' @@ -921,7 +941,7 @@ def __init__(self): if alert_data['last_triggered']: last_triggered = datetime.datetime.fromisoformat(alert_data['last_triggered']).strftime("%Y-%m-%d %H:%M UTC") - # Get channel information + # Get channel and role information custom_channel_id = alert_data.get('custom_channel') channel_info = "" if custom_channel_id: @@ -930,13 +950,19 @@ def __init__(self): channel_info = f" | Channel: #{channel_name}" else: channel_info = " | Channel: Default" + role_info = "" + custom_role_id = alert_data.get('custom_role') + if custom_role_id: + role_obj = guild.get_role(custom_role_id) + role_name = f"@{role_obj.name}" if role_obj else f"<@&{custom_role_id}>" + role_info = f" | Role: {role_name}" alerts_html += f'''
🔔 {alert_id}
- Type: {alert_data['type']} | Value: {alert_data['value']} | Cooldown: {alert_data['cooldown']} min{channel_info}
+ Type: {alert_data['type']} | Value: {alert_data['value']} | Cooldown: {alert_data['cooldown']} min{channel_info}{role_info}
Created: {created_at.strftime('%Y-%m-%d %H:%M UTC')} | Last Triggered: {last_triggered}
diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index 538175d..db35125 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -225,7 +225,7 @@ async def aircraft_group(self, ctx): embed.add_field(name=_("Special Aircraft"), value="`military` `ladd` `pia`", inline=False) embed.add_field(name=_("Export"), value=_("`export` - Export aircraft data to CSV, PDF, TXT, or HTML"), inline=False) embed.add_field(name=_("Configuration"), value="`alertchannel` `alertrole` `autoicao` `autodelete` `showalertchannel` `setapimode` `apimode`", inline=False) - embed.add_field(name=_("Custom Alerts"), value="`addalert` `removealert` `listalerts` `clearalerts`\n*Use `addalert` with optional channel parameter*", inline=False) + embed.add_field(name=_("Custom Alerts"), value="`addalert` `removealert` `listalerts` `clearalerts`\n*Use `addalert` with optional channel and role parameters*", inline=False) # Add brief mention of force and cooldown clear for owners if await ctx.bot.is_owner(ctx.author): embed.add_field(name=_("Custom Alert Admin"), value="`forcealert` (owner) `clearalertcooldown`", inline=False) @@ -312,6 +312,10 @@ async def aircraft_feeder(self, ctx, *, json_input: str = None): """Extract feeder URL from JSON data or a URL containing feeder data using secure modal.""" await self.aircraft_commands.extract_feeder_url(ctx, json_input=json_input) + + + + # Admin commands @aircraft_group.command(name='alertchannel') async def aircraft_alertchannel(self, ctx, channel: discord.TextChannel = None): @@ -370,17 +374,23 @@ async def aircraft_set_api_mode(self, ctx, mode: str): await self.config.api_mode.set(mode) await ctx.send(f"✅ API mode set to **{mode}**.") + + + + + # Custom Alerts Commands @commands.guild_only() @aircraft_group.command(name='addalert') - async def aircraft_add_alert(self, ctx, alert_type: str, value: str, cooldown: int = 5, channel: discord.TextChannel = None): + async def aircraft_add_alert(self, ctx, alert_type: str, value: str, cooldown: int = 5, channel: discord.TextChannel = None, role: discord.Role = None): """Add a custom alert for specific aircraft or squawks. Alert types: icao, callsign, squawk, type, reg Cooldown: 1-1440 minutes (default: 5) Channel: Optional channel to send alerts to (default: uses alert channel) + Role: Optional role to ping for this alert (overrides default alert role) """ - await self.admin_commands.add_custom_alert(ctx, alert_type, value, cooldown, channel) + await self.admin_commands.add_custom_alert(ctx, alert_type, value, cooldown, channel, role) @commands.guild_only() @aircraft_group.command(name='removealert') @@ -805,8 +815,9 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a else: log.warning(f"Custom channel {custom_channel_id} not found for alert {alert_id}, using default channel") - # Get the alert role - alert_role_id = await guild_config.alert_role() + # Get the alert role (prefer per-alert custom_role, else guild default) + custom_role_id = alert_data.get('custom_role') + alert_role_id = custom_role_id if custom_role_id else await guild_config.alert_role() alert_role_mention = f"<@&{alert_role_id}>" if alert_role_id else "" # Prepare message data to mirror emergency alert style (pre/post hooks support) From 41bbfb714217655220cda6b3ec979359ce9c5fdd Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 17:26:12 +0100 Subject: [PATCH 11/25] Add guild-level AMP conversion statistics tracking Introduces tracking of AMP URL conversion statistics per guild, including total conversions, URLs detected, canonical URLs returned, and last conversion timestamp. Updates the dashboard stats page to display these metrics and a calculated success rate. --- ampremover/ampremover.py | 30 +++++++++++- ampremover/dashboard_integration.py | 71 +++++++++++++++-------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/ampremover/ampremover.py b/ampremover/ampremover.py index 4ff289a..ee51903 100644 --- a/ampremover/ampremover.py +++ b/ampremover/ampremover.py @@ -5,6 +5,7 @@ import aiohttp import asyncio from .dashboard_integration import DashboardIntegration +import time class AmputatorBot(DashboardIntegration, commands.Cog): """Cog to convert AMP URLs to canonical forms using the AmputatorBot API. @@ -14,8 +15,17 @@ class AmputatorBot(DashboardIntegration, commands.Cog): def __init__(self, bot): self.bot = bot - self.config = Config.get_conf(self, identifier=492089091320446976) # Use a unique identifier for your cog - self.config.register_guild(opted_in=False) # Register a guild-specific variable for opted-in status + self.config = Config.get_conf(self, identifier=492089091320446976) + # Register a guild-specific variable for opted-in status and basic stats + self.config.register_guild( + opted_in=False, + stats={ + "total_conversions": 0, + "total_urls_detected": 0, + "total_canonical_returned": 0, + "last_conversion_ts": 0, + }, + ) self.opted_in_users = set() async def initialize_config(self, guild): @@ -54,6 +64,9 @@ async def convert_amp(self, ctx, *, message: str): return canonical_links = await self.fetch_canonical_links(urls) + # Update stats if invoked in a guild context + if ctx.guild is not None: + await self._update_guild_stats(ctx.guild, urls_detected=len(urls), canonical_returned=len(canonical_links)) if canonical_links: if ctx.guild: # If in a server, respond in the channel await ctx.send(f"Canonical URL(s): {'; '.join(canonical_links)}") @@ -91,6 +104,8 @@ async def on_message(self, message): urls = self.extract_urls(message.content) if urls: canonical_links = await self.fetch_canonical_links(urls) + # Update stats for guild automatic conversion checks + await self._update_guild_stats(message.guild, urls_detected=len(urls), canonical_returned=len(canonical_links)) if canonical_links: await message.channel.send(f"Canonical URL(s): {'; '.join(canonical_links)}") else: # DM context @@ -101,6 +116,17 @@ async def on_message(self, message): if canonical_links: await message.author.send(f"Canonical URL(s): {'; '.join(canonical_links)}") + async def _update_guild_stats(self, guild, *, urls_detected: int, canonical_returned: int) -> None: + """Update guild-level stats used by the dashboard. + + Increments total conversion events, accumulates counts, and records the last conversion timestamp. + """ + async with self.config.guild(guild).stats() as stats: + stats["total_conversions"] = int(stats.get("total_conversions", 0)) + 1 + stats["total_urls_detected"] = int(stats.get("total_urls_detected", 0)) + int(urls_detected) + stats["total_canonical_returned"] = int(stats.get("total_canonical_returned", 0)) + int(canonical_returned) + stats["last_conversion_ts"] = int(time.time()) + @amputator.command(name='settings') async def show_settings(self, ctx): """Display the current configuration settings for the AmputatorBot in this guild.""" diff --git a/ampremover/dashboard_integration.py b/ampremover/dashboard_integration.py index bed1af0..abb9dfc 100644 --- a/ampremover/dashboard_integration.py +++ b/ampremover/dashboard_integration.py @@ -197,24 +197,37 @@ def __init__(self): @dashboard_page(name="stats", description="View AMP URL conversion statistics", methods=("GET",)) async def stats_page(self, user: discord.User, guild: discord.Guild, **kwargs) -> typing.Dict[str, typing.Any]: """Dashboard page for viewing conversion statistics.""" - # Get guild settings + # Get guild settings and stats opted_in = await self.config.guild(guild).opted_in() - - # For now, we'll show basic stats. You can expand this later with actual conversion tracking + stats = await self.config.guild(guild).stats() + + total_conversions = int(stats.get("total_conversions", 0)) + total_urls_detected = int(stats.get("total_urls_detected", 0)) + total_canonical_returned = int(stats.get("total_canonical_returned", 0)) + last_ts = int(stats.get("last_conversion_ts", 0)) + + # Compute simple rate + success_rate = 0.0 + if total_urls_detected > 0: + success_rate = (total_canonical_returned / total_urls_detected) * 100.0 + + # Format last conversion time (Dashboard templates can format raw epoch too; keep simple here) + last_conversion = "Never" if last_ts == 0 else f"{last_ts}" + stats_html = f""" -
+

📊 AMP Remover Statistics

Statistics for {guild.name}

- -
-
-
-
+ +
+
+
+
Guild Settings
-
-

Automatic Conversion: - +

+

Automatic Conversion: + {'Enabled' if opted_in else 'Disabled'}

@@ -223,34 +236,24 @@ async def stats_page(self, user: discord.User, guild: discord.Guild, **kwargs) -
- -
-
-
-
Bot Information
+ +
+
+
+
Conversion Stats
-
-

Bot Name: {self.bot.user.display_name}

-

API Used: AmputatorBot API

-

Commands Available:

-
    -
  • [p]amputator convert - Manual conversion
  • -
  • [p]amputator optin - Enable auto-conversion
  • -
  • [p]amputator optout - Disable auto-conversion
  • -
  • [p]amputator settings - View settings
  • +
    +
      +
    • Total conversion events: {total_conversions}
    • +
    • Total URLs detected: {total_urls_detected}
    • +
    • Total canonical returned: {total_canonical_returned}
    • +
    • Success rate: {success_rate:.1f}%
    • +
    • Last conversion (epoch): {last_conversion}
- -
-
-
💡 Tip
-

To track conversion statistics, you would need to add logging functionality to the main cog. - This could include tracking the number of URLs converted, success rates, and user activity.

-
-
""" From f4b7949a30b6753b8cc7b317d570a3ccffdedf05 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 17:57:12 +0100 Subject: [PATCH 12/25] Update skysearch.py --- skysearch/skysearch.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index db35125..1be082b 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -882,6 +882,7 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a # Allow other cogs to modify the message before sending (mirror emergency flow) original_view = message_data.get('view') + original_content = message_data.get('content') squawk_code = aircraft_data.get('squawk', 'CUSTOM') message_data = await self.squawk_api.run_pre_send(alert_channel.guild, aircraft_data, squawk_code, message_data) @@ -889,12 +890,24 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for custom alert {alert_id}, restoring buttons") message_data['view'] = original_view - - # Send the message using the possibly modified data + # Ensure role mention content is preserved if removed by callbacks + if message_data.get('content') is None and original_content is not None: + log.warning(f"Pre-send callback removed content for custom alert {alert_id}, restoring mention content") + message_data['content'] = original_content + + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = alert_channel.guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent From 5de0f51121368b0809b7ca291439b1174470ff9f Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:04:20 +0100 Subject: [PATCH 13/25] Revert "Update skysearch.py" This reverts commit f4b7949a30b6753b8cc7b317d570a3ccffdedf05. --- skysearch/skysearch.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index 1be082b..db35125 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -882,7 +882,6 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a # Allow other cogs to modify the message before sending (mirror emergency flow) original_view = message_data.get('view') - original_content = message_data.get('content') squawk_code = aircraft_data.get('squawk', 'CUSTOM') message_data = await self.squawk_api.run_pre_send(alert_channel.guild, aircraft_data, squawk_code, message_data) @@ -890,24 +889,12 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for custom alert {alert_id}, restoring buttons") message_data['view'] = original_view - # Ensure role mention content is preserved if removed by callbacks - if message_data.get('content') is None and original_content is not None: - log.warning(f"Pre-send callback removed content for custom alert {alert_id}, restoring mention content") - message_data['content'] = original_content - - # Send the message using the possibly modified data (allow role mentions) - allowed_mentions = None - if alert_role_id: - role_obj = alert_channel.guild.get_role(alert_role_id) - if role_obj: - allowed_mentions = discord.AllowedMentions(roles=[role_obj]) - else: - allowed_mentions = discord.AllowedMentions(roles=True) + + # Send the message using the possibly modified data sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view'), - allowed_mentions=allowed_mentions + view=message_data.get('view') ) # Let other cogs react after the message is sent From 11188fcac12a01d6e7bf114ba65901c10c84d625 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:10:04 +0100 Subject: [PATCH 14/25] Allow role mentions in alert messages Updated alert message sending to support role mentions by setting the allowed_mentions parameter based on the alert_role_id. This ensures that only the intended roles are mentioned in alert notifications. --- skysearch/skysearch.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index db35125..d599daa 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -890,11 +890,19 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a log.warning(f"Pre-send callback removed view for custom alert {alert_id}, restoring buttons") message_data['view'] = original_view - # Send the message using the possibly modified data + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = alert_channel.guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent From c7c52874a514627ab41c027d95ca8a1518354741 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:13:14 +0100 Subject: [PATCH 15/25] Update skysearch.py --- skysearch/skysearch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index d599daa..9ca7979 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -627,11 +627,11 @@ async def check_emergency_squawks(self): pass if squawk_code in ['7500', '7600', '7700']: - tweet_text = f"Spotted an aircraft declaring an emergency! #Squawk #{squawk_code}, flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #Emergency\n\nJoin via Discord to search and discuss planes with your friends for free - https://discord.gg/X8huyaeXrA" + tweet_text = f"Spotted an aircraft declaring an emergency! #Squawk #{squawk_code}, flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #Emergency\n\nJoin via Discord to search and discuss planes with your friends for free - discord.gg/WW4eNQj9qr" else: - tweet_text = f"Tracking flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph using #SkySearch\n\nJoin via Discord to search and discuss planes with your friends for free - https://discord.gg/X8huyaeXrA" + tweet_text = f"Tracking flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph using #SkySearch\n\nJoin via Discord to search and discuss planes with your friends for free - discord.gg/WW4eNQj9qr" - tweet_url = f"https://twitter.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" + tweet_url = f"https://x.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" view.add_item(discord.ui.Button(label="Post on X", emoji="📣", url=tweet_url, style=discord.ButtonStyle.link)) whatsapp_text = f"Check out this aircraft! Flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. Track live @ https://globe.airplanes.live/?icao={icao} #SkySearch" @@ -868,8 +868,8 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a except Exception: pass - tweet_text = f"Custom alert triggered! {alert_data['type'].upper()} '{alert_data['value']}' spotted - Flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #CustomAlert\n\nJoin via Discord to search and discuss planes with your friends for free - https://discord.gg/X8huyaeXrA" - tweet_url = f"https://twitter.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" + tweet_text = f"alert triggered! {alert_data['type'].upper()} '{alert_data['value']}' spotted - Flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #discordAlert\n\nJoin via Discord to search and discuss planes with your friends for free - discord.gg/WW4eNQj9qr" + tweet_url = f"https://x.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" view.add_item(discord.ui.Button(label="Post on X", emoji="📣", url=tweet_url, style=discord.ButtonStyle.link)) whatsapp_text = f"Custom alert! {alert_data['type'].upper()} '{alert_data['value']}' spotted - Flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. Track live @ https://globe.airplanes.live/?icao={icao} #SkySearch" @@ -1067,11 +1067,11 @@ async def simulate_emergency_alert(self, ctx, hex_code: str, squawk_code: str = pass if squawk_code in ['7500', '7600', '7700']: - tweet_text = f"Spotted an aircraft declaring an emergency! #Squawk #{squawk_code}, flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #Emergency\n\nJoin via Discord to search and discuss planes with your friends for free - https://discord.gg/X8huyaeXrA" + tweet_text = f"Spotted an aircraft declaring an emergency! #Squawk #{squawk_code}, flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. #SkySearch #Emergency\n\nJoin via Discord to search and discuss planes with your friends for free - discord.gg/WW4eNQj9qr" else: - tweet_text = f"Tracking flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph using #SkySearch\n\nJoin via Discord to search and discuss planes with your friends for free - https://discord.gg/X8huyaeXrA" + tweet_text = f"Tracking flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph using #SkySearch\n\nJoin via Discord to search and discuss planes with your friends for free - discord.gg/WW4eNQj9qr" - tweet_url = f"https://twitter.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" + tweet_url = f"https://x.com/intent/tweet?text={urllib.parse.quote_plus(tweet_text)}" view.add_item(discord.ui.Button(label="Post on X", emoji="📣", url=tweet_url, style=discord.ButtonStyle.link)) whatsapp_text = f"Check out this aircraft! Flight {aircraft_data.get('flight', '')} at position {lat}, {lon} with speed {ground_speed_mph} mph. Track live @ https://globe.airplanes.live/?icao={icao} #SkySearch" From f48801a660db45dd2771006aa40b07d6751b33b6 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:15:38 +0100 Subject: [PATCH 16/25] Update skysearch.py --- skysearch/skysearch.py | 45 +++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index 9ca7979..cf824b4 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -648,18 +648,31 @@ async def check_emergency_squawks(self): # Let other cogs modify the message before sending original_view = message_data.get('view') + original_content = message_data.get('content') message_data = await self.squawk_api.run_pre_send(guild, aircraft_info, squawk_code, message_data) # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for {icao_hex}, restoring buttons") message_data['view'] = original_view - - # Send the message using the possibly modified data + # Ensure role mention content is preserved if removed by callbacks + if message_data.get('content') is None and original_content is not None: + log.warning(f"Pre-send callback removed content for {icao_hex}, restoring mention content") + message_data['content'] = original_content + + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent @@ -882,13 +895,18 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a # Allow other cogs to modify the message before sending (mirror emergency flow) original_view = message_data.get('view') + original_content = message_data.get('content') squawk_code = aircraft_data.get('squawk', 'CUSTOM') message_data = await self.squawk_api.run_pre_send(alert_channel.guild, aircraft_data, squawk_code, message_data) - + # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for custom alert {alert_id}, restoring buttons") message_data['view'] = original_view + # Ensure role mention content is preserved if removed by callbacks + if message_data.get('content') is None and original_content is not None: + log.warning(f"Pre-send callback removed content for custom alert {alert_id}, restoring mention content") + message_data['content'] = original_content # Send the message using the possibly modified data (allow role mentions) allowed_mentions = None @@ -1091,18 +1109,31 @@ async def simulate_emergency_alert(self, ctx, hex_code: str, squawk_code: str = # Let other cogs modify the message before sending original_view = message_data.get('view') + original_content = message_data.get('content') message_data = await self.squawk_api.run_pre_send(guild, fake_aircraft, squawk_code, message_data) # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for {hex_code}, restoring buttons") message_data['view'] = original_view - - # Send the message using the possibly modified data + # Ensure role mention content is preserved if removed by callbacks + if message_data.get('content') is None and original_content is not None: + log.warning(f"Pre-send callback removed content for {hex_code}, restoring mention content") + message_data['content'] = original_content + + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent From df86b4a3166bb972476617220d5a53e4185eda76 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:19:42 +0100 Subject: [PATCH 17/25] Revert "Update skysearch.py" This reverts commit f48801a660db45dd2771006aa40b07d6751b33b6. --- skysearch/skysearch.py | 45 +++++++----------------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index cf824b4..9ca7979 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -648,31 +648,18 @@ async def check_emergency_squawks(self): # Let other cogs modify the message before sending original_view = message_data.get('view') - original_content = message_data.get('content') message_data = await self.squawk_api.run_pre_send(guild, aircraft_info, squawk_code, message_data) # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for {icao_hex}, restoring buttons") message_data['view'] = original_view - # Ensure role mention content is preserved if removed by callbacks - if message_data.get('content') is None and original_content is not None: - log.warning(f"Pre-send callback removed content for {icao_hex}, restoring mention content") - message_data['content'] = original_content - - # Send the message using the possibly modified data (allow role mentions) - allowed_mentions = None - if alert_role_id: - role_obj = guild.get_role(alert_role_id) - if role_obj: - allowed_mentions = discord.AllowedMentions(roles=[role_obj]) - else: - allowed_mentions = discord.AllowedMentions(roles=True) + + # Send the message using the possibly modified data sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view'), - allowed_mentions=allowed_mentions + view=message_data.get('view') ) # Let other cogs react after the message is sent @@ -895,18 +882,13 @@ async def _send_custom_alert(self, alert_channel, guild_config, aircraft_info, a # Allow other cogs to modify the message before sending (mirror emergency flow) original_view = message_data.get('view') - original_content = message_data.get('content') squawk_code = aircraft_data.get('squawk', 'CUSTOM') message_data = await self.squawk_api.run_pre_send(alert_channel.guild, aircraft_data, squawk_code, message_data) - + # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for custom alert {alert_id}, restoring buttons") message_data['view'] = original_view - # Ensure role mention content is preserved if removed by callbacks - if message_data.get('content') is None and original_content is not None: - log.warning(f"Pre-send callback removed content for custom alert {alert_id}, restoring mention content") - message_data['content'] = original_content # Send the message using the possibly modified data (allow role mentions) allowed_mentions = None @@ -1109,31 +1091,18 @@ async def simulate_emergency_alert(self, ctx, hex_code: str, squawk_code: str = # Let other cogs modify the message before sending original_view = message_data.get('view') - original_content = message_data.get('content') message_data = await self.squawk_api.run_pre_send(guild, fake_aircraft, squawk_code, message_data) # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for {hex_code}, restoring buttons") message_data['view'] = original_view - # Ensure role mention content is preserved if removed by callbacks - if message_data.get('content') is None and original_content is not None: - log.warning(f"Pre-send callback removed content for {hex_code}, restoring mention content") - message_data['content'] = original_content - - # Send the message using the possibly modified data (allow role mentions) - allowed_mentions = None - if alert_role_id: - role_obj = guild.get_role(alert_role_id) - if role_obj: - allowed_mentions = discord.AllowedMentions(roles=[role_obj]) - else: - allowed_mentions = discord.AllowedMentions(roles=True) + + # Send the message using the possibly modified data sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view'), - allowed_mentions=allowed_mentions + view=message_data.get('view') ) # Let other cogs react after the message is sent From ec5af6000ed1eb4e539578ff5b0dd04e9e542bbb Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 18:23:28 +0100 Subject: [PATCH 18/25] Update skysearch.py --- skysearch/skysearch.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index 9ca7979..3cc80f0 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -655,11 +655,19 @@ async def check_emergency_squawks(self): log.warning(f"Pre-send callback removed view for {icao_hex}, restoring buttons") message_data['view'] = original_view - # Send the message using the possibly modified data + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent @@ -1098,11 +1106,19 @@ async def simulate_emergency_alert(self, ctx, hex_code: str, squawk_code: str = log.warning(f"Pre-send callback removed view for {hex_code}, restoring buttons") message_data['view'] = original_view - # Send the message using the possibly modified data + # Send the message using the possibly modified data (allow role mentions) + allowed_mentions = None + if alert_role_id: + role_obj = guild.get_role(alert_role_id) + if role_obj: + allowed_mentions = discord.AllowedMentions(roles=[role_obj]) + else: + allowed_mentions = discord.AllowedMentions(roles=True) sent_message = await alert_channel.send( content=message_data.get('content'), embed=message_data.get('embed'), - view=message_data.get('view') + view=message_data.get('view'), + allowed_mentions=allowed_mentions ) # Let other cogs react after the message is sent From c5247b3bfc557bf0ddefcbea1e8fc6a1bafb2906 Mon Sep 17 00:00:00 2001 From: BenCos17 Date: Sun, 19 Oct 2025 19:48:18 +0100 Subject: [PATCH 19/25] Update skysearch.py --- skysearch/skysearch.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skysearch/skysearch.py b/skysearch/skysearch.py index 3cc80f0..09a7c72 100644 --- a/skysearch/skysearch.py +++ b/skysearch/skysearch.py @@ -582,6 +582,9 @@ async def check_emergency_squawks(self): alert_role_id = await guild_config.alert_role() alert_role_mention = f"<@&{alert_role_id}>" if alert_role_id else "" + # Debug logging for emergency alerts + log.info(f"EMERGENCY ALERT {icao_hex}: alert_role_id={alert_role_id}, mention='{alert_role_mention}'") + # Prepare message data for pre-send hooks message_data = { 'content': alert_role_mention if alert_role_mention else None, @@ -648,12 +651,20 @@ async def check_emergency_squawks(self): # Let other cogs modify the message before sending original_view = message_data.get('view') + original_content = message_data.get('content') message_data = await self.squawk_api.run_pre_send(guild, aircraft_info, squawk_code, message_data) # Ensure buttons are preserved if no other cog modified the view if message_data.get('view') is None and original_view is not None: log.warning(f"Pre-send callback removed view for {icao_hex}, restoring buttons") message_data['view'] = original_view + # Ensure role mention content is preserved if removed by callbacks + if message_data.get('content') is None and original_content is not None: + log.warning(f"Pre-send callback removed content for {icao_hex}, restoring mention content") + message_data['content'] = original_content + + # Debug final content before sending + log.info(f"EMERGENCY ALERT {icao_hex}: Final content before send: '{message_data.get('content')}'") # Send the message using the possibly modified data (allow role mentions) allowed_mentions = None From 591c7e717929a55583333188617d3f62c51ba3a5 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:08:23 +0000 Subject: [PATCH 20/25] forgot name in dank cog info.json --- dank/info.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dank/info.json b/dank/info.json index f32e31f..bcbf22d 100644 --- a/dank/info.json +++ b/dank/info.json @@ -1,6 +1,6 @@ { "name": "Dank", - "author": ["yourname"], + "author": ["bencos17"], "description": "Gay and Simp rating machine, Dank Memer style.", "requirements": [], "min_bot_version": "3.0.0", @@ -8,4 +8,4 @@ "hidden": false, "disabled": false, "type": "COG" -} \ No newline at end of file +} From b3efa515f31769ade761b9b481bfd04153e0276d Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:02:55 +0000 Subject: [PATCH 21/25] Update README.md --- skysearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skysearch/README.md b/skysearch/README.md index bd06afe..5fcadb6 100644 --- a/skysearch/README.md +++ b/skysearch/README.md @@ -77,4 +77,4 @@ Notes: - `[p]skysearch apistats_save` - Manually save API statistics (owner only) ### Dashboard Integration -- `/dashboard/apistats` - Web interface for viewing API statistics and performance metrics +- `/third-parties/Skysearch` - Web interface for cog From b8a51667ad7c24b10e3a399ad32889c320baad8f Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:09:43 +0000 Subject: [PATCH 22/25] Update README.md --- skysearch/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skysearch/README.md b/skysearch/README.md index 5fcadb6..4a2655f 100644 --- a/skysearch/README.md +++ b/skysearch/README.md @@ -77,4 +77,9 @@ Notes: - `[p]skysearch apistats_save` - Manually save API statistics (owner only) ### Dashboard Integration -- `/third-parties/Skysearch` - Web interface for cog +- `/third-parties/Skysearch` - Web interface for the cog +there is 4 total pages in it +- Main Page - shows stats for airplanes.live and tagged aircraft (tags aren't currently updated) +- Apistats - shows apistats for the cog itself +- Guild - allows you to change cog settings in the dashboard (uses ids, to get them enable developer mode on discord) +- Lookup - allows you to lookup data directly in the cog dashboard page From 48e65683a65305954b29b0e1e08b4a35610ab560 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:16:47 +0000 Subject: [PATCH 23/25] Update README.md --- skysearch/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skysearch/README.md b/skysearch/README.md index 4a2655f..b79b58a 100644 --- a/skysearch/README.md +++ b/skysearch/README.md @@ -79,7 +79,7 @@ Notes: ### Dashboard Integration - `/third-parties/Skysearch` - Web interface for the cog there is 4 total pages in it -- Main Page - shows stats for airplanes.live and tagged aircraft (tags aren't currently updated) -- Apistats - shows apistats for the cog itself -- Guild - allows you to change cog settings in the dashboard (uses ids, to get them enable developer mode on discord) -- Lookup - allows you to lookup data directly in the cog dashboard page + - Main Page - shows stats for airplanes.live and tagged aircraft (tags aren't currently updated) + - Apistats - shows apistats for the cog itself + - Guild - allows you to change cog settings in the dashboard (uses ids, to get them enable developer mode on discord) + - Lookup - allows you to lookup data directly in the cog dashboard page From 07d957c60437897daec1a79c633fc4b17ed9af43 Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:17:32 +0000 Subject: [PATCH 24/25] Update README.md --- skysearch/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skysearch/README.md b/skysearch/README.md index b79b58a..4a042c1 100644 --- a/skysearch/README.md +++ b/skysearch/README.md @@ -79,7 +79,7 @@ Notes: ### Dashboard Integration - `/third-parties/Skysearch` - Web interface for the cog there is 4 total pages in it - - Main Page - shows stats for airplanes.live and tagged aircraft (tags aren't currently updated) - - Apistats - shows apistats for the cog itself - - Guild - allows you to change cog settings in the dashboard (uses ids, to get them enable developer mode on discord) - - Lookup - allows you to lookup data directly in the cog dashboard page + - `Main Page` - shows stats for airplanes.live and tagged aircraft (tags aren't currently updated) + - `Apistats` - shows apistats for the cog itself + - `Guild` - allows you to change cog settings in the dashboard (uses ids, to get them enable developer mode on discord) + - `Lookup` - allows you to lookup data directly in the cog dashboard page From bae289721443d6cdeb908d2c2aee694d81a1896d Mon Sep 17 00:00:00 2001 From: Ben Cos <52817096+BenCos17@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:18:52 +0000 Subject: [PATCH 25/25] Create check-cogs.yml --- .github/workflows/check-cogs.yml | 171 +++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 .github/workflows/check-cogs.yml diff --git a/.github/workflows/check-cogs.yml b/.github/workflows/check-cogs.yml new file mode 100644 index 0000000..31f5fe0 --- /dev/null +++ b/.github/workflows/check-cogs.yml @@ -0,0 +1,171 @@ + +# For tests with nektos/act use: +# act -e .\.github\workflows\fixtures\push.json -W .\.github\workflows\check-cogs.yml --container-options "-v /c/privat/codex/d-cogs/.artifacts/dist:/tmp/dist:ro" --secret-file .secrets + +# To test with local Red-DiscordBot build artifact, mount the host folder containing the built wheels to /tmp/dist in the container. +# docker run -it --rm -v /c/privat/codex/d-cogs/.artifacts/dist:/tmp/dist python:3.11 bash +name: "Check Cogs" + +on: + pull_request: + +env: + BUILD_ARTIFACT_NAME: "my-build-artifact" + COG_PATHS: "dworld" # comma-separated list of cog folder names + RPC_PORT: "6133" + +jobs: + install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # DEMO: how to install Red-DiscordBot, can install from PyPI directly! + - name: Build Red-DiscordBot + uses: nntin/d-flows/actions/build-red-discordbot@v1 + with: + red_commit: "" # optional commit SHA + artifact_name: ${{ env.BUILD_ARTIFACT_NAME }} # optional artifact name + + test-cogs: + runs-on: ubuntu-latest + needs: install + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Skip artifact_name if installing from PyPI directly + - name: Install Red-DiscordBot + uses: nntin/d-flows/actions/install-red-discordbot@v1 + with: + artifact_name: ${{ env.BUILD_ARTIFACT_NAME }} # same artifact name used for build + + # Configure Red-DiscordBot with instance name "tinkerer" + # todo: add input for skipping --dry-run + - name: Configure Red-DiscordBot + uses: nntin/d-flows/actions/setup-red-discordbot@v1 + with: + token: ${{ secrets.DISCORD_BOT_TOKEN }} + optional_args: "--no-cogs" # Example optional argument to run without loading any cogs + # --dry-run fails due to bug in Red-DiscordBot https://github.com/Cog-Creators/Red-DiscordBot/issues/6572 + continue-on-error: true + + # For dworld cog d-back and wtforms are required + - name: Install dependencies + run: | + uv pip install d-back wtforms --system + + # Actual magic: test that cogs can be loaded/unloaded via RPC + - name: Test cogs via RPC + uses: nntin/d-flows/actions/test-red-discordbot@v1 + with: + token: ${{ secrets.DISCORD_BOT_TOKEN }} + cog_paths: ${{ env.COG_PATHS }} + rpc_port: ${{ env.RPC_PORT }} + + ################################################################## + #### Build validation results for Discord notification #### + ################################################################## + build-validation-fields: + name: Build Validation Fields + runs-on: ubuntu-latest + needs: [install, test-cogs] + if: always() + outputs: + discord_fields: ${{ steps.discord_fields.outputs.fields }} + validation_result: ${{ steps.validation_result.outputs.result }} + + steps: + - name: Determine validation result + id: validation_result + run: | + BUILD_STATUS="${{ needs.install.result }}" + TEST_STATUS="${{ needs['test-cogs'].result }}" + + if [[ "$BUILD_STATUS" == "success" && "$TEST_STATUS" == "success" ]]; then + echo "result=success" >> "$GITHUB_OUTPUT" + else + echo "result=failed" >> "$GITHUB_OUTPUT" + fi + + - name: Build Discord Fields + id: discord_fields + run: | + BUILD_STATUS="${{ needs.install.result }}" + TEST_STATUS="${{ needs['test-cogs'].result }}" + + BUILD_EMOJI=$([[ "$BUILD_STATUS" == "success" ]] && echo "✅" || echo "❌") + TEST_EMOJI=$([[ "$TEST_STATUS" == "success" ]] && echo "✅" || echo "❌") + + FIELDS=$(jq -n \ + --arg build_status "$BUILD_EMOJI $BUILD_STATUS" \ + --arg test_status "$TEST_EMOJI $TEST_STATUS" \ + --arg cogs "${{ env.COG_PATHS }}" \ + --arg artifact "${{ env.BUILD_ARTIFACT_NAME }}" \ + --arg actor "${{ github.actor }}" \ + --arg pr_number "${{ github.event.pull_request.number }}" \ + --arg pr_url "${{ github.event.pull_request.html_url }}" \ + --arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + '[ + {"name": "Build Red-DiscordBot", "value": $build_status, "inline": true}, + {"name": "Cog Tests", "value": $test_status, "inline": true}, + {"name": "Cogs Tested", "value": $cogs, "inline": true}, + {"name": "Artifact", "value": $artifact, "inline": true}, + {"name": "PR Details", "value": ("[#" + $pr_number + "](" + $pr_url + ") by @" + $actor), "inline": false}, + {"name": "Run Details", "value": ("[View Full Run](" + $run_url + ")"), "inline": false} + ]') + + delimiter="$(openssl rand -hex 8)" + { + echo "fields<<${delimiter}" + echo "$FIELDS" + echo "${delimiter}" + } >> "$GITHUB_OUTPUT" + + ################################################################## + #### Display validation results via Step Summary #### + ################################################################## + display-validation-summary: + name: Display Validation Summary + runs-on: ubuntu-latest + needs: [install, test-cogs, build-validation-fields] + if: always() + + steps: + - name: Write Step Summary + uses: nntin/d-flows/actions/step-summary@v1 + with: + title: 'Cog Validation Results' + markdown: | + | Stage | Status | + |-------|--------| + | Build Red-DiscordBot | ${{ needs.install.result == 'success' && '✅ Passed' || '❌ Failed' }} | + | Cog Tests | ${{ needs['test-cogs'].result == 'success' && '✅ Passed' || '❌ Failed' }} | + + **Artifact**: `${{ env.BUILD_ARTIFACT_NAME }}` + + **Cogs Tested**: `${{ env.COG_PATHS }}` + + **Overall Result**: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '✅ All checks passed' || '⚠️ Some checks failed or were skipped' }} + overwrite: false + + ################################################################## + #### Display validation results via Discord #### + ################################################################## + notify-cog-validation: + name: Notify Validation Results + runs-on: ubuntu-latest + needs: [build-validation-fields, display-validation-summary] + if: always() + + steps: + - name: Send Discord Notification + uses: nntin/d-flows/actions/discord-notify@v1 + with: + webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }} + message_type: 'embed' + title: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '✅ Cog Validation Completed Successfully' || '⚠️ Cog Validation Completed with Issues' }} + description: >- + ${{ needs.build-validation-fields.outputs.validation_result == 'success' && format('All cogs ({0}) passed build and RPC validation.', env.COG_PATHS) || format('One or more stages failed for cogs: {0}. Review the workflow logs.', env.COG_PATHS) }} + color: ${{ needs.build-validation-fields.outputs.validation_result == 'success' && '3066993' || '16776960' }} + fields: ${{ needs.build-validation-fields.outputs.discord_fields }} \ No newline at end of file