# cogs/profile.py import discord from discord import app_commands from discord.ext import commands import aiohttp import asyncio import logging import io from database.db import db from services.achievement_service import TITLES from services.profile_service import ProfileService from utils.code_converter import decode_id, encode_id from utils.cdn import DUMP_CHANNEL_ID logger = logging.getLogger(__name__) # The new endpoint on your Forge Space specifically for profiles HF_PROFILE_API_URL = "https://amit9011-gacha-forge.hf.space/generate_profile" class ProfileCog(commands.Cog): def __init__(self, bot): self.bot = bot @app_commands.command(name="suprofile", description="View your massive 8-card showcase grid") @app_commands.describe(user="The player you want to look up") async def profile(self, interaction: discord.Interaction, user: discord.Member = None): await interaction.response.defer(thinking=True) target = user or interaction.user if target.bot: return await interaction.followup.send("Bots don't play gacha games.") # 1. Fetch user data async with db.pool.acquire() as conn: await conn.execute("INSERT INTO users (user_id) VALUES ($1) ON CONFLICT DO NOTHING", target.id) user_data = await conn.fetchrow( "SELECT *, (SELECT COUNT(*) FROM user_characters WHERE user_id = $1) as card_count FROM users WHERE user_id = $1", target.id ) if not user_data: return await interaction.followup.send("This user hasn't started their journey yet.") title_display = f" | {TITLES.get(user_data['equipped_title'], '')} {user_data['equipped_title']}" if user_data['equipped_title'] else "" coins = user_data['coins'] or 0 dust = user_data['dust'] or 0 cards = user_data['card_count'] or 0 embed = discord.Embed( title=f"👤 {target.display_name}'s Showcase{title_display}", description=f"🪙 **{coins:,}** Coins | 🎇 **{dust:,}** Dust | 🎴 **{cards:,}** Cards", color=discord.Color.dark_theme() ) dump_channel = self.bot.get_channel(DUMP_CHANNEL_ID) if not dump_channel: try: dump_channel = await self.bot.fetch_channel(DUMP_CHANNEL_ID) except Exception as e: logger.error(f"Could not fetch Dump Channel: {e}") # ⚡ CDN Cache Check if user_data.get('profile_dump_msg_id') and dump_channel: try: msg = await dump_channel.fetch_message(user_data['profile_dump_msg_id']) if msg.attachments: embed.set_image(url=msg.attachments[0].url) return await interaction.followup.send(embed=embed) except discord.NotFound: logger.warning(f"Profile cache message {user_data['profile_dump_msg_id']} not found. Rebuilding...") except Exception as e: logger.error(f"Failed to fetch profile image cache: {e}") # ⚙️ Rebuild Data Payload for the Forge Space team_data = [] async with db.pool.acquire() as conn: slots = [user_data.get(f'profile_slot_{i}') for i in range(1, 9)] active_ids = [id for id in slots if id is not None] if active_ids: char_rows = await conn.fetch(""" SELECT uc.instance_id, uc.print_num, c.name, c.image_url, c.rarity, c.element, c.custom_frame_url FROM user_characters uc JOIN characters c ON uc.char_id = c.char_id WHERE uc.instance_id = ANY($1::int[]) """, active_ids) row_map = {row['instance_id']: row for row in char_rows} for slot_id in slots: if slot_id and slot_id in row_map: # Send lightweight URLs and data to the Forge, let the Forge download the images team_data.append({ "name": row_map[slot_id]['name'], "print_num": row_map[slot_id]['print_num'], "rarity": row_map[slot_id]['rarity'], "element": row_map[slot_id]['element'] or "None", "image_url": row_map[slot_id]['image_url'], "custom_frame_url": row_map[slot_id]['custom_frame_url'] }) else: team_data.append(None) # 🚀 OFF-LOAD CPU TO THE FORGE SPACE payload = { "username": target.display_name, "team_data": team_data } try: async with aiohttp.ClientSession() as session: async with session.post(HF_PROFILE_API_URL, json=payload, timeout=45) as resp: if resp.status != 200: raise ValueError(f"Forge rejected the profile render: {await resp.text()}") profile_bytes = await resp.read() except Exception as e: logger.error(f"Forge Profile Error: {e}") return await interaction.followup.send("❌ The Cloud Forge is currently overloaded. Please try again in a moment.") file = discord.File(fp=io.BytesIO(profile_bytes), filename="profile.webp") # 📦 Backup to CDN and Cache the Message ID dump_msg = await dump_channel.send(file=file) async with db.pool.acquire() as conn: await conn.execute("UPDATE users SET profile_dump_msg_id = $1 WHERE user_id = $2", dump_msg.id, target.id) embed.set_image(url=dump_msg.attachments[0].url) await interaction.followup.send(embed=embed) display = app_commands.Group(name="sudisplay", description="Manage the 8 display cards on your /profile") @display.command(name="set", description="Equip a cosmetic card to your profile grid") @app_commands.choices(slot=[app_commands.Choice(name=f"Slot {i}", value=i) for i in range(1, 9)]) async def display_set(self, interaction: discord.Interaction, slot: app_commands.Choice[int], card_code: str): await interaction.response.defer(thinking=True) try: instance_id = decode_id(card_code) except ValueError: return await interaction.followup.send("❌ Invalid code.") try: name = await ProfileService.set_display_card(interaction.user.id, slot.value, instance_id) async with db.pool.acquire() as conn: await conn.execute("UPDATE users SET profile_dump_msg_id = NULL WHERE user_id = $1", interaction.user.id) await interaction.followup.send(f"🖼️ **{name}** is now displayed in **Slot {slot.value}**! Your profile image will update shortly.") except ValueError as e: await interaction.followup.send(f"❌ {e}") @display.command(name="remove", description="Remove a cosmetic card from your profile grid") @app_commands.choices(slot=[app_commands.Choice(name=f"Slot {i}", value=i) for i in range(1, 9)]) async def display_remove(self, interaction: discord.Interaction, slot: app_commands.Choice[int]): await interaction.response.defer(thinking=True) try: await ProfileService.remove_display_card(interaction.user.id, slot.value) async with db.pool.acquire() as conn: await conn.execute("UPDATE users SET profile_dump_msg_id = NULL WHERE user_id = $1", interaction.user.id) await interaction.followup.send(f"✅ **Slot {slot.value}** cleared. Your profile image will update shortly.") except ValueError as e: await interaction.followup.send(f"❌ {e}") async def setup(bot): await bot.add_cog(ProfileCog(bot))