From fe8cf3588586d59b5f2a8cca774537ac5a8b892f Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 25 May 2023 16:45:54 +0300 Subject: [PATCH] rewrite: Various bug fixes. --- src/babel/translator.py | 2 +- src/modules/paradox/__init__.py | 5 + src/modules/paradox/cog.py | 215 +++++++++++++++++++++++ src/modules/pomodoro/cog.py | 2 +- src/modules/ranks/ui/editor.py | 14 +- src/modules/rooms/cog.py | 3 +- src/modules/rooms/roomui.py | 3 +- src/modules/statistics/graphics/goals.py | 2 +- src/modules/tasklist/cog.py | 6 +- src/modules/test/__init__.py | 4 +- src/tracking/voice/cog.py | 2 +- 11 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 src/modules/paradox/__init__.py create mode 100644 src/modules/paradox/cog.py diff --git a/src/babel/translator.py b/src/babel/translator.py index 7a7b1231..0f8cb778 100644 --- a/src/babel/translator.py +++ b/src/babel/translator.py @@ -11,7 +11,7 @@ from discord.enums import Locale logger = logging.getLogger(__name__) -SOURCE_LOCALE = 'en_uk' +SOURCE_LOCALE = 'en-GB' ctx_locale: ContextVar[str] = ContextVar('locale', default=SOURCE_LOCALE) ctx_translator: ContextVar['LeoBabel'] = ContextVar('translator', default=None) # type: ignore diff --git a/src/modules/paradox/__init__.py b/src/modules/paradox/__init__.py new file mode 100644 index 00000000..4086a429 --- /dev/null +++ b/src/modules/paradox/__init__.py @@ -0,0 +1,5 @@ +from .cog import ParaCog + + +async def setup(bot): + await bot.add_cog(ParaCog(bot)) diff --git a/src/modules/paradox/cog.py b/src/modules/paradox/cog.py new file mode 100644 index 00000000..e875ae3b --- /dev/null +++ b/src/modules/paradox/cog.py @@ -0,0 +1,215 @@ +from typing import Optional +import asyncio +from io import BytesIO + +from PIL import Image + +import discord +from discord.ext import commands as cmds +from discord import app_commands as appcmds + +from discord.ui.button import button + +from utils.lib import error_embed +from utils.ui import LeoUI + +from meta import LionCog, LionBot, LionContext + + +emoji_rotate_cw = "↩️" +emoji_rotate_ccw = "↪️" +emoji_close = "❌" + + +class ParaCog(LionCog): + def __init__(self, bot: LionBot): + self.bot = bot + + @cmds.hybrid_command( + name="quote", + description="Quote a previous message by id from this or another channel." + ) + @appcmds.describe( + message_id="Message id of the message you want to quote." + ) + async def quote_cmd(self, ctx: LionContext, message_id: str): + message_id = message_id.strip() + + if not message_id or not message_id.isdigit(): + await ctx.error_reply("Please provide a valid message id.") + + msgid = int(message_id) + + if ctx.interaction: + await ctx.interaction.response.defer(thinking=True) + + # Look in the current channel + message = None + try: + message = await ctx.channel.fetch_message(msgid) + except discord.HTTPException: + pass + + if message is None: + # Search for the message in other channels + channels = [channel for channel in ctx.guild.text_channels if channel.id != ctx.channel.id] + message = await self.message_seeker(msgid, channels) + + if message is None: + # We couldn't find the message in any of the channels the user could see. + embed = discord.Embed( + title="Message not found!", + colour=discord.Colour.red(), + description=f"Could not find a message in this server with the id `{msgid}`" + ) + else: + content = message.content + header = f"[Click to jump to message]({message.jump_url})" + content = ( + '\n'.join(f"> {line}" for line in message.content.splitlines()) + '\n' + header + ) + embed = discord.Embed( + description=content, + colour=discord.Colour.light_grey(), + timestamp=message.created_at + ) + embed.set_author(name=message.author.name, icon_url=message.author.avatar.url) + embed.set_footer(text=f"Sent in #{message.channel.name}") + + await ctx.interaction.edit_original_response(embed=embed) + + async def message_seeker(self, msgid: int, channels: list[discord.TextChannel]): + tasks = [] + for channel in channels: + task = asyncio.create_task(self.channel_message_seeker(channel, msgid)) + tasks.append(task) + + for coro in asyncio.as_completed(tasks): + result = await coro + if result is None: + continue + else: + for task in tasks: + task.cancel() + return result + + return None + + async def channel_message_seeker(self, channel, msgid): + try: + message = await channel.fetch_message(msgid) + except discord.HTTPException: + return None + else: + return message + + @cmds.hybrid_command( + name='rotate', + description="Rotate an image sent in the last 10 messages." + ) + @appcmds.describe( + angle="Angle to rotate in degrees anticlockwise." + ) + async def rotate_cmd(self, ctx: LionContext, angle: Optional[int] = 90): + await ctx.interaction.response.defer(thinking=True) + + image_url = None + async for message in ctx.channel.history(limit=10): + if ( + message.attachments and + message.attachments[-1].content_type.startswith('image') + ): + image_url = message.attachments[-1].proxy_url + break + + for embed in reversed(message.embeds): + if embed.type == 'image': + image_url = embed.url + break + elif embed.type == 'rich': + if embed.image: + image_url = embed.image.proxy_url + break + + if image_url is None: + await ctx.interaction.edit_original_response( + embed=error_embed("Could not find an image in the last 10 images.") + ) + else: + # We have an image, now rotate it. + async with ctx.bot.web_client.get(image_url) as r: + if r.status == 200: + response = await r.read() + else: + return await ctx.interaction.edit_original_response( + embed=error_embed("Retrieving the previous image failed.") + ) + with Image.open(BytesIO(response)) as im: + ui = RotateUI(im, str(ctx.author.id)) + await ui.run(ctx.interaction, angle) + + +class RotateUI(LeoUI): + def __init__(self, image, name): + super().__init__() + self.original = image + self.filename = name + + self._out_message: Optional[discord.Message] = None + self._rotated: Optional[Image] = None + self._interaction: Optional[discord.Interaction] = None + self._angle = 0 + + @button(emoji=emoji_rotate_ccw) + async def press_ccw(self, interaction, press): + await interaction.response.defer() + self._angle += 90 + await self.update() + + @button(emoji=emoji_close) + async def press_close(self, interaction, press): + await interaction.response.defer() + await self._interaction.delete_original_response() + await self.close() + + @button(emoji=emoji_rotate_cw) + async def press_cw(self, interaction, press): + await interaction.response.defer() + self._angle -= 90 + await self.update() + + async def cleanup(self): + if self.original: + self.original.close() + + async def run(self, interaction, angle: int): + self._angle = angle + self._interaction = interaction + await self.update() + await self.wait() + + async def update(self): + with self._rotate() as rotated: + with BytesIO() as output: + self.save_into(rotated, output) + await self._interaction.edit_original_response( + attachments=[discord.File(output, filename=f"{self.filename}.jpg")], + view=self + ) + + def save_into(self, rotated, output): + exif = self.original.info.get('exif', None) + if exif: + rotated.convert('RGB').save(output, exif=exif, format="JPEG", quality=85, optimize=True) + else: + rotated.convert("RGB").save(output, format="JPEG", quality=85, optimize=True) + output.seek(0) + + def _rotate(self): + """ + Rotate original image by the provided amount. + """ + im = self.original + with im.rotate(self._angle, expand=1) as rotated: + bbox = rotated.getbbox() + return rotated.crop(bbox) diff --git a/src/modules/pomodoro/cog.py b/src/modules/pomodoro/cog.py index 64c6479e..e47d7827 100644 --- a/src/modules/pomodoro/cog.py +++ b/src/modules/pomodoro/cog.py @@ -503,7 +503,7 @@ class TimerCog(LionCog): if ctx.guild.me.guild_permissions.manage_channels: try: channel = await ctx.guild.create_voice_channel( - name="Timer", + name=name or "Timer", reason="Creating Pomodoro Voice Channel", category=ctx.channel.category ) diff --git a/src/modules/ranks/ui/editor.py b/src/modules/ranks/ui/editor.py index e47874e7..5a90de56 100644 --- a/src/modules/ranks/ui/editor.py +++ b/src/modules/ranks/ui/editor.py @@ -49,7 +49,7 @@ class RankEditor(FastModal): def role_colour_setup(self): self.role_colour.label = self.bot.translator.t(_p( - 'ui:rank_editor|input:role_volour|label', + 'ui:rank_editor|input:role_colour|label', "Role Colour" )) self.role_colour.placeholder = self.bot.translator.t(_p( @@ -198,19 +198,15 @@ class RankEditor(FastModal): )) self.message.placeholder = t(_p( 'ui:rank_editor|input:message|placeholder', - ( - "Congratulatory message sent to the user upon achieving this rank." - ) + "Congratulatory message sent to the user upon achieving this rank." )) if self.rank_type is RankType.VOICE: # TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key msg_default = t(_p( 'ui:rank_editor|input:message|default|type:voice', - ( - "Congratulations {user_mention}!\n" - "For working hard for **{requires}**, you have achieved the rank of " - "**{role_name}** in **{guild_name}**! Keep up the good work." - ) + "Congratulations {user_mention}!\n" + "For working hard for **{requires}**, you have achieved the rank of " + "**{role_name}** in **{guild_name}**! Keep up the good work." )) elif self.rank_type is RankType.XP: # TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key diff --git a/src/modules/rooms/cog.py b/src/modules/rooms/cog.py index 25c0e353..7cac51ab 100644 --- a/src/modules/rooms/cog.py +++ b/src/modules/rooms/cog.py @@ -216,7 +216,8 @@ class RoomCog(LionCog): # TODO: Consider extending invites to members rather than giving them immediate access # Potential for abuse in moderation-free channel a member can add anyone too everyone_overwrite = discord.PermissionOverwrite( - view_channel=lguild.config.get(RoomSettings.Visible.setting_id).value + view_channel=lguild.config.get(RoomSettings.Visible.setting_id).value, + connect=False ) # Build permission overwrites for owner and members, take into account visible setting diff --git a/src/modules/rooms/roomui.py b/src/modules/rooms/roomui.py index c4753270..efb808bd 100644 --- a/src/modules/rooms/roomui.py +++ b/src/modules/rooms/roomui.py @@ -103,7 +103,7 @@ class RoomUI(MessageUI): # Input checking response = response.strip() - if not response.isdigit(): + if not response.isdigit() or (amount := int(response)) == 0: await submit.response.send_message( embed=error_embed( t(_p( @@ -113,7 +113,6 @@ class RoomUI(MessageUI): ), ephemeral=True ) return - amount = int(response) await submit.response.defer(thinking=True, ephemeral=True) # Start transaction for deposit diff --git a/src/modules/statistics/graphics/goals.py b/src/modules/statistics/graphics/goals.py index 8e852e94..36ab5662 100644 --- a/src/modules/statistics/graphics/goals.py +++ b/src/modules/statistics/graphics/goals.py @@ -69,7 +69,7 @@ async def get_goals_card( # Set and compute correct middle goal column if mode in (CardMode.VOICE, CardMode.STUDY): model = data.VoiceSessionStats - middle_completed = (await model.study_times_between(guildid or None, userid, start, end))[0] + middle_completed = int((await model.study_times_between(guildid or None, userid, start, end))[0] // 3600) middle_goal = goals['study_goal'] elif mode is CardMode.TEXT: model = TextTrackerData.TextSessions diff --git a/src/modules/tasklist/cog.py b/src/modules/tasklist/cog.py index 51b3c492..937720f9 100644 --- a/src/modules/tasklist/cog.py +++ b/src/modules/tasklist/cog.py @@ -191,8 +191,8 @@ class TasklistCog(LionCog): appcmds.Choice( name=t(_p( 'argtype:taskid|error:no_matching', - "No tasks matching {partial}!", - )).format(partial=partial), + "No tasks matching '{partial}'!", + )).format(partial=partial[:100]), value=partial ) ] @@ -201,7 +201,7 @@ class TasklistCog(LionCog): appcmds.Choice(name=task_string, value=label) for label, task_string in matching ] - return options[:25] + return options[:25] async def is_tasklist_channel(self, channel) -> bool: if not channel.guild: diff --git a/src/modules/test/__init__.py b/src/modules/test/__init__.py index 0e275185..326c9536 100644 --- a/src/modules/test/__init__.py +++ b/src/modules/test/__init__.py @@ -1,9 +1,9 @@ async def setup(bot): - from .test import TestCog + # from .test import TestCog # from .data import test_data # bot.db.load_registry(test_data) - await bot.add_cog(TestCog(bot)) + # await bot.add_cog(TestCog(bot)) pass diff --git a/src/tracking/voice/cog.py b/src/tracking/voice/cog.py index 752aa47d..68fa47ea 100644 --- a/src/tracking/voice/cog.py +++ b/src/tracking/voice/cog.py @@ -590,7 +590,7 @@ class VoiceTrackerCog(LionCog): VoiceSession._sessions_.pop(guild.id, None) now = utc_now() to_close = [] # (guildid, userid, _at) - for session in sessions.vallues(): + for session in sessions.values(): if session.start_task is not None: session.start_task.cancel() if session.expiry_task is not None: