| |
| 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 |
|
|
| |
| |
| |
| @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)) |
|
|