diff --git a/src/bot/cogs/constants.py b/src/bot/cogs/constants.py new file mode 100644 index 0000000..12d10ef --- /dev/null +++ b/src/bot/cogs/constants.py @@ -0,0 +1,74 @@ +LATEX_COLORS = { + "emerald": "Emerald", + "violet": "Violet", + "blueviolet": "BlueViolet", + "skyblue": "SkyBlue", + "wildstrawberry": "WildStrawberry", + "red": "Red", + "periwinkle": "Periwinkle", + "sepia": "Sepia", + "magenta": "Magenta", + "peach": "Peach", + "cyan": "Cyan", + "bluegreen": "BlueGreen", + "orangered": "OrangeRed", + "limegreen": "LimeGreen", + "royalpurple": "RoyalPurple", + "fuchsia": "Fuchsia", + "teal": "Teal", + "cornflowerblue": "CornflowerBlue", + "turquoise": "Turquoise", + "yelloworange": "YellowOrange", + "mulberry": "Mulberry", + "white": "White", + "violetred": "VioletRed", + "brown": "Brown", + "redviolet": "RedViolet", + "tealblue": "TealBlue", + "green": "Green", + "navyblue": "NavyBlue", + "tan": "Tan", + "carnationpink": "CarnationPink", + "burntorange": "BurntOrange", + "maroon": "Maroon", + "goldenrod": "Goldenrod", + "orchid": "Orchid", + "rubinered": "RubineRed", + "processblue": "ProcessBlue", + "olivegreen": "OliveGreen", + "yellowgreen": "YellowGreen", + "salmon": "Salmon", + "junglegreen": "JungleGreen", + "rawsienna": "RawSienna", + "thistle": "Thistle", + "greenyellow": "GreenYellow", + "melon": "Melon", + "pinegreen": "PineGreen", + "lavender": "Lavender", + "aquamarine": "Aquamarine", + "springgreen": "SpringGreen", + "darkred": "DarkRed", + "darkblue": "DarkBlue", + "forestgreen": "ForestGreen", + "darkorchid": "DarkOrchid", + "orange": "Orange", + "yellow": "Yellow", + "royalblue": "RoyalBlue", + "gray": "Gray", + "midnightblue": "MidnightBlue", + "redorange": "RedOrange", + "darkorange": "DarkOrange", + "cadetblue": "CadetBlue", + "cerulean": "Cerulean", + "apricot": "Apricot", + "mahogany": "Mahogany", + "bittersweet": "Bittersweet", + "blue": "Blue", + "purple": "Purple", + "darkgreen": "DarkGreen", + "plum": "Plum", + "dandelion": "Dandelion", + "seagreen": "SeaGreen", + "black": "Black", + "brickred": "BrickRed", +} diff --git a/src/bot/cogs/features/latex.py b/src/bot/cogs/features/latex.py new file mode 100644 index 0000000..f048738 --- /dev/null +++ b/src/bot/cogs/features/latex.py @@ -0,0 +1,155 @@ +""" +LaTeX command to generate mathematical equations. +""" + +import logging +from pathlib import Path + +import aiohttp +import discord +from discord import app_commands +from discord.ext import commands + +from ..constants import LATEX_COLORS + +logger = logging.getLogger(__name__) + + +def embed_info(title: str, message: str, image_url: str | None = None) -> discord.Embed: + """ + Embedding for general things + """ + embed = discord.Embed( + title=title, description=message, color=discord.Color.dark_purple() + ) + + if image_url: + embed.set_image(url=image_url) + + return embed + + +class LaTeX(commands.Cog): + """ + # Generates a LaTeX equation image using the CodeCogs API and displays it in an embed. + """ + + def __init__(self, bot: commands.Bot): + self.bot = bot + self.session: aiohttp.ClientSession | None = None + + async def cog_load(self) -> None: + self.session = aiohttp.ClientSession() + + async def cog_unload(self) -> None: + if self.session: + await self.session.close() + + @app_commands.command(description="Show the available LaTeX colors.") + async def latex_colors(self, interaction: discord.Interaction) -> None: + file = discord.File( + Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"), + filename="colors.png", + ) + embed = embed_info( + "LaTeX Colors", + "The following colors are available:", + "attachment://colors.png", + ) + await interaction.response.send_message(file=file, embed=embed, ephemeral=True) + + async def latex_available(self, image_path: str) -> bool: + try: + if not self.session: + return False + async with self.session.get(image_path) as response: + return response.status == 200 + except aiohttp.ClientError: + logger.exception("Failed to fetch LaTeX image from CodeCogs") + return False + + def normalize_color(self, value: str, default: str) -> str: + normalized = value.strip().replace(" ", "").lower() + return LATEX_COLORS.get(normalized, default) + + @app_commands.command() + async def latex( + self, + interaction: discord.Interaction, + equation: str, + *, + bg_color: str = "Black", + txt_color: str = "White", + dpi: int = 300, + ) -> None: + """ + Send a mathematical equation using LaTeX. + + Parameters + ---------- + equation : LaTeX equation (do not include `$` delimiters). + bg_color : Background color. See `/latex_colors` for available colors. + txt_color : Text color. See `/latex_colors` for available colors. + dpi : Image resolution in DPI (200–500). + """ + logger.info( + "%s used the %s command.", + interaction.user.display_name, + interaction.command, + ) + + # check for valid user specified color for text and background + bg_color = self.normalize_color(bg_color, "Black") + txt_color = self.normalize_color(txt_color, "White") + if txt_color == bg_color: + txt_color = "White" if bg_color == "Black" else "Black" + + # handle necessary character replacements + character_replacements = { + "\n": r"\n", + "\r": r"\r", + "\t": r"\t", + "\x08": r"\b", + "\x0c": r"\f", + "\x0b": r"\v", + "\x07": r"\a", + "%": r"\%", + "&": r"\&", + "#": r"%23", + " ": "&space;", + } + + for e, c in character_replacements.items(): + equation = equation.replace(e, c) + + # prevent user specified dpi from being too big/small + dpi = max(200, min(dpi, 500)) + + image_path = ( + rf"https://latex.codecogs.com/png.image?" + rf"\dpi{{{dpi}}}" + rf"\color{{{txt_color}}}" + r"\setlength{\fboxsep}{6pt}" + r"\setlength{\fboxrule}{0pt}" + rf"\colorbox{{{bg_color}}}{{${equation}$}}" + ) + + if await self.latex_available(image_path): + embed = embed_info( + "LaTeX", + equation.replace("&space;", " "), + image_path, + ) + # Send the embed with the image + await interaction.response.send_message(embed=embed) + else: + await interaction.response.send_message( + f"Failed to fetch the image ({image_path}) from the API", ephemeral=True + ) + + +async def setup(bot: commands.Bot) -> None: + """ + Required. + """ + await bot.add_cog(LaTeX(bot)) diff --git a/src/bot/static/images/colors.png b/src/bot/static/images/colors.png new file mode 100644 index 0000000..592cf1a Binary files /dev/null and b/src/bot/static/images/colors.png differ