rewrite: Various bug fixes.
This commit is contained in:
@@ -11,7 +11,7 @@ from discord.enums import Locale
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SOURCE_LOCALE = 'en_uk'
|
SOURCE_LOCALE = 'en-GB'
|
||||||
ctx_locale: ContextVar[str] = ContextVar('locale', default=SOURCE_LOCALE)
|
ctx_locale: ContextVar[str] = ContextVar('locale', default=SOURCE_LOCALE)
|
||||||
ctx_translator: ContextVar['LeoBabel'] = ContextVar('translator', default=None) # type: ignore
|
ctx_translator: ContextVar['LeoBabel'] = ContextVar('translator', default=None) # type: ignore
|
||||||
|
|
||||||
|
|||||||
5
src/modules/paradox/__init__.py
Normal file
5
src/modules/paradox/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .cog import ParaCog
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(ParaCog(bot))
|
||||||
215
src/modules/paradox/cog.py
Normal file
215
src/modules/paradox/cog.py
Normal file
@@ -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)
|
||||||
@@ -503,7 +503,7 @@ class TimerCog(LionCog):
|
|||||||
if ctx.guild.me.guild_permissions.manage_channels:
|
if ctx.guild.me.guild_permissions.manage_channels:
|
||||||
try:
|
try:
|
||||||
channel = await ctx.guild.create_voice_channel(
|
channel = await ctx.guild.create_voice_channel(
|
||||||
name="Timer",
|
name=name or "Timer",
|
||||||
reason="Creating Pomodoro Voice Channel",
|
reason="Creating Pomodoro Voice Channel",
|
||||||
category=ctx.channel.category
|
category=ctx.channel.category
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class RankEditor(FastModal):
|
|||||||
|
|
||||||
def role_colour_setup(self):
|
def role_colour_setup(self):
|
||||||
self.role_colour.label = self.bot.translator.t(_p(
|
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"
|
"Role Colour"
|
||||||
))
|
))
|
||||||
self.role_colour.placeholder = self.bot.translator.t(_p(
|
self.role_colour.placeholder = self.bot.translator.t(_p(
|
||||||
@@ -198,19 +198,15 @@ class RankEditor(FastModal):
|
|||||||
))
|
))
|
||||||
self.message.placeholder = t(_p(
|
self.message.placeholder = t(_p(
|
||||||
'ui:rank_editor|input:message|placeholder',
|
'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:
|
if self.rank_type is RankType.VOICE:
|
||||||
# TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key
|
# TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key
|
||||||
msg_default = t(_p(
|
msg_default = t(_p(
|
||||||
'ui:rank_editor|input:message|default|type:voice',
|
'ui:rank_editor|input:message|default|type:voice',
|
||||||
(
|
|
||||||
"Congratulations {user_mention}!\n"
|
"Congratulations {user_mention}!\n"
|
||||||
"For working hard for **{requires}**, you have achieved the rank of "
|
"For working hard for **{requires}**, you have achieved the rank of "
|
||||||
"**{role_name}** in **{guild_name}**! Keep up the good work."
|
"**{role_name}** in **{guild_name}**! Keep up the good work."
|
||||||
)
|
|
||||||
))
|
))
|
||||||
elif self.rank_type is RankType.XP:
|
elif self.rank_type is RankType.XP:
|
||||||
# TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key
|
# TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key
|
||||||
|
|||||||
@@ -216,7 +216,8 @@ class RoomCog(LionCog):
|
|||||||
# TODO: Consider extending invites to members rather than giving them immediate access
|
# 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
|
# Potential for abuse in moderation-free channel a member can add anyone too
|
||||||
everyone_overwrite = discord.PermissionOverwrite(
|
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
|
# Build permission overwrites for owner and members, take into account visible setting
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class RoomUI(MessageUI):
|
|||||||
|
|
||||||
# Input checking
|
# Input checking
|
||||||
response = response.strip()
|
response = response.strip()
|
||||||
if not response.isdigit():
|
if not response.isdigit() or (amount := int(response)) == 0:
|
||||||
await submit.response.send_message(
|
await submit.response.send_message(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
t(_p(
|
t(_p(
|
||||||
@@ -113,7 +113,6 @@ class RoomUI(MessageUI):
|
|||||||
), ephemeral=True
|
), ephemeral=True
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
amount = int(response)
|
|
||||||
await submit.response.defer(thinking=True, ephemeral=True)
|
await submit.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
# Start transaction for deposit
|
# Start transaction for deposit
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ async def get_goals_card(
|
|||||||
# Set and compute correct middle goal column
|
# Set and compute correct middle goal column
|
||||||
if mode in (CardMode.VOICE, CardMode.STUDY):
|
if mode in (CardMode.VOICE, CardMode.STUDY):
|
||||||
model = data.VoiceSessionStats
|
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']
|
middle_goal = goals['study_goal']
|
||||||
elif mode is CardMode.TEXT:
|
elif mode is CardMode.TEXT:
|
||||||
model = TextTrackerData.TextSessions
|
model = TextTrackerData.TextSessions
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ class TasklistCog(LionCog):
|
|||||||
appcmds.Choice(
|
appcmds.Choice(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'argtype:taskid|error:no_matching',
|
'argtype:taskid|error:no_matching',
|
||||||
"No tasks matching {partial}!",
|
"No tasks matching '{partial}'!",
|
||||||
)).format(partial=partial),
|
)).format(partial=partial[:100]),
|
||||||
value=partial
|
value=partial
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
from .test import TestCog
|
# from .test import TestCog
|
||||||
# from .data import test_data
|
# from .data import test_data
|
||||||
|
|
||||||
# bot.db.load_registry(test_data)
|
# bot.db.load_registry(test_data)
|
||||||
await bot.add_cog(TestCog(bot))
|
# await bot.add_cog(TestCog(bot))
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ class VoiceTrackerCog(LionCog):
|
|||||||
VoiceSession._sessions_.pop(guild.id, None)
|
VoiceSession._sessions_.pop(guild.id, None)
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
to_close = [] # (guildid, userid, _at)
|
to_close = [] # (guildid, userid, _at)
|
||||||
for session in sessions.vallues():
|
for session in sessions.values():
|
||||||
if session.start_task is not None:
|
if session.start_task is not None:
|
||||||
session.start_task.cancel()
|
session.start_task.cancel()
|
||||||
if session.expiry_task is not None:
|
if session.expiry_task is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user