aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/constants.py9
-rw-r--r--bot/exts/core/error_handler.py3
-rw-r--r--bot/exts/core/source.py4
-rw-r--r--bot/exts/fun/latex.py127
-rw-r--r--bot/exts/holidays/easter/egg_facts.py2
-rw-r--r--bot/exts/holidays/halloween/candy_collection.py6
-rw-r--r--bot/exts/holidays/halloween/spookynamerate.py8
-rw-r--r--bot/exts/holidays/pride/pride_facts.py2
-rw-r--r--bot/exts/holidays/pride/pride_leader.py2
-rw-r--r--bot/exts/holidays/valentines/be_my_valentine.py2
-rw-r--r--bot/exts/holidays/valentines/lovecalculator.py4
-rw-r--r--bot/resources/fun/latex_template.txt5
-rw-r--r--bot/utils/checks.py2
-rw-r--r--bot/utils/decorators.py4
14 files changed, 158 insertions, 22 deletions
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/fun/latex.py b/bot/exts/fun/latex.py
new file mode 100644
index 00000000..1dc9c2e5
--- /dev/null
+++ b/bot/exts/fun/latex.py
@@ -0,0 +1,127 @@
+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
+
+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 = "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())
+
+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:
+ 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."""
+
+ 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']}",
+ raise_for_status=True
+ ) as response:
+ _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."""
+ 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)
+
+ # 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():
+ 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/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/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}
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