From 69dc2cc350cb306ceafbeca8cf0d6216d8dc39c5 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 13:51:08 +0530 Subject: add latex command --- bot/exts/fun/latex/__init__.py | 7 +++++ bot/exts/fun/latex/_latex_cog.py | 60 ++++++++++++++++++++++++++++++++++++++++ bot/exts/fun/latex/_renderer.py | 45 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 bot/exts/fun/latex/__init__.py create mode 100644 bot/exts/fun/latex/_latex_cog.py create mode 100644 bot/exts/fun/latex/_renderer.py (limited to 'bot') diff --git a/bot/exts/fun/latex/__init__.py b/bot/exts/fun/latex/__init__.py new file mode 100644 index 00000000..e58e0447 --- /dev/null +++ b/bot/exts/fun/latex/__init__.py @@ -0,0 +1,7 @@ +from bot.bot import Bot +from bot.exts.fun.latex._latex_cog import Latex + + +def setup(bot: Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py new file mode 100644 index 00000000..239f499c --- /dev/null +++ b/bot/exts/fun/latex/_latex_cog.py @@ -0,0 +1,60 @@ +import asyncio +import hashlib +import sys +from pathlib import Path +import re + +import discord +from discord.ext import commands + + +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + +THIS_DIR = Path(__file__).parent +CACHE_DIRECTORY = THIS_DIR / "cache" +CACHE_DIRECTORY.mkdir(exist_ok=True) + + +def _prepare_input(text: str) -> str: + text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ + + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + + +class Latex(commands.Cog): + """Renders latex.""" + @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + async def latex(self, ctx: commands.Context, *, query: str) -> None: + """Renders the text in latex and sends the image.""" + query = _prepare_input(query) + query_hash = hashlib.md5(query.encode()).hexdigest() + image_path = CACHE_DIRECTORY / f"{query_hash}.png" + async with ctx.typing(): + if not image_path.exists(): + proc = await asyncio.subprocess.create_subprocess_exec( + sys.executable, + "_renderer.py", + query, + image_path.relative_to(THIS_DIR), + cwd=THIS_DIR, + stderr=asyncio.subprocess.PIPE + ) + return_code = await proc.wait() + if return_code != 0: + image_path.unlink() + err = (await proc.stderr.read()).decode() + raise commands.BadArgument(err) + + await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py new file mode 100644 index 00000000..3f6528ad --- /dev/null +++ b/bot/exts/fun/latex/_renderer.py @@ -0,0 +1,45 @@ +import sys + +from pathlib import Path +from typing import BinaryIO + +import matplotlib.pyplot as plt + +# configure fonts and colors for matplotlib +plt.rcParams.update( + { + "font.size": 16, + "mathtext.fontset": "cm", # Computer Modern font set + "mathtext.rm": "serif", + "figure.facecolor": "36393F", # matches Discord's dark mode background color + "text.color": "white", + } +) + + +def render(text: str, file_handle: BinaryIO) -> None: + """ + Saves rendered image in `file_handle`. In case the input is invalid latex, it prints the error to `stderr`. + """ + fig = plt.figure() + fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") + try: + plt.savefig(file_handle, bbox_inches="tight", dpi=600) + except ValueError as err: + # get rid of traceback, keeping just the latex error + sys.exit(err) + + +def main(): + """ + Renders a latex query and saves the output in a specified file. + Expects two command line arguments: the query and the path to the output file. + """ + query = sys.argv[1] + out_file_path = Path(sys.argv[2]) + with open(out_file_path, "wb") as out_file: + render(query, out_file) + + +if __name__ == "__main__": + main() -- cgit v1.2.3 From 36422df82d9aa010a71a7744a38105fa91aad20d Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 13:57:28 +0530 Subject: run isort and flake8 --- bot/exts/fun/latex/_latex_cog.py | 4 ++-- bot/exts/fun/latex/_renderer.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 239f499c..9ab85238 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -1,13 +1,12 @@ import asyncio import hashlib +import re import sys from pathlib import Path -import re import discord from discord.ext import commands - FORMATTED_CODE_REGEX = re.compile( r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) @@ -34,6 +33,7 @@ def _prepare_input(text: str) -> str: class Latex(commands.Cog): """Renders latex.""" + @commands.command() @commands.max_concurrency(1, commands.BucketType.guild, wait=True) async def latex(self, ctx: commands.Context, *, query: str) -> None: diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py index 3f6528ad..fb72b94c 100644 --- a/bot/exts/fun/latex/_renderer.py +++ b/bot/exts/fun/latex/_renderer.py @@ -1,5 +1,4 @@ import sys - from pathlib import Path from typing import BinaryIO @@ -19,7 +18,9 @@ plt.rcParams.update( def render(text: str, file_handle: BinaryIO) -> None: """ - Saves rendered image in `file_handle`. In case the input is invalid latex, it prints the error to `stderr`. + Saves rendered image in `file_handle`. + + In case the input is invalid latex, it prints the error to `stderr`. """ fig = plt.figure() fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") @@ -30,9 +31,10 @@ def render(text: str, file_handle: BinaryIO) -> None: sys.exit(err) -def main(): +def main() -> None: """ Renders a latex query and saves the output in a specified file. + Expects two command line arguments: the query and the path to the output file. """ query = sys.argv[1] -- cgit v1.2.3 From 8c849136a1bbcdaf367851fac2bf5d4716717325 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 17:21:15 +0530 Subject: format error messages in codeblocks --- bot/exts/fun/latex/_latex_cog.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 9ab85238..0cd4c981 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -3,6 +3,7 @@ import hashlib import re import sys from pathlib import Path +from textwrap import dedent import discord from discord.ext import commands @@ -31,6 +32,18 @@ def _prepare_input(text: str) -> str: return text +def _format_err(text: str) -> str: + # prevent escaping the codeblock by inserting a zero-width-joiner + text = text.replace("`", "`\u200d") + return dedent( + f""" + ``` + {text} + ``` + """ + ).strip() + + class Latex(commands.Cog): """Renders latex.""" @@ -55,6 +68,6 @@ class Latex(commands.Cog): if return_code != 0: image_path.unlink() err = (await proc.stderr.read()).decode() - raise commands.BadArgument(err) + raise commands.BadArgument(_format_err(err)) await ctx.send(file=discord.File(image_path, "latex.png")) -- cgit v1.2.3 From a10c07a85a2387a26103aafcabb7bd3e789624c3 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Tue, 18 Jan 2022 19:30:16 +0530 Subject: rewrite, use the rtex api instead of mpl --- bot/exts/fun/latex/_latex_cog.py | 86 +++++++++++++++++++++++++++------------- bot/exts/fun/latex/_renderer.py | 47 ---------------------- bot/exts/fun/latex/template.txt | 5 +++ 3 files changed, 63 insertions(+), 75 deletions(-) delete mode 100644 bot/exts/fun/latex/_renderer.py create mode 100644 bot/exts/fun/latex/template.txt (limited to 'bot') diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 0cd4c981..72d48b2a 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -1,13 +1,14 @@ -import asyncio import hashlib import re -import sys +import string from pathlib import Path -from textwrap import dedent +from typing import BinaryIO, Optional import discord from discord.ext import commands +from bot.bot import Bot + FORMATTED_CODE_REGEX = re.compile( r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) @@ -18,35 +19,65 @@ FORMATTED_CODE_REGEX = re.compile( re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive ) +LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" +PASTEBIN_URL = "https://paste.pythondiscord.com" + THIS_DIR = Path(__file__).parent CACHE_DIRECTORY = THIS_DIR / "cache" CACHE_DIRECTORY.mkdir(exist_ok=True) +TEMPLATE = string.Template((THIS_DIR / "template.txt").read_text()) def _prepare_input(text: str) -> str: - text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ - if match := FORMATTED_CODE_REGEX.match(text): return match.group("code") else: return text -def _format_err(text: str) -> str: - # prevent escaping the codeblock by inserting a zero-width-joiner - text = text.replace("`", "`\u200d") - return dedent( - f""" - ``` - {text} - ``` - """ - ).strip() +class InvalidLatexError(Exception): + """Represents an error caused by invalid latex.""" + + def __init__(self, logs: str): + super().__init__(logs) + self.logs = logs class Latex(commands.Cog): """Renders latex.""" + def __init__(self, bot: Bot): + self.bot = bot + + async def _generate_image(self, query: str, out_file: BinaryIO) -> None: + """Make an API request and save the generated image to cache.""" + payload = {"code": query, "format": "png"} + async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: + response_json = await response.json() + if response_json["status"] != "success": + raise InvalidLatexError(logs=response_json["log"]) + async with self.bot.http_session.get( + f"{LATEX_API_URL}/{response_json['filename']}", + data=payload, + raise_for_status=True + ) as response: + out_file.write(await response.read()) + + async def _upload_to_pastebin(self, text: str) -> Optional[str]: + """Uploads `text` to the paste service, returning the url if successful.""" + try: + async with self.bot.http_session.post( + PASTEBIN_URL + "/documents", + data=text, + raise_for_status=True + ) as response: + response_json = await response.json() + if "key" in response_json: + return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" + except Exception: + # 400 (Bad Request) means there are too many characters + pass + @commands.command() @commands.max_concurrency(1, commands.BucketType.guild, wait=True) async def latex(self, ctx: commands.Context, *, query: str) -> None: @@ -56,18 +87,17 @@ class Latex(commands.Cog): image_path = CACHE_DIRECTORY / f"{query_hash}.png" async with ctx.typing(): if not image_path.exists(): - proc = await asyncio.subprocess.create_subprocess_exec( - sys.executable, - "_renderer.py", - query, - image_path.relative_to(THIS_DIR), - cwd=THIS_DIR, - stderr=asyncio.subprocess.PIPE - ) - return_code = await proc.wait() - if return_code != 0: + try: + with open(image_path, "wb") as out_file: + await self._generate_image(TEMPLATE.substitute(text=query), out_file) + except InvalidLatexError as err: + logs_paste_url = await self._upload_to_pastebin(err.logs) + embed = discord.Embed(title="Failed to render input.") + if logs_paste_url: + embed.description = f"[View Logs]({logs_paste_url})" + else: + embed.description = "Couldn't upload logs." + await ctx.send(embed=embed) image_path.unlink() - err = (await proc.stderr.read()).decode() - raise commands.BadArgument(_format_err(err)) - + return await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py deleted file mode 100644 index fb72b94c..00000000 --- a/bot/exts/fun/latex/_renderer.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys -from pathlib import Path -from typing import BinaryIO - -import matplotlib.pyplot as plt - -# configure fonts and colors for matplotlib -plt.rcParams.update( - { - "font.size": 16, - "mathtext.fontset": "cm", # Computer Modern font set - "mathtext.rm": "serif", - "figure.facecolor": "36393F", # matches Discord's dark mode background color - "text.color": "white", - } -) - - -def render(text: str, file_handle: BinaryIO) -> None: - """ - Saves rendered image in `file_handle`. - - In case the input is invalid latex, it prints the error to `stderr`. - """ - fig = plt.figure() - fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") - try: - plt.savefig(file_handle, bbox_inches="tight", dpi=600) - except ValueError as err: - # get rid of traceback, keeping just the latex error - sys.exit(err) - - -def main() -> None: - """ - Renders a latex query and saves the output in a specified file. - - Expects two command line arguments: the query and the path to the output file. - """ - query = sys.argv[1] - out_file_path = Path(sys.argv[2]) - with open(out_file_path, "wb") as out_file: - render(query, out_file) - - -if __name__ == "__main__": - main() diff --git a/bot/exts/fun/latex/template.txt b/bot/exts/fun/latex/template.txt new file mode 100644 index 00000000..a20cc279 --- /dev/null +++ b/bot/exts/fun/latex/template.txt @@ -0,0 +1,5 @@ +\documentclass{article} +\begin{document} + \pagenumbering{gobble} + $text +\end{document} -- cgit v1.2.3 From 9d49fa0ae13900be5f14d237f92c43d11d85dfc9 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 21 Jan 2022 20:21:33 +0530 Subject: rework directory structure --- .gitignore | 2 +- bot/exts/fun/latex.py | 108 +++++++++++++++++++++++++++++++++++ bot/exts/fun/latex/__init__.py | 7 --- bot/exts/fun/latex/_latex_cog.py | 103 --------------------------------- bot/exts/fun/latex/template.txt | 5 -- bot/resources/fun/latex_template.txt | 5 ++ 6 files changed, 114 insertions(+), 116 deletions(-) create mode 100644 bot/exts/fun/latex.py delete mode 100644 bot/exts/fun/latex/__init__.py delete mode 100644 bot/exts/fun/latex/_latex_cog.py delete mode 100644 bot/exts/fun/latex/template.txt create mode 100644 bot/resources/fun/latex_template.txt (limited to 'bot') diff --git a/.gitignore b/.gitignore index 765f33c6..665df8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # bot (project-specific) log/* data/* -bot/exts/fun/latex/cache/* +bot/exts/fun/_latex_cache/* diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py new file mode 100644 index 00000000..ac43e95b --- /dev/null +++ b/bot/exts/fun/latex.py @@ -0,0 +1,108 @@ +import hashlib +import re +import string +from pathlib import Path +from typing import BinaryIO, Optional + +import discord +from discord.ext import commands + +from bot.bot import Bot + +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + +LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" +PASTEBIN_URL = "https://paste.pythondiscord.com" + +THIS_DIR = Path(__file__).parent +CACHE_DIRECTORY = THIS_DIR / "_latex_cache" +CACHE_DIRECTORY.mkdir(exist_ok=True) +TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) + + +def _prepare_input(text: str) -> str: + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + + +class InvalidLatexError(Exception): + """Represents an error caused by invalid latex.""" + + def __init__(self, logs: str): + super().__init__(logs) + self.logs = logs + + +class Latex(commands.Cog): + """Renders latex.""" + + def __init__(self, bot: Bot): + self.bot = bot + + async def _generate_image(self, query: str, out_file: BinaryIO) -> None: + """Make an API request and save the generated image to cache.""" + payload = {"code": query, "format": "png"} + async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: + response_json = await response.json() + if response_json["status"] != "success": + raise InvalidLatexError(logs=response_json["log"]) + async with self.bot.http_session.get( + f"{LATEX_API_URL}/{response_json['filename']}", + data=payload, + raise_for_status=True + ) as response: + out_file.write(await response.read()) + + async def _upload_to_pastebin(self, text: str) -> Optional[str]: + """Uploads `text` to the paste service, returning the url if successful.""" + try: + async with self.bot.http_session.post( + PASTEBIN_URL + "/documents", + data=text, + raise_for_status=True + ) as response: + response_json = await response.json() + if "key" in response_json: + return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" + except Exception: + # 400 (Bad Request) means there are too many characters + pass + + @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + async def latex(self, ctx: commands.Context, *, query: str) -> None: + """Renders the text in latex and sends the image.""" + query = _prepare_input(query) + query_hash = hashlib.md5(query.encode()).hexdigest() + image_path = CACHE_DIRECTORY / f"{query_hash}.png" + async with ctx.typing(): + if not image_path.exists(): + try: + with open(image_path, "wb") as out_file: + await self._generate_image(TEMPLATE.substitute(text=query), out_file) + except InvalidLatexError as err: + logs_paste_url = await self._upload_to_pastebin(err.logs) + embed = discord.Embed(title="Failed to render input.") + if logs_paste_url: + embed.description = f"[View Logs]({logs_paste_url})" + else: + embed.description = "Couldn't upload logs." + await ctx.send(embed=embed) + image_path.unlink() + return + await ctx.send(file=discord.File(image_path, "latex.png")) + + +def setup(bot: Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/__init__.py b/bot/exts/fun/latex/__init__.py deleted file mode 100644 index e58e0447..00000000 --- a/bot/exts/fun/latex/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from bot.bot import Bot -from bot.exts.fun.latex._latex_cog import Latex - - -def setup(bot: Bot) -> None: - """Load the Latex Cog.""" - bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py deleted file mode 100644 index 72d48b2a..00000000 --- a/bot/exts/fun/latex/_latex_cog.py +++ /dev/null @@ -1,103 +0,0 @@ -import hashlib -import re -import string -from pathlib import Path -from typing import BinaryIO, Optional - -import discord -from discord.ext import commands - -from bot.bot import Bot - -FORMATTED_CODE_REGEX = re.compile( - r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block - r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) - r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all code inside the markup - r"\s*" # any more whitespace before the end of the code markup - r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive -) - -LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" -PASTEBIN_URL = "https://paste.pythondiscord.com" - -THIS_DIR = Path(__file__).parent -CACHE_DIRECTORY = THIS_DIR / "cache" -CACHE_DIRECTORY.mkdir(exist_ok=True) -TEMPLATE = string.Template((THIS_DIR / "template.txt").read_text()) - - -def _prepare_input(text: str) -> str: - if match := FORMATTED_CODE_REGEX.match(text): - return match.group("code") - else: - return text - - -class InvalidLatexError(Exception): - """Represents an error caused by invalid latex.""" - - def __init__(self, logs: str): - super().__init__(logs) - self.logs = logs - - -class Latex(commands.Cog): - """Renders latex.""" - - def __init__(self, bot: Bot): - self.bot = bot - - async def _generate_image(self, query: str, out_file: BinaryIO) -> None: - """Make an API request and save the generated image to cache.""" - payload = {"code": query, "format": "png"} - async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: - response_json = await response.json() - if response_json["status"] != "success": - raise InvalidLatexError(logs=response_json["log"]) - async with self.bot.http_session.get( - f"{LATEX_API_URL}/{response_json['filename']}", - data=payload, - raise_for_status=True - ) as response: - out_file.write(await response.read()) - - async def _upload_to_pastebin(self, text: str) -> Optional[str]: - """Uploads `text` to the paste service, returning the url if successful.""" - try: - async with self.bot.http_session.post( - PASTEBIN_URL + "/documents", - data=text, - raise_for_status=True - ) as response: - response_json = await response.json() - if "key" in response_json: - return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" - except Exception: - # 400 (Bad Request) means there are too many characters - pass - - @commands.command() - @commands.max_concurrency(1, commands.BucketType.guild, wait=True) - async def latex(self, ctx: commands.Context, *, query: str) -> None: - """Renders the text in latex and sends the image.""" - query = _prepare_input(query) - query_hash = hashlib.md5(query.encode()).hexdigest() - image_path = CACHE_DIRECTORY / f"{query_hash}.png" - async with ctx.typing(): - if not image_path.exists(): - try: - with open(image_path, "wb") as out_file: - await self._generate_image(TEMPLATE.substitute(text=query), out_file) - except InvalidLatexError as err: - logs_paste_url = await self._upload_to_pastebin(err.logs) - embed = discord.Embed(title="Failed to render input.") - if logs_paste_url: - embed.description = f"[View Logs]({logs_paste_url})" - else: - embed.description = "Couldn't upload logs." - await ctx.send(embed=embed) - image_path.unlink() - return - await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/template.txt b/bot/exts/fun/latex/template.txt deleted file mode 100644 index a20cc279..00000000 --- a/bot/exts/fun/latex/template.txt +++ /dev/null @@ -1,5 +0,0 @@ -\documentclass{article} -\begin{document} - \pagenumbering{gobble} - $text -\end{document} diff --git a/bot/resources/fun/latex_template.txt b/bot/resources/fun/latex_template.txt new file mode 100644 index 00000000..a20cc279 --- /dev/null +++ b/bot/resources/fun/latex_template.txt @@ -0,0 +1,5 @@ +\documentclass{article} +\begin{document} + \pagenumbering{gobble} + $text +\end{document} -- cgit v1.2.3 From 7ae4792482f2a496a747f3b5892c623f71100f3f Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Wed, 26 Jan 2022 19:44:41 +0530 Subject: paste image on a white background --- bot/exts/fun/latex.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index ac43e95b..80145843 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -1,10 +1,12 @@ import hashlib import re import string +from io import BytesIO from pathlib import Path from typing import BinaryIO, Optional import discord +from PIL import Image from discord.ext import commands from bot.bot import Bot @@ -27,6 +29,9 @@ CACHE_DIRECTORY = THIS_DIR / "_latex_cache" CACHE_DIRECTORY.mkdir(exist_ok=True) TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) +BG_COLOR = (54, 57, 63, 255) +PAD = 10 + def _prepare_input(text: str) -> str: if match := FORMATTED_CODE_REGEX.match(text): @@ -35,6 +40,14 @@ def _prepare_input(text: str) -> str: return text +def _process_image(data: bytes, out_file: BinaryIO) -> None: + image = Image.open(BytesIO(data)).convert("RGBA") + width, height = image.size + background = Image.new("RGBA", (width + 2 * PAD, height + 2 * PAD), "WHITE") + background.paste(image, (PAD, PAD), image) + background.save(out_file) + + class InvalidLatexError(Exception): """Represents an error caused by invalid latex.""" @@ -58,10 +71,9 @@ class Latex(commands.Cog): raise InvalidLatexError(logs=response_json["log"]) async with self.bot.http_session.get( f"{LATEX_API_URL}/{response_json['filename']}", - data=payload, raise_for_status=True ) as response: - out_file.write(await response.read()) + _process_image(await response.read(), out_file) async def _upload_to_pastebin(self, text: str) -> Optional[str]: """Uploads `text` to the paste service, returning the url if successful.""" -- cgit v1.2.3 From 365fe989ee0b659b211ffc503f7ab3ca9641c7c9 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Tue, 1 Feb 2022 10:34:31 +0530 Subject: add comments and docstrings, remove an unused variable --- bot/exts/fun/latex.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index 80145843..1dc9c2e5 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -29,11 +29,11 @@ CACHE_DIRECTORY = THIS_DIR / "_latex_cache" CACHE_DIRECTORY.mkdir(exist_ok=True) TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) -BG_COLOR = (54, 57, 63, 255) PAD = 10 def _prepare_input(text: str) -> str: + """Extract latex from a codeblock, if it is in one.""" if match := FORMATTED_CODE_REGEX.match(text): return match.group("code") else: @@ -41,9 +41,14 @@ def _prepare_input(text: str) -> str: def _process_image(data: bytes, out_file: BinaryIO) -> None: + """Read `data` as an image file, and paste it on a white background.""" image = Image.open(BytesIO(data)).convert("RGBA") width, height = image.size background = Image.new("RGBA", (width + 2 * PAD, height + 2 * PAD), "WHITE") + + # paste the image on the background, using the same image as the mask + # when an RGBA image is passed as the mask, its alpha band is used. + # this has the effect of skipping pasting the pixels where the image is transparent. background.paste(image, (PAD, PAD), image) background.save(out_file) @@ -95,6 +100,8 @@ class Latex(commands.Cog): async def latex(self, ctx: commands.Context, *, query: str) -> None: """Renders the text in latex and sends the image.""" query = _prepare_input(query) + + # the hash of the query is used as the filename in the cache. query_hash = hashlib.md5(query.encode()).hexdigest() image_path = CACHE_DIRECTORY / f"{query_hash}.png" async with ctx.typing(): -- cgit v1.2.3 From 436c9f740cc5002ff8199b57a6b7bc1a778d6b37 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 20 Feb 2022 11:24:51 +0000 Subject: Allow `.src` in dev-contrib and community-meta (#1033) --- bot/constants.py | 9 +++++---- bot/exts/core/error_handler.py | 3 ++- bot/exts/core/source.py | 4 +++- bot/exts/holidays/easter/egg_facts.py | 2 +- bot/exts/holidays/halloween/candy_collection.py | 6 +++--- bot/exts/holidays/halloween/spookynamerate.py | 8 ++++---- bot/exts/holidays/pride/pride_facts.py | 2 +- bot/exts/holidays/pride/pride_leader.py | 2 +- bot/exts/holidays/valentines/be_my_valentine.py | 2 +- bot/exts/holidays/valentines/lovecalculator.py | 4 ++-- bot/utils/checks.py | 2 +- bot/utils/decorators.py | 4 ++-- 12 files changed, 26 insertions(+), 22 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index d39f7361..b4d7bc24 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -109,7 +109,8 @@ class Cats: class Channels(NamedTuple): advent_of_code = int(environ.get("AOC_CHANNEL_ID", 897932085766004786)) advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 897932607545823342)) - bot = 267659945086812160 + bot_commands = 267659945086812160 + community_meta = 267659945086812160 organisation = 551789653284356126 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) dev_contrib = 635950537262759947 @@ -118,7 +119,7 @@ class Channels(NamedTuple): off_topic_0 = 291284109232308226 off_topic_1 = 463035241142026251 off_topic_2 = 463035268514185226 - community_bot_commands = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354)) + sir_lancebot_playground = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354)) voice_chat_0 = 412357430186344448 voice_chat_1 = 799647045886541885 staff_voice = 541638762007101470 @@ -350,8 +351,8 @@ STAFF_ROLES = {Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners} # Whitelisted channels WHITELISTED_CHANNELS = ( - Channels.bot, - Channels.community_bot_commands, + Channels.bot_commands, + Channels.sir_lancebot_playground, Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2, diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py index 676a1e70..983632ba 100644 --- a/bot/exts/core/error_handler.py +++ b/bot/exts/core/error_handler.py @@ -98,7 +98,8 @@ class CommandErrorHandler(commands.Cog): if isinstance(error, commands.NoPrivateMessage): await ctx.send( embed=self.error_embed( - f"This command can only be used in the server. Go to <#{Channels.community_bot_commands}> instead!", + "This command can only be used in the server. " + f"Go to <#{Channels.sir_lancebot_playground}> instead!", NEGATIVE_REPLIES ) ) diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py index 7572ce51..e9568933 100644 --- a/bot/exts/core/source.py +++ b/bot/exts/core/source.py @@ -6,14 +6,16 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Source +from bot.constants import Channels, Source, WHITELISTED_CHANNELS from bot.utils.converters import SourceConverter, SourceType +from bot.utils.decorators import whitelist_override class BotSource(commands.Cog): """Displays information about the bot's source code.""" @commands.command(name="source", aliases=("src",)) + @whitelist_override(channels=WHITELISTED_CHANNELS+[Channels.community_meta, Channels.dev_contrib]) async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None: """Display information and a GitHub link to the source code of a command, tag, or cog.""" if not source_item: diff --git a/bot/exts/holidays/easter/egg_facts.py b/bot/exts/holidays/easter/egg_facts.py index 5f216e0d..152af6a4 100644 --- a/bot/exts/holidays/easter/egg_facts.py +++ b/bot/exts/holidays/easter/egg_facts.py @@ -31,7 +31,7 @@ class EasterFacts(commands.Cog): """A background task that sends an easter egg fact in the event channel everyday.""" await self.bot.wait_until_guild_available() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) await channel.send(embed=self.make_embed()) @commands.command(name="eggfact", aliases=("fact",)) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 729bbc97..220ba8e5 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -55,7 +55,7 @@ class CandyCollection(commands.Cog): if message.author.bot: return # ensure it's hacktober channel - if message.channel.id != Channels.community_bot_commands: + if message.channel.id != Channels.sir_lancebot_playground: return # do random check for skull first as it has the lower chance @@ -77,7 +77,7 @@ class CandyCollection(commands.Cog): return # check to ensure it is in correct channel - if message.channel.id != Channels.community_bot_commands: + if message.channel.id != Channels.sir_lancebot_playground: return # if its not a candy or skull, and it is one of 10 most recent messages, @@ -139,7 +139,7 @@ class CandyCollection(commands.Cog): @property def hacktober_channel(self) -> discord.TextChannel: """Get #hacktoberbot channel from its ID.""" - return self.bot.get_channel(Channels.community_bot_commands) + return self.bot.get_channel(Channels.sir_lancebot_playground) @staticmethod async def send_spook_msg( diff --git a/bot/exts/holidays/halloween/spookynamerate.py b/bot/exts/holidays/halloween/spookynamerate.py index a3aa4f13..02fb71c3 100644 --- a/bot/exts/holidays/halloween/spookynamerate.py +++ b/bot/exts/holidays/halloween/spookynamerate.py @@ -223,7 +223,7 @@ class SpookyNameRate(Cog): if self.first_time: await channel.send( "Okkey... Welcome to the **Spooky Name Rate Game**! It's a relatively simple game.\n" - f"Everyday, a random name will be sent in <#{Channels.community_bot_commands}> " + f"Everyday, a random name will be sent in <#{Channels.sir_lancebot_playground}> " "and you need to try and spookify it!\nRegister your name using " f"`{Client.prefix}spookynamerate add spookified name`" ) @@ -359,10 +359,10 @@ class SpookyNameRate(Cog): """Gets the sir-lancebot-channel after waiting until ready.""" await self.bot.wait_until_ready() channel = self.bot.get_channel( - Channels.community_bot_commands - ) or await self.bot.fetch_channel(Channels.community_bot_commands) + Channels.sir_lancebot_playground + ) or await self.bot.fetch_channel(Channels.sir_lancebot_playground) if not channel: - logger.warning("Bot is unable to get the #seasonalbot-commands channel. Please check the channel ID.") + logger.warning("Bot is unable to get the #sir-lancebot-playground channel. Please check the channel ID.") return channel @staticmethod diff --git a/bot/exts/holidays/pride/pride_facts.py b/bot/exts/holidays/pride/pride_facts.py index e6ef7108..340f0b43 100644 --- a/bot/exts/holidays/pride/pride_facts.py +++ b/bot/exts/holidays/pride/pride_facts.py @@ -30,7 +30,7 @@ class PrideFacts(commands.Cog): """Background task to post the daily pride fact every day.""" await self.bot.wait_until_guild_available() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) await self.send_select_fact(channel, datetime.utcnow()) async def send_random_fact(self, ctx: commands.Context) -> None: diff --git a/bot/exts/holidays/pride/pride_leader.py b/bot/exts/holidays/pride/pride_leader.py index 298c9328..adf01134 100644 --- a/bot/exts/holidays/pride/pride_leader.py +++ b/bot/exts/holidays/pride/pride_leader.py @@ -83,7 +83,7 @@ class PrideLeader(commands.Cog): embed.add_field( name="For More Information", value=f"Do `{constants.Client.prefix}wiki {name}`" - f" in <#{constants.Channels.community_bot_commands}>", + f" in <#{constants.Channels.sir_lancebot_playground}>", inline=False ) embed.set_thumbnail(url=pride_leader["url"]) diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index 1572d474..cbb95157 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -70,7 +70,7 @@ class BeMyValentine(commands.Cog): raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:") emoji_1, emoji_2 = self.random_emoji() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) valentine, title = self.valentine_check(valentine_type) embed = discord.Embed( diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 99fba150..10dea9df 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -32,7 +32,7 @@ class LoveCalculator(Cog): Tells you how much the two love each other. This command requires at least one member as input, if two are given love will be calculated between - those two users, if only one is given, the second member is asusmed to be the invoker. + those two users, if only one is given, the second member is assumed to be the invoker. Members are converted from: - User ID - Mention @@ -51,7 +51,7 @@ class LoveCalculator(Cog): raise BadArgument( "This command can only be ran against members with the lovefest role! " "This role be can assigned by running " - f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot}>." + f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot_commands}>." ) if whom is None: diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 8c426ed7..5433f436 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -33,7 +33,7 @@ def in_whitelist_check( channels: Container[int] = (), categories: Container[int] = (), roles: Container[int] = (), - redirect: Optional[int] = constants.Channels.community_bot_commands, + redirect: Optional[int] = constants.Channels.sir_lancebot_playground, fail_silently: bool = False, ) -> bool: """ diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 7a3b14ad..8954e016 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -257,10 +257,10 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo channels = set(kwargs.get("channels") or {}) categories = kwargs.get("categories") - # Only output override channels + community_bot_commands + # Only output override channels + sir_lancebot_playground if channels: default_whitelist_channels = set(WHITELISTED_CHANNELS) - default_whitelist_channels.discard(Channels.community_bot_commands) + default_whitelist_channels.discard(Channels.sir_lancebot_playground) channels.difference_update(default_whitelist_channels) # Add all whitelisted category channels, but skip if we're in DMs -- cgit v1.2.3