| |
| 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__) |
|
|
| |
| 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.") |
|
|
| |
| 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}") |
| |
| |
| 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}") |
|
|
| |
| 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: |
| |
| 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) |
|
|
| |
| 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") |
| |
| |
| 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)) |
|
|