# cogs/admin.py import discord from discord import app_commands from discord.ext import commands import os import time import aiohttp import urllib.parse import logging import asyncio from database.db import db from utils.image_prep import prepare_database_image logger = logging.getLogger(__name__) class AdminCog(commands.Cog): def __init__(self, bot): self.bot = bot # ========================================================== # 👤 ADD SINGLE CHARACTER # ========================================================== @app_commands.command(name="add_character", description="[ADMIN] Add a character or alternate variant") @app_commands.default_permissions(administrator=True) @app_commands.describe( name="Character Name (e.g. Raiden Shogun)", series="Anime/Game Series", rarity="Rarity (Common, Rare, Epic, Legendary, Mythic)", element="Element type", atk="Base Attack Stat", defense="Base Defense Stat", image="Upload the RAW character image", variant="Optional: Create an alt version (e.g. Summer)" ) @app_commands.choices(rarity=[ app_commands.Choice(name="Common", value="C"), app_commands.Choice(name="Rare", value="R"), app_commands.Choice(name="Epic", value="SR"), app_commands.Choice(name="Legendary", value="SSR"), app_commands.Choice(name="Mythic", value="UR") ]) async def add_character(self, interaction: discord.Interaction, name: str, series: str, rarity: app_commands.Choice[str], element: str, atk: int, defense: int, image: discord.Attachment, variant: str = None): await interaction.response.defer(ephemeral=True) base_name = name.strip() final_name = f"{base_name} ({variant.strip()})" if variant else base_name async with db.pool.acquire() as conn: exists = await conn.fetchval("SELECT 1 FROM characters WHERE name ILIKE $1", final_name) if exists: if variant: return await interaction.followup.send(f"❌ Error: The variant **{final_name}** already exists!", ephemeral=True) else: count = await conn.fetchval("SELECT COUNT(*) FROM characters WHERE name ILIKE $1", f"{base_name} (Alt %)") alt_num = count + 1 if count else 1 final_name = f"{base_name} (Alt {alt_num})" while await conn.fetchval("SELECT 1 FROM characters WHERE name ILIKE $1", final_name): alt_num += 1 final_name = f"{base_name} (Alt {alt_num})" supabase_url = os.getenv("SUPABASE_URL") supabase_key = os.getenv("SUPABASE_KEY") bucket_name = os.getenv("SUPABASE_BUCKET", "gacha-images") if not supabase_url or not supabase_key: return await interaction.followup.send("❌ Error: Supabase keys are missing from your .env file.", ephemeral=True) raw_image_bytes = await image.read() try: optimized_bytes = await asyncio.to_thread(prepare_database_image, raw_image_bytes) safe_filename = f"single_{int(time.time())}_{final_name.replace(' ', '_').lower()}.webp" upload_url = f"{supabase_url}/storage/v1/object/{bucket_name}/{safe_filename}" headers = { "Authorization": f"Bearer {supabase_key}", "apikey": supabase_key, "Content-Type": "image/webp" } async with aiohttp.ClientSession() as session: async with session.post(upload_url, headers=headers, data=optimized_bytes) as resp: if resp.status not in [200, 201]: error_json = await resp.json() logger.error(f"Supabase upload failed: {error_json}") return await interaction.followup.send("❌ Supabase API rejected the upload.", ephemeral=True) public_url = f"{supabase_url}/storage/v1/object/public/{bucket_name}/{safe_filename}" async with db.pool.acquire() as conn: await conn.execute( """ INSERT INTO characters (name, series, rarity, element, atk, def, image_url) VALUES ($1, $2, $3, $4, $5, $6, $7) """, final_name, series, rarity.value, element, atk, defense, public_url ) embed = discord.Embed(title="✅ Character Added Safely!", color=discord.Color.green()) embed.add_field(name="Name", value=final_name, inline=True) embed.add_field(name="Series", value=series, inline=True) embed.add_field(name="Rarity", value=rarity.name, inline=True) embed.set_thumbnail(url=public_url) await interaction.followup.send(embed=embed) except Exception as e: logger.error(f"Add Character Error: {e}", exc_info=True) await interaction.followup.send("❌ An unexpected error occurred.", ephemeral=True) @app_commands.command(name="add_frame", description="[ADMIN] Upload a card frame directly to your bot's assets") @app_commands.default_permissions(administrator=True) @app_commands.choices(rarity=[ app_commands.Choice(name="Common", value="C"), app_commands.Choice(name="Rare", value="R"), app_commands.Choice(name="Epic", value="SR"), app_commands.Choice(name="Legendary", value="SSR"), app_commands.Choice(name="Mythic", value="UR") ]) async def add_frame(self, interaction: discord.Interaction, rarity: app_commands.Choice[str], frame_image: discord.Attachment): await interaction.response.defer(ephemeral=True) if not frame_image.filename.lower().endswith('.png'): return await interaction.followup.send("❌ Error: Frame images MUST be `.png` files.", ephemeral=True) base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) frames_dir = os.path.join(base_dir, "assets", "frames") os.makedirs(frames_dir, exist_ok=True) save_path = os.path.join(frames_dir, f"{rarity.value}.png") try: await frame_image.save(save_path) await interaction.followup.send(f"✅ The **{rarity.name}** frame has been uploaded!") except Exception as e: await interaction.followup.send("❌ Failed to save the frame.", ephemeral=True) async def setup(bot): await bot.add_cog(AdminCog(bot))