feat(stats): New /profile command.

This commit is contained in:
2023-10-09 18:23:10 +03:00
parent d674dc4c8e
commit cfc9ea5ea9
3 changed files with 112 additions and 3 deletions

View File

@@ -8,14 +8,17 @@ from discord import app_commands as appcmds
from discord.ui.button import ButtonStyle from discord.ui.button import ButtonStyle
from meta import LionBot, LionCog, LionContext from meta import LionBot, LionCog, LionContext
from core.lion_guild import VoiceMode
from utils.lib import error_embed from utils.lib import error_embed
from utils.ui import LeoUI, AButton, utc_now from utils.ui import LeoUI, AButton, utc_now
from gui.base import CardMode
from wards import low_management_ward from wards import low_management_ward
from . import babel from . import babel
from .data import StatsData from .data import StatsData
from .ui import ProfileUI, WeeklyMonthlyUI, LeaderboardUI from .ui import ProfileUI, WeeklyMonthlyUI, LeaderboardUI
from .settings import StatisticsSettings, StatisticsConfigUI from .settings import StatisticsSettings, StatisticsConfigUI
from .graphics.profilestats import get_full_profile
_p = babel._p _p = babel._p
@@ -43,7 +46,7 @@ class StatsCog(LionCog):
name=_p('cmd:me', "me"), name=_p('cmd:me', "me"),
description=_p( description=_p(
'cmd:me|desc', 'cmd:me|desc',
"Display your personal profile and summary statistics." "Edit your personal profile and see your statistics."
) )
) )
@appcmds.guild_only @appcmds.guild_only
@@ -53,6 +56,50 @@ class StatsCog(LionCog):
await ui.run(ctx.interaction) await ui.run(ctx.interaction)
await ui.wait() await ui.wait()
@cmds.hybrid_command(
name=_p('cmd:profile', 'profile'),
description=_p(
'cmd:profile|desc',
"Display the target's profile and statistics summary."
)
)
@appcmds.rename(
member=_p('cmd:profile|param:member', "member")
)
@appcmds.describe(
member=_p(
'cmd:profile|param:member|desc', "Member to display profile for."
)
)
@appcmds.guild_only
async def profile_cmd(self, ctx: LionContext, member: Optional[discord.Member] = None):
if not ctx.guild:
return
if not ctx.interaction:
return
member = member if member is not None else ctx.author
if member.bot:
# TODO: Localise
await ctx.reply(
"Bots cannot have profiles!",
ephemeral=True
)
return
await ctx.interaction.response.defer(thinking=True)
# Ensure the lion exists
await self.bot.core.lions.fetch_member(member.guild.id, member.id, member=member)
if ctx.lguild.guild_mode.voice:
mode = CardMode.VOICE
else:
mode = CardMode.TEXT
profile_data = await get_full_profile(self.bot, member.id, member.guild.id, mode)
with profile_data:
file = discord.File(profile_data, 'profile.png')
await ctx.reply(file=file)
@cmds.hybrid_command( @cmds.hybrid_command(
name=_p('cmd:stats', "stats"), name=_p('cmd:stats', "stats"),
description=_p( description=_p(

View File

@@ -17,11 +17,11 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
ranks: Optional[RankCog] = bot.get_cog('RankCog') ranks: Optional[RankCog] = bot.get_cog('RankCog')
stats: Optional[StatsCog] = bot.get_cog('StatsCog') stats: Optional[StatsCog] = bot.get_cog('StatsCog')
if ranks is None or stats is None: if ranks is None or stats is None:
return raise ValueError("Cannot get profile card without ranks and stats cog loaded.")
guild = bot.get_guild(guildid) guild = bot.get_guild(guildid)
if guild is None: if guild is None:
return raise ValueError(f"Cannot get profile card without guild {guildid}")
lion = await bot.core.lions.fetch_member(guildid, userid) lion = await bot.core.lions.fetch_member(guildid, userid)
luser = lion.luser luser = lion.luser

View File

@@ -0,0 +1,62 @@
import asyncio
from io import BytesIO
from PIL import Image
from meta import LionBot
from gui.base import CardMode
from .stats import get_stats_card
from .profile import get_profile_card
card_gap = 10
async def get_full_profile(bot: LionBot, userid: int, guildid: int, mode: CardMode) -> BytesIO:
"""
Render both profile and stats for the target member in the given mode.
Combines the resulting cards into a single image and returns the image data.
"""
# Prepare cards for rendering
get_tasks = (
asyncio.create_task(get_stats_card(bot, userid, guildid, mode), name='get-stats-for-combined'),
asyncio.create_task(get_profile_card(bot, userid, guildid), name='get-profile-for-combined'),
)
stats_card, profile_card = await asyncio.gather(*get_tasks)
# Render cards
render_tasks = (
asyncio.create_task(stats_card.render(), name='render-stats-for-combined'),
asyncio.create_task(profile_card.render(), name='render=profile-for-combined'),
)
# Load the card data into images
stats_data, profile_data = await asyncio.gather(*render_tasks)
with BytesIO(stats_data) as stats_stream, BytesIO(profile_data) as profile_stream:
with Image.open(stats_stream) as stats_image, Image.open(profile_stream) as profile_image:
# Create a new blank image of the correct dimenstions
stats_bbox = stats_image.getbbox(alpha_only=False)
profile_bbox = profile_image.getbbox(alpha_only=False)
if stats_bbox is None or profile_bbox is None:
# Should be impossible, image is already checked by GUI client
raise ValueError("Could not combine, empty stats or profile image.")
combined = Image.new(
'RGBA',
(
max(stats_bbox[2], profile_bbox[2]),
stats_bbox[3] + card_gap + profile_bbox[3]
),
color=None
)
with combined:
combined.alpha_composite(profile_image)
combined.alpha_composite(stats_image, (0, profile_bbox[3] + card_gap))
results = BytesIO()
combined.save(results, format='PNG', compress_type=3, compress_level=1)
results.seek(0)
return results