Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/bot/cogs/features/_latex_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
from .latex import LaTeX

logger = logging.getLogger(__name__)


class LatexEditModal(discord.ui.Modal, title="Edit LaTeX"):
equation = discord.ui.TextInput(
label="LaTeX equation",
style=discord.TextStyle.paragraph,
required=True,
)
text_color = discord.ui.TextInput(label="Text color", required=False)
bg_color = discord.ui.TextInput(label="Background color", required=False)
dpi = discord.ui.TextInput(label="DPI", required=False)

def __init__(self, view: "LatexView"):
super().__init__()

self.view = view

self.equation.default = view.equation
self.text_color.default = view.text_color
self.bg_color.default = view.bg_color
self.dpi.default = str(view.dpi)

async def on_submit(self, interaction: discord.Interaction):
self.view.equation = self.equation.value

if self.text_color.value:
self.view.text_color = self.text_color.value

if self.bg_color.value:
self.view.bg_color = self.bg_color.value

if self.dpi.value:
try:
self.view.dpi = int(self.dpi.value)
except ValueError:
logger.exception(
"%s input a non-number value for DIP in the /latex command.",
interaction.user.display_name,
)

await interaction.response.defer(ephemeral=True)

embed, file = await self.view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=self.view,
)


class LatexView(discord.ui.View):
def __init__(
self,
latex: LaTeX,
interaction: discord.Interaction,
equation: str,
bg_color: str,
text_color: str,
dpi: int,
):
super().__init__(timeout=300)

self.latex = latex
self.interaction = interaction
self.equation = equation
self.bg_color = bg_color
self.text_color = text_color
self.dpi = dpi

async def render(self) -> tuple[discord.Embed, discord.File | None]:
embed = await self.latex.render(
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url and await self.latex.latex_available(embed.image.url):
return embed, None

invalid = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "invalid.png"),
filename="invalid.png",
)
embed.set_image(url="attachment://invalid.png")
return embed, invalid

@discord.ui.button(label="Edit", emoji="✏️", style=discord.ButtonStyle.primary)
async def edit(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.send_modal(LatexEditModal(self))

@discord.ui.button(label="Send", emoji="✅", style=discord.ButtonStyle.success)
async def publish(
self, interaction: discord.Interaction, button: discord.ui.Button
) -> None:
await interaction.response.defer(ephemeral=True)

embed = await self.latex.render(
Comment thread
JefeThePug marked this conversation as resolved.
self.equation,
self.bg_color,
self.text_color,
self.dpi,
)

if embed.image.url is None or not await self.latex.latex_available(
embed.image.url
):
await interaction.followup.send(
content="Failed to display the LaTeX equation.",
ephemeral=True,
)
return

await interaction.followup.send(embed=embed)

for child in self.children:
if isinstance(child, discord.ui.Button):
child.disabled = True

await self.interaction.edit_original_response(view=self)
130 changes: 78 additions & 52 deletions src/bot/cogs/features/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from src.bot.cogs import BaseCog
from src.bot.cogs.constants import LATEX_COLORS
from src.bot.cogs.features._latex_utilities import LatexView

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,6 +55,8 @@ async def cog_unload(self) -> None:

@app_commands.command(description="Show the available LaTeX colors.")
async def latex_colors(self, interaction: discord.Interaction) -> None:
logger.info("%s used the /latex_colors command.", interaction.user.display_name)

file = discord.File(
Path.cwd().joinpath("src", "bot", "static", "images", "colors.png"),
filename="colors.png",
Expand All @@ -66,52 +69,45 @@ async def latex_colors(self, interaction: discord.Interaction) -> None:
await interaction.response.send_message(file=file, embed=embed, ephemeral=True)

async def latex_available(self, image_path: str) -> bool:
"""
Check whether the CodeCogs JSON response reports a valid equation.
"""
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")

json_path = image_path.replace(
"png.image?",
"png.json?",
1,
)

async with self.session.get(json_path) as response:
if response.status != 200:
return False
data = await response.json(content_type="text/json")

return data["latex"]["valid"]

except (aiohttp.ClientError, KeyError, TypeError, ValueError):
logger.exception("Failed to validate LaTeX equation with 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
async def render(
self, equation: str, bg_color: str, txt_color: str, dpi: int
) -> discord.Embed:
# 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
# Handle necessary character replacements.
character_replacements = {
"\n": r"\n",
"\r": r"\r",
Expand All @@ -120,17 +116,13 @@ async def latex(
"\x0c": r"\f",
"\x0b": r"\v",
"\x07": r"\a",
"%": r"\%",
"&": r"\&",
"#": r"%23",
"#": "&hash;",
" ": "&space;",
}
for old, new in character_replacements.items():
equation = equation.replace(old, new)

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))
dpi = max(200, min(dpi, 800))

image_path = (
rf"https://latex.codecogs.com/png.image?"
Expand All @@ -141,18 +133,52 @@ async def latex(
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
)
return embed_info(
"LaTeX",
equation.replace("&space;", " "),
image_path,
)

@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–800).
"""
logger.info("%s used the /latex command.", interaction.user.display_name)

view = LatexView(
self,
interaction,
equation,
bg_color,
txt_color,
dpi,
)

await interaction.response.defer(ephemeral=True)

embed, file = await view.render()
Comment thread
JefeThePug marked this conversation as resolved.

await interaction.edit_original_response(
embed=embed,
attachments=[file] if file else [],
view=view,
)


async def setup(bot: commands.Bot) -> None:
Expand Down
Binary file added src/bot/static/images/invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading