Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typst command implementation #1626

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
log/*
data/*
bot/exts/fun/_latex_cache/*
bot/exts/fun/_typst_cache/*



Expand Down
4 changes: 3 additions & 1 deletion bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,6 @@ async def main() -> None:
await _bot.start(constants.Client.token.get_secret_value())


asyncio.run(main())
# the main-guard is needed for launching subprocesses, e.g. via anyio.to_process
if __name__ == "__main__":
asyncio.run(main())
17 changes: 17 additions & 0 deletions bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,23 @@ class _Reddit(EnvConfig, env_prefix="reddit_"):

Reddit = _Reddit()


# "typst_" is the prefix for Typst's own envvars, so "typstext_"
class _Typst(EnvConfig, env_prefix="typstext_"):
# the path to the typst binary that will be used.
# the Typst cog can download it to this path automatically
typst_path: str = "bot/exts/fun/_typst_cache/typst"

# fetching configuration. note that the defaults assume Linux on x86_64

# the direct url to fetch a typst release archive from. It will be unpacked and the executable from it used.
typst_archive_url: str = "https://github.com/typst/typst/releases/download/v0.12.0/typst-x86_64-unknown-linux-musl.tar.xz"
# SHA256 hex digest the archive at typst_archive_url will be checked against. can be obtained by sha256sum
typst_archive_sha256: str = "605130a770ebd59a4a579673079cb913a13e75985231657a71d6239a57539ec3"


Typst = _Typst()

# Default role combinations
MODERATION_ROLES = {Roles.moderation_team, Roles.admins, Roles.owners}
STAFF_ROLES = {Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners}
Expand Down
39 changes: 5 additions & 34 deletions bot/exts/fun/latex.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import hashlib
import os
import re
import string
from io import BytesIO
from pathlib import Path
from typing import BinaryIO

import discord
from PIL import Image
from aiohttp import client_exceptions
from discord.ext import commands
from pydis_core.utils.logging import get_logger
from pydis_core.utils.paste_service import PasteFile, PasteTooLongError, PasteUploadError, send_to_paste_service

from bot.bot import Bot
from bot.constants import Channels, WHITELISTED_CHANNELS
from bot.utils.codeblocks import prepare_input
from bot.utils.decorators import whitelist_override
from bot.utils.images import process_image

log = get_logger(__name__)
FORMATTED_CODE_REGEX = re.compile(
r"(?P<delim>(?P<block>```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block
r"(?(block)(?:(?P<lang>[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<code>.*?)" # 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 = os.getenv("LATEX_API_URL", "https://rtex.probablyaweb.site/api/v2")
PASTEBIN_URL = "https://paste.pythondiscord.com"
Expand All @@ -45,26 +36,6 @@
)


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")
return text


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)


class InvalidLatexError(Exception):
"""Represents an error caused by invalid latex."""

Expand Down Expand Up @@ -97,7 +68,7 @@ async def _generate_image(self, query: str, out_file: BinaryIO) -> None:
f"{LATEX_API_URL}/{response_json['filename']}",
raise_for_status=True
) as response:
_process_image(await response.read(), out_file)
process_image(await response.read(), out_file, PAD)

async def _upload_to_pastebin(self, text: str) -> str | None:
"""Uploads `text` to the paste service, returning the url if successful."""
Expand Down Expand Up @@ -132,7 +103,7 @@ async def _prepare_error_embed(self, err: InvalidLatexError | LatexServerError |
@whitelist_override(channels=LATEX_ALLOWED_CHANNNELS)
async def latex(self, ctx: commands.Context, *, query: str) -> None:
"""Renders the text in latex and sends the image."""
query = _prepare_input(query)
query = prepare_input(query)

# the hash of the query is used as the filename in the cache.
query_hash = hashlib.md5(query.encode()).hexdigest() # noqa: S324
Expand Down
Loading