import discord from discord.ext import commands import logging import asyncio logger = logging.getLogger(__name__) RESERVED_TRIGGERS = ['help', 'oc', 'ping', 'stats', 'invite', 'support', 'about', 'reload', 'debug', 'admin', 'config'] class OC(commands.Cog): def __init__(self, bot): self.bot = bot @commands.command(name='oc') async def oc_command(self, ctx): """Base OC command""" await ctx.send('Use `,oc create`, `,oc list`, `,oc edit`, `,oc delete`, `,oc info`, or `,oc preview`\n\nType `,help oc` for more information.') @commands.command(name='oc') async def oc_create(self, ctx): """Start interactive character creation wizard""" if ctx.invoked_subcommand is not None: return await ctx.send('Use `,oc create` to start the wizard.') async def prompt_user(self, ctx, prompt_text, timeout=120): """Helper to get user input with timeout""" await ctx.send(prompt_text) try: msg = await self.bot.wait_for( 'message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=timeout ) return msg.content except asyncio.TimeoutError: await ctx.send('❌ Character creation cancelled (timeout).') return None @oc_create.command(name='create') async def create(self, ctx): """Interactive character creation""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return # Get character name name = await self.prompt_user(ctx, 'What is your character name?') if not name: return # Get avatar URL avatar_url = await self.prompt_user(ctx, 'What avatar URL would you like to use?') if not avatar_url: return # Get trigger while True: trigger = await self.prompt_user(ctx, 'What trigger would you like? (Example: `ba`)') if not trigger: return # Check if reserved if trigger.lower() in RESERVED_TRIGGERS: await ctx.send(f'❌ `{trigger}` is reserved. Please choose another.') continue # Check if available if await db.is_trigger_available(ctx.author.id, trigger): break else: await ctx.send(f'❌ Trigger `,{trigger}` is already in use. Please choose another.') # Get description (optional) await ctx.send('Description? (optional - type `skip` to skip)') try: msg = await self.bot.wait_for( 'message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=120 ) description = '' if msg.content.lower() == 'skip' else msg.content except asyncio.TimeoutError: await ctx.send('❌ Character creation cancelled.') return # Create character char_id = await db.create_character( ctx.author.id, name, trigger, avatar_url, description ) if char_id: embed = discord.Embed( title='✅ Character Created!', color=discord.Color.green() ) embed.add_field(name='Name', value=name, inline=False) embed.add_field(name='Trigger', value=f'`,{trigger}`', inline=False) embed.add_field(name='Avatar', value=f'[Link]({avatar_url})', inline=False) if description: embed.add_field(name='Description', value=description, inline=False) await ctx.send(embed=embed) else: await ctx.send('❌ Failed to create character.') @oc_create.command(name='list') async def list_characters(self, ctx): """List all user's characters""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return characters = await db.get_user_characters(ctx.author.id) if not characters: await ctx.send('You have no characters yet. Use `,oc create` to create one.') return embed = discord.Embed( title='Your Characters', color=discord.Color.blue() ) for char in characters: embed.add_field( name=char['name'], value=f"Trigger: `,{char['trigger']}`", inline=False ) await ctx.send(embed=embed) @oc_create.command(name='info') async def info(self, ctx, *, character_name): """Show character information""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return # Find character by name characters = await db.get_user_characters(ctx.author.id) character = None for char in characters: if char['name'].lower() == character_name.lower(): character = char break if not character: await ctx.send(f'❌ Character `{character_name}` not found.') return embed = discord.Embed( title=character['name'], color=discord.Color.purple() ) embed.add_field(name='Trigger', value=f"`,{character['trigger']}`", inline=False) embed.add_field(name='Avatar', value=f"[Link]({character['avatar_url']})", inline=False) if character['description']: embed.add_field(name='Description', value=character['description'], inline=False) embed.add_field(name='Created', value=character['created_at'], inline=True) embed.add_field(name='Last Used', value=character['last_used'] or 'Never', inline=True) embed.set_thumbnail(url=character['avatar_url']) await ctx.send(embed=embed) @oc_create.command(name='edit') async def edit(self, ctx, character_name=None): """Edit a character""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return characters = await db.get_user_characters(ctx.author.id) if not characters: await ctx.send('You have no characters to edit.') return # If no character specified, show selection if not character_name: embed = discord.Embed( title='Select a character to edit:', color=discord.Color.blue() ) for i, char in enumerate(characters, 1): embed.add_field( name=f'{i}. {char["name"]}', value=f"Trigger: `,{char['trigger']}`", inline=False ) await ctx.send(embed=embed) await ctx.send('Reply with the number of the character you want to edit.') try: msg = await self.bot.wait_for( 'message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=120 ) try: idx = int(msg.content) - 1 if 0 <= idx < len(characters): character = characters[idx] else: await ctx.send('❌ Invalid selection.') return except ValueError: await ctx.send('❌ Invalid input.') return except asyncio.TimeoutError: await ctx.send('❌ Edit cancelled.') return else: # Find by name character = None for char in characters: if char['name'].lower() == character_name.lower(): character = char break if not character: await ctx.send(f'❌ Character `{character_name}` not found.') return # Show edit menu embed = discord.Embed( title=f'Editing: {character["name"]}', color=discord.Color.gold() ) embed.add_field(name='1', value='Edit Name', inline=False) embed.add_field(name='2', value='Edit Avatar', inline=False) embed.add_field(name='3', value='Edit Trigger', inline=False) embed.add_field(name='4', value='Edit Description', inline=False) await ctx.send(embed=embed) try: msg = await self.bot.wait_for( 'message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=120 ) choice = msg.content except asyncio.TimeoutError: await ctx.send('❌ Edit cancelled.') return updates = {} if choice == '1': new_value = await self.prompt_user(ctx, 'New name?') if new_value: updates['name'] = new_value elif choice == '2': new_value = await self.prompt_user(ctx, 'New avatar URL?') if new_value: updates['avatar_url'] = new_value elif choice == '3': while True: new_value = await self.prompt_user(ctx, 'New trigger?') if not new_value: return if new_value.lower() in RESERVED_TRIGGERS: await ctx.send(f'❌ `{new_value}` is reserved.') continue if await db.is_trigger_available(ctx.author.id, new_value): updates['trigger'] = new_value break else: await ctx.send(f'❌ Trigger `,{new_value}` is already in use.') elif choice == '4': new_value = await self.prompt_user(ctx, 'New description?') if new_value: updates['description'] = new_value if updates and await db.update_character(character['id'], **updates): await ctx.send(f'✅ Character updated!') else: await ctx.send('❌ Failed to update character.') @oc_create.command(name='delete') async def delete(self, ctx, *, character_name): """Delete a character""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return characters = await db.get_user_characters(ctx.author.id) character = None for char in characters: if char['name'].lower() == character_name.lower(): character = char break if not character: await ctx.send(f'❌ Character `{character_name}` not found.') return await ctx.send(f'Are you sure you want to delete **{character["name"]}**? Type `YES` within 30 seconds to confirm.') try: msg = await self.bot.wait_for( 'message', check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=30 ) if msg.content.upper() != 'YES': await ctx.send('❌ Deletion cancelled.') return except asyncio.TimeoutError: await ctx.send('❌ Deletion cancelled (timeout).') return if await db.delete_character(character['id']): await ctx.send(f'✅ Character **{character["name"]}** deleted.') else: await ctx.send('❌ Failed to delete character.') @oc_create.command(name='preview') async def preview(self, ctx, *, character_name): """Preview how a character message will look""" db = self.bot.db if hasattr(self.bot, 'db') else None if not db: await ctx.send('❌ Database connection error.') return characters = await db.get_user_characters(ctx.author.id) character = None for char in characters: if char['name'].lower() == character_name.lower(): character = char break if not character: await ctx.send(f'❌ Character `{character_name}` not found.') return await ctx.send('**Preview of how your character message will appear:**') try: webhooks = await ctx.channel.webhooks() webhook = None for wh in webhooks: if wh.user.id == self.bot.user.id: webhook = wh break if not webhook: webhook = await ctx.channel.create_webhook(name='OC-Bot') await webhook.send( content='*This is a preview message*', username=character['name'], avatar_url=character['avatar_url'] ) except Exception as e: logger.error(f'Preview error: {e}') await ctx.send('❌ Failed to create preview.') async def setup(bot): await bot.add_cog(OC(bot))