import discord from discord import app_commands from discord.ext import commands from typing import Optional, Literal import config from utils.database import Database from utils.cooldowns import CooldownManager class CommissionModal(discord.ui.Modal, title="Submit a Commission"): """Modal for submitting a new commission request.""" email = discord.ui.TextInput( label="Email", placeholder="your@email.com", required=True, max_length=255, ) name = discord.ui.TextInput( label="Name", placeholder="Your name or preferred alias", required=True, max_length=255, ) commission_type = discord.ui.TextInput( label="Type (art, design, other, unsure)", placeholder="art", required=True, max_length=20, ) description = discord.ui.TextInput( label="Description", placeholder="Describe what you're looking for...", style=discord.TextStyle.paragraph, required=True, max_length=2000, ) budget = discord.ui.TextInput( label="Budget (optional)", placeholder="e.g., $50-100", required=False, max_length=100, ) def __init__(self, cog: "Commissions"): super().__init__() self.cog = cog async def on_submit(self, interaction: discord.Interaction): # Validate commission type valid_types = config.COMMISSION_TYPES c_type = self.commission_type.value.lower().strip() if c_type not in valid_types: await interaction.response.send_message( f"Invalid commission type. Please use one of: {', '.join(valid_types)}", ephemeral=True, ) return # Insert into database query = """ INSERT INTO commissions (discord_user_id, email, name, commission_type, description, budget) VALUES (%s, %s, %s, %s, %s, %s) """ commission_id = await Database.execute_returning_id( query, ( interaction.user.id, self.email.value, self.name.value, c_type, self.description.value, self.budget.value or None, ), ) # Set cooldown self.cog.cooldown_manager.set(interaction.user.id) await interaction.response.send_message( f"Commission submitted! Your commission ID is **#{commission_id}**.\n" "You'll be notified via DM when the status changes.", ephemeral=True, ) # Notify owner try: owner = await interaction.client.fetch_user(config.OWNER_ID) embed = discord.Embed( title="New Commission Submitted", color=discord.Color.green(), ) embed.add_field(name="ID", value=f"#{commission_id}", inline=True) embed.add_field(name="From", value=f"{interaction.user} ({interaction.user.id})", inline=True) embed.add_field(name="Type", value=c_type, inline=True) embed.add_field(name="Email", value=self.email.value, inline=True) embed.add_field(name="Name", value=self.name.value, inline=True) embed.add_field(name="Budget", value=self.budget.value or "Not specified", inline=True) embed.add_field(name="Description", value=self.description.value[:1024], inline=False) await owner.send(embed=embed) except Exception: pass # Don't fail if we can't notify owner class Commissions(commands.Cog): """Commission management commands.""" def __init__(self, bot: commands.Bot): self.bot = bot self.cooldown_manager = CooldownManager() # ============ PUBLIC COMMANDS ============ @app_commands.command(name="queue", description="Check how many commissions are in the queue") @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) @app_commands.allowed_installs(guilds=True, users=True) async def queue(self, interaction: discord.Interaction): query = """ SELECT COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending, COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress FROM commissions """ result = await Database.fetchone(query) pending = result["pending"] if result else 0 in_progress = result["in_progress"] if result else 0 total = pending + in_progress await interaction.response.send_message( f"**Commission Queue:** {total} total ({pending} pending, {in_progress} in progress)" ) @app_commands.command(name="commission", description="Submit a new commission request") @app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True) @app_commands.allowed_installs(guilds=True, users=True) async def commission(self, interaction: discord.Interaction): # Check cooldown remaining = self.cooldown_manager.check(interaction.user.id) if remaining is not None: minutes = remaining // 60 seconds = remaining % 60 await interaction.response.send_message( f"You're on cooldown. Please wait {minutes}m {seconds}s before submitting another commission.", ephemeral=True, ) return await interaction.response.send_modal(CommissionModal(self)) # ============ OWNER COMMANDS ============ commissions_group = app_commands.Group( name="commissions", description="Manage commissions (owner only)", allowed_contexts=app_commands.AppCommandContext(guild=True, dm_channel=True, private_channel=True), allowed_installs=app_commands.AppInstallationType(guild=True, user=True), ) async def _check_owner(self, interaction: discord.Interaction) -> bool: """Check if user is owner, send error if not.""" if interaction.user.id != config.OWNER_ID: await interaction.response.send_message( "You don't have permission to use this command.", ephemeral=True ) return False return True @commissions_group.command(name="list", description="List all commissions") @app_commands.describe(status="Filter by status") async def list_commissions( self, interaction: discord.Interaction, status: Optional[Literal["pending", "accepted", "in_progress", "completed", "rejected"]] = None, ): if not await self._check_owner(interaction): return await interaction.response.defer(ephemeral=True) if status: query = "SELECT id, name, commission_type, status, created_at FROM commissions WHERE status = %s ORDER BY created_at DESC LIMIT 25" commissions = await Database.fetchall(query, (status,)) else: query = "SELECT id, name, commission_type, status, created_at FROM commissions ORDER BY created_at DESC LIMIT 25" commissions = await Database.fetchall(query) if not commissions: await interaction.followup.send("No commissions found.", ephemeral=True) return embed = discord.Embed( title="Commissions" + (f" ({status})" if status else ""), color=discord.Color.blue(), ) for c in commissions: status_emoji = { "pending": ":yellow_circle:", "accepted": ":white_circle:", "in_progress": ":blue_circle:", "completed": ":green_circle:", "rejected": ":red_circle:", }.get(c["status"], ":black_circle:") embed.add_field( name=f"#{c['id']} - {c['name']}", value=f"{status_emoji} {c['status']} | {c['commission_type']} | {c['created_at'].strftime('%Y-%m-%d')}", inline=False, ) await interaction.followup.send(embed=embed, ephemeral=True) @commissions_group.command(name="view", description="View full details of a commission") @app_commands.describe(commission_id="The commission ID to view") async def view_commission(self, interaction: discord.Interaction, commission_id: int): if not await self._check_owner(interaction): return await interaction.response.defer(ephemeral=True) query = "SELECT * FROM commissions WHERE id = %s" c = await Database.fetchone(query, (commission_id,)) if not c: await interaction.followup.send(f"Commission #{commission_id} not found.", ephemeral=True) return embed = discord.Embed( title=f"Commission #{c['id']}", color=discord.Color.blue(), ) embed.add_field(name="Name", value=c["name"], inline=True) embed.add_field(name="Email", value=c["email"], inline=True) embed.add_field(name="Type", value=c["commission_type"], inline=True) embed.add_field(name="Status", value=c["status"], inline=True) embed.add_field(name="Budget", value=c["budget"] or "Not specified", inline=True) if c["discord_user_id"]: embed.add_field(name="Discord ID", value=str(c["discord_user_id"]), inline=True) embed.add_field(name="Description", value=c["description"][:1024], inline=False) if c["rejection_reason"]: embed.add_field(name="Rejection Reason", value=c["rejection_reason"], inline=False) embed.set_footer(text=f"Created: {c['created_at']} | Updated: {c['updated_at']}") await interaction.followup.send(embed=embed, ephemeral=True) async def _update_status( self, interaction: discord.Interaction, commission_id: int, new_status: str, rejection_reason: Optional[str] = None, ): """Update commission status and notify user.""" if not await self._check_owner(interaction): return await interaction.response.defer(ephemeral=True) # Get current commission query = "SELECT * FROM commissions WHERE id = %s" c = await Database.fetchone(query, (commission_id,)) if not c: await interaction.followup.send(f"Commission #{commission_id} not found.", ephemeral=True) return # Update status if rejection_reason: update_query = "UPDATE commissions SET status = %s, rejection_reason = %s WHERE id = %s" await Database.execute(update_query, (new_status, rejection_reason, commission_id)) else: update_query = "UPDATE commissions SET status = %s WHERE id = %s" await Database.execute(update_query, (new_status, commission_id)) await interaction.followup.send( f"Commission #{commission_id} marked as **{new_status}**.", ephemeral=True ) # Notify user via DM if they submitted via Discord if c["discord_user_id"]: try: user = await self.bot.fetch_user(c["discord_user_id"]) status_messages = { "accepted": f"Your commission (#{commission_id}) has been **accepted**!", "in_progress": f"Your commission (#{commission_id}) is now **in progress**!", "completed": f"Your commission (#{commission_id}) has been **completed**!", "rejected": f"Your commission (#{commission_id}) has been **rejected**." + (f"\nReason: {rejection_reason}" if rejection_reason else ""), } message = status_messages.get(new_status, f"Your commission (#{commission_id}) status updated to: {new_status}") await user.send(message) except Exception: pass # Don't fail if we can't DM @commissions_group.command(name="accept", description="Accept a pending commission") @app_commands.describe(commission_id="The commission ID to accept") async def accept_commission(self, interaction: discord.Interaction, commission_id: int): await self._update_status(interaction, commission_id, "accepted") @commissions_group.command(name="progress", description="Mark a commission as in progress") @app_commands.describe(commission_id="The commission ID to mark in progress") async def progress_commission(self, interaction: discord.Interaction, commission_id: int): await self._update_status(interaction, commission_id, "in_progress") @commissions_group.command(name="done", description="Mark a commission as completed") @app_commands.describe(commission_id="The commission ID to mark completed") async def done_commission(self, interaction: discord.Interaction, commission_id: int): await self._update_status(interaction, commission_id, "completed") @commissions_group.command(name="reject", description="Reject a commission") @app_commands.describe(commission_id="The commission ID to reject", reason="Reason for rejection") async def reject_commission( self, interaction: discord.Interaction, commission_id: int, reason: Optional[str] = None ): await self._update_status(interaction, commission_id, "rejected", reason) async def setup(bot: commands.Bot): await bot.add_cog(Commissions(bot))