First layout
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
.env
|
||||||
|
SETUP.md
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.log
|
||||||
119
README.md
119
README.md
@@ -1,2 +1,119 @@
|
|||||||
# rp-bot
|
# OC Bot - Original Character Management Discord Bot
|
||||||
|
|
||||||
|
A Discord bot for creating and managing original characters (OCs) with a user-friendly prefix-based command system.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Install dependencies**: `pip install -r requirements.txt`
|
||||||
|
2. **Setup MariaDB**: Create a database named `rp_bot`
|
||||||
|
3. **Configure**: Edit `.env` with your Discord token and database credentials
|
||||||
|
4. **Run**: `python bot.py`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✨ **Character Management**
|
||||||
|
- Create, edit, delete, and list original characters
|
||||||
|
- User-scoped triggers (multiple users can use same code)
|
||||||
|
- Avatar customization
|
||||||
|
- Character descriptions and metadata
|
||||||
|
- Last-used tracking
|
||||||
|
|
||||||
|
🎭 **Character Messaging**
|
||||||
|
- Send messages as your characters through webhooks
|
||||||
|
- Automatic message deletion of original command
|
||||||
|
- Character preview system
|
||||||
|
|
||||||
|
📚 **Help System**
|
||||||
|
- Comprehensive beginner-friendly guides
|
||||||
|
- Interactive command assistance
|
||||||
|
- FAQ and troubleshooting
|
||||||
|
|
||||||
|
⚡ **Performance**
|
||||||
|
- O(1) trigger lookup with caching
|
||||||
|
- Database indexes for fast queries
|
||||||
|
- Optimized for thousands of users
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Basic
|
||||||
|
- `,help` - Show help menu
|
||||||
|
- `,ping` - Check bot status
|
||||||
|
- `,stats` - View statistics
|
||||||
|
|
||||||
|
### Character Management
|
||||||
|
- `,oc create` - Create new character (interactive)
|
||||||
|
- `,oc list` - List your characters
|
||||||
|
- `,oc info <name>` - View details
|
||||||
|
- `,oc edit <name>` - Edit character
|
||||||
|
- `,oc delete <name>` - Delete character
|
||||||
|
- `,oc preview <name>` - Preview message
|
||||||
|
|
||||||
|
### Character Messaging
|
||||||
|
```
|
||||||
|
,ba Hello everyone!
|
||||||
|
```
|
||||||
|
Send message as character with trigger 'ba'
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- MariaDB
|
||||||
|
- Discord.py 2.3.2+
|
||||||
|
- MySQL Connector
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
See [SETUP.md](SETUP.md) for detailed installation and configuration guide.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
rp-bot/
|
||||||
|
├── bot.py # Main bot application
|
||||||
|
├── database.py # Database management
|
||||||
|
├── requirements.txt # Dependencies
|
||||||
|
├── .env # Configuration (create this)
|
||||||
|
├── SETUP.md # Setup guide
|
||||||
|
└── cogs/
|
||||||
|
├── oc.py # Character commands
|
||||||
|
├── help.py # Help system
|
||||||
|
├── utils.py # Utility commands
|
||||||
|
└── config.py # Admin commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **User creates character** via `,oc create` with name, avatar, trigger
|
||||||
|
2. **Bot stores in database** with user ID and trigger
|
||||||
|
3. **User sends message** with trigger: `,ba Hello`
|
||||||
|
4. **Bot processes**:
|
||||||
|
- Checks if (user_id, trigger) exists
|
||||||
|
- Deletes original message
|
||||||
|
- Creates/reuses webhook
|
||||||
|
- Sends message as character
|
||||||
|
5. **Others see** message from character avatar/name
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Trigger Lookup**: < 1ms (in-memory cache + DB index)
|
||||||
|
- **Message Send**: ~100-500ms (Discord API)
|
||||||
|
- **Scale**: Supports 10,000+ users, 50,000+ characters
|
||||||
|
|
||||||
|
## Reserved Commands
|
||||||
|
|
||||||
|
These cannot be used as character triggers:
|
||||||
|
```
|
||||||
|
help, oc, ping, stats, invite, support, about, reload, debug, admin, config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- User-specific trigger scoping prevents conflicts
|
||||||
|
- Webhook management automatic and secure
|
||||||
|
- Original messages deleted to prevent confusion
|
||||||
|
- Database role-based access ready
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Check [SETUP.md](SETUP.md) troubleshooting section for common issues.
|
||||||
|
|
||||||
|
|||||||
117
bot.py
Normal file
117
bot.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from database import Database
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Bot setup
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix=',', intents=intents)
|
||||||
|
|
||||||
|
# Database instance
|
||||||
|
db = None
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
global db
|
||||||
|
logger.info(f'{bot.user} has connected to Discord!')
|
||||||
|
if db is None:
|
||||||
|
db = Database()
|
||||||
|
await db.connect()
|
||||||
|
await load_cogs()
|
||||||
|
|
||||||
|
async def load_cogs():
|
||||||
|
"""Load all command cogs"""
|
||||||
|
cogs_dir = 'cogs'
|
||||||
|
for filename in os.listdir(cogs_dir):
|
||||||
|
if filename.endswith('.py') and not filename.startswith('_'):
|
||||||
|
try:
|
||||||
|
await bot.load_extension(f'cogs.{filename[:-3]}')
|
||||||
|
logger.info(f'Loaded cog: {filename}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to load cog {filename}: {e}')
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_message(message):
|
||||||
|
"""Handle character trigger messages"""
|
||||||
|
# Ignore bot messages
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if message starts with prefix
|
||||||
|
if message.content.startswith(','):
|
||||||
|
# Extract trigger and content
|
||||||
|
parts = message.content.split(' ', 1)
|
||||||
|
trigger = parts[0][1:] # Remove the comma
|
||||||
|
msg_content = parts[1] if len(parts) > 1 else ''
|
||||||
|
|
||||||
|
# Check if this is a registered command
|
||||||
|
if trigger in ['oc', 'help', 'ping', 'stats', 'invite', 'support', 'about', 'reload', 'debug', 'admin', 'config']:
|
||||||
|
await bot.process_commands(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try to find a character with this trigger
|
||||||
|
if db:
|
||||||
|
character = await db.get_character_by_trigger(message.author.id, trigger)
|
||||||
|
if character:
|
||||||
|
# Delete original message
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create webhook and send message
|
||||||
|
await send_character_message(message.channel, character, msg_content)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process other commands
|
||||||
|
await bot.process_commands(message)
|
||||||
|
|
||||||
|
async def send_character_message(channel, character, content):
|
||||||
|
"""Send a message through webhook as a character"""
|
||||||
|
try:
|
||||||
|
# Get existing webhooks
|
||||||
|
webhooks = await channel.webhooks()
|
||||||
|
webhook = None
|
||||||
|
|
||||||
|
# Look for existing bot webhook
|
||||||
|
for wh in webhooks:
|
||||||
|
if wh.user.id == bot.user.id:
|
||||||
|
webhook = wh
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create webhook if doesn't exist
|
||||||
|
if not webhook:
|
||||||
|
webhook = await channel.create_webhook(name='OC-Bot')
|
||||||
|
|
||||||
|
# Send message through webhook
|
||||||
|
await webhook.send(
|
||||||
|
content=content,
|
||||||
|
username=character['name'],
|
||||||
|
avatar_url=character['avatar_url']
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to send character message: {e}')
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_command_error(ctx, error):
|
||||||
|
"""Error handling for commands"""
|
||||||
|
logger.error(f'Command error: {error}')
|
||||||
|
|
||||||
|
# Run bot
|
||||||
|
if __name__ == '__main__':
|
||||||
|
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||||
|
if not TOKEN:
|
||||||
|
raise ValueError('DISCORD_TOKEN not found in .env file')
|
||||||
|
bot.run(TOKEN)
|
||||||
1
cogs/__init__.py
Normal file
1
cogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# OC Bot Cogs Package
|
||||||
106
cogs/config.py
Normal file
106
cogs/config.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Config(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
async def is_bot_owner(self, ctx):
|
||||||
|
"""Check if user is bot owner"""
|
||||||
|
return ctx.author.id == self.bot.owner_id or await self.bot.is_owner(ctx.author)
|
||||||
|
|
||||||
|
@commands.command(name='reload')
|
||||||
|
async def reload(self, ctx):
|
||||||
|
"""Reload all cogs (owner only)"""
|
||||||
|
if not await self.is_bot_owner(ctx):
|
||||||
|
await ctx.send('❌ You don\'t have permission to use this command.')
|
||||||
|
return
|
||||||
|
|
||||||
|
cogs_dir = 'cogs'
|
||||||
|
reloaded = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for filename in os.listdir(cogs_dir):
|
||||||
|
if filename.endswith('.py') and not filename.startswith('_'):
|
||||||
|
cog_name = filename[:-3]
|
||||||
|
try:
|
||||||
|
await self.bot.reload_extension(f'cogs.{cog_name}')
|
||||||
|
reloaded.append(cog_name)
|
||||||
|
except Exception as e:
|
||||||
|
failed.append(f'{cog_name}: {str(e)}')
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🔄 Cog Reload',
|
||||||
|
color=discord.Color.green() if not failed else discord.Color.orange()
|
||||||
|
)
|
||||||
|
|
||||||
|
if reloaded:
|
||||||
|
embed.add_field(
|
||||||
|
name='✅ Reloaded',
|
||||||
|
value=', '.join(reloaded),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
embed.add_field(
|
||||||
|
name='❌ Failed',
|
||||||
|
value='\n'.join(failed),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name='debug')
|
||||||
|
async def debug(self, ctx):
|
||||||
|
"""Show debug information (owner only)"""
|
||||||
|
if not await self.is_bot_owner(ctx):
|
||||||
|
await ctx.send('❌ You don\'t have permission to use this command.')
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🔧 Debug Info',
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Bot ID',
|
||||||
|
value=str(self.bot.user.id),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Latency',
|
||||||
|
value=f'{self.bot.latency * 1000:.2f}ms',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Guilds',
|
||||||
|
value=str(len(self.bot.guilds)),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Cogs Loaded',
|
||||||
|
value=str(len(self.bot.cogs)),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(self.bot, 'db') and self.bot.db:
|
||||||
|
embed.add_field(
|
||||||
|
name='Database',
|
||||||
|
value='✅ Connected',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed.add_field(
|
||||||
|
name='Database',
|
||||||
|
value='❌ Not connected',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Config(bot))
|
||||||
425
cogs/help.py
Normal file
425
cogs/help.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
class Help(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name='help')
|
||||||
|
async def help_command(self, ctx, topic=None):
|
||||||
|
"""Show help information"""
|
||||||
|
if not topic:
|
||||||
|
return await self.show_main_help(ctx)
|
||||||
|
|
||||||
|
topic = topic.lower()
|
||||||
|
|
||||||
|
if topic == 'create':
|
||||||
|
return await self.help_create(ctx)
|
||||||
|
elif topic == 'list':
|
||||||
|
return await self.help_list(ctx)
|
||||||
|
elif topic == 'edit':
|
||||||
|
return await self.help_edit(ctx)
|
||||||
|
elif topic == 'delete':
|
||||||
|
return await self.help_delete(ctx)
|
||||||
|
elif topic == 'info':
|
||||||
|
return await self.help_info(ctx)
|
||||||
|
elif topic == 'preview':
|
||||||
|
return await self.help_preview(ctx)
|
||||||
|
elif topic == 'triggers':
|
||||||
|
return await self.help_triggers(ctx)
|
||||||
|
elif topic == 'oc':
|
||||||
|
return await self.help_oc(ctx)
|
||||||
|
else:
|
||||||
|
await ctx.send('Unknown help topic. Use `,help` for available topics.')
|
||||||
|
|
||||||
|
async def show_main_help(self, ctx):
|
||||||
|
"""Show main help menu"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🎭 Original Character Bot - Help Menu',
|
||||||
|
description='Welcome to the OC Bot! Create and manage original characters with ease.',
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='📚 What are OCs?',
|
||||||
|
value='OCs (Original Characters) are fictional characters you create. This bot lets you manage them and send messages as them!',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='🚀 Getting Started',
|
||||||
|
value='`,oc create` - Start creating your first character!\n`,oc list` - View all your characters',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='📖 Commands',
|
||||||
|
value='**Character Management:**\n'
|
||||||
|
'`,oc create` - Create a new character\n'
|
||||||
|
'`,oc list` - List your characters\n'
|
||||||
|
'`,oc info <name>` - View character details\n'
|
||||||
|
'`,oc edit <name>` - Edit a character\n'
|
||||||
|
'`,oc delete <name>` - Delete a character\n'
|
||||||
|
'`,oc preview <name>` - Preview a character message\n\n'
|
||||||
|
'**Help:**\n'
|
||||||
|
'`,help` - Show this menu\n'
|
||||||
|
'`,help <topic>` - Show help for a specific topic',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='💡 Quick Example',
|
||||||
|
value='1. `,oc create` and follow the wizard\n'
|
||||||
|
'2. Use `,ba Hello everyone` to send as your character\n'
|
||||||
|
'3. `,oc list` to see all your characters',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='📝 Help Topics',
|
||||||
|
value='`,help create` - Character creation guide\n'
|
||||||
|
'`,help triggers` - How triggers work\n'
|
||||||
|
'`,help oc` - OC command overview\n'
|
||||||
|
'`,help edit` - Editing guide\n'
|
||||||
|
'`,help delete` - Deletion guide',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_create(self, ctx):
|
||||||
|
"""Help for character creation"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Character Creation Guide',
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc create`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What is it?',
|
||||||
|
value='Starts an interactive wizard to create your first character.',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Step-by-step',
|
||||||
|
value='1. Run `,oc create`\n'
|
||||||
|
'2. Enter your character\'s name\n'
|
||||||
|
'3. Provide an avatar URL (image link)\n'
|
||||||
|
'4. Choose a unique trigger (e.g., `ba`, `sa`)\n'
|
||||||
|
'5. Add an optional description\n'
|
||||||
|
'6. Confirm!',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Avatar URL',
|
||||||
|
value='This should be a direct link to an image:\n'
|
||||||
|
'✅ `https://example.com/avatar.png`\n'
|
||||||
|
'❌ Discord user profile link\n'
|
||||||
|
'❌ Local file path',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Trigger',
|
||||||
|
value='A short code (2-5 chars) that activates your character.\n'
|
||||||
|
'You\'ll use it like: `,ba Hello!`\n'
|
||||||
|
'Must be unique to you (others can have `,ba` too)',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Example',
|
||||||
|
value='* Name: **Barny**\n'
|
||||||
|
'* Avatar: `https://example.com/barny.png`\n'
|
||||||
|
'* Trigger: `ba`\n'
|
||||||
|
'* Final command: `,ba Hello everyone!`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_triggers(self, ctx):
|
||||||
|
"""Help for trigger system"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Trigger System Guide',
|
||||||
|
color=discord.Color.orange()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What is a trigger?',
|
||||||
|
value='A trigger is a short code that activates your character when you use it with the comma prefix.',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='How to use triggers',
|
||||||
|
value='If your character has trigger `ba`:\n'
|
||||||
|
'`,ba Hello!` → Message sent as your character\n'
|
||||||
|
'`,ba I\'m excited!` → Another message as them',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Trigger Rules',
|
||||||
|
value='✅ 1-10 characters\n'
|
||||||
|
'✅ Lowercase letters (a-z)\n'
|
||||||
|
'✅ Numbers (0-9)\n'
|
||||||
|
'❌ Cannot be reserved words (help, oc, ping, etc.)\n'
|
||||||
|
'❌ Must be unique to YOU (no duplicates per user)',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Multiple Users, Same Trigger',
|
||||||
|
value='User A can have `,ba = Barny`\n'
|
||||||
|
'User B can have `,ba = Bob`\n'
|
||||||
|
'User C can have `,ba = Dragon`\n\n'
|
||||||
|
'Bot checks: Your ID + Trigger = Your character',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Reserved Triggers',
|
||||||
|
value='These cannot be used as character triggers:\n'
|
||||||
|
'help, oc, ping, stats, invite, support, about,\n'
|
||||||
|
'reload, debug, admin, config',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_oc(self, ctx):
|
||||||
|
"""Help for oc command"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='OC Command Overview',
|
||||||
|
color=discord.Color.purple()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc create`',
|
||||||
|
value='Start an interactive wizard to create a character',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc list`',
|
||||||
|
value='See all your characters and their triggers',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc info <name>`',
|
||||||
|
value='View detailed information about a character',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc edit <name>`',
|
||||||
|
value='Modify character details (name, avatar, trigger, description)',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc delete <name>`',
|
||||||
|
value='Delete a character (requires confirmation)',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='`,oc preview <name>`',
|
||||||
|
value='See how a character\'s message will look',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_edit(self, ctx):
|
||||||
|
"""Help for editing"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Editing Characters',
|
||||||
|
color=discord.Color.gold()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc edit <character_name>`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What can you edit?',
|
||||||
|
value='• **Name** - Your character\'s name\n'
|
||||||
|
'• **Avatar** - Their profile picture URL\n'
|
||||||
|
'• **Trigger** - Their command code\n'
|
||||||
|
'• **Description** - Background info',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='How to edit',
|
||||||
|
value='1. `,oc edit Barny`\n'
|
||||||
|
'2. Choose what to edit (number 1-4)\n'
|
||||||
|
'3. Provide new value\n'
|
||||||
|
'4. Done!',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Trigger Change Tips',
|
||||||
|
value='When changing a trigger:\n'
|
||||||
|
'• Old trigger becomes unusable\n'
|
||||||
|
'• New trigger must be unique\n'
|
||||||
|
'• Use new trigger immediately\n'
|
||||||
|
'• E.g., if `,ba` → `,bn`, use `,bn` now',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_delete(self, ctx):
|
||||||
|
"""Help for deletion"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Deleting Characters',
|
||||||
|
color=discord.Color.red()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc delete <character_name>`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What happens?',
|
||||||
|
value='1. Bot asks for confirmation\n'
|
||||||
|
'2. Type `YES` within 30 seconds\n'
|
||||||
|
'3. Character is permanently deleted\n'
|
||||||
|
'4. Trigger becomes available again',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='⚠️ Warning',
|
||||||
|
value='Deletion is **permanent**! You cannot undo this.\n'
|
||||||
|
'Create a backup/description if needed.',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Example',
|
||||||
|
value='User: `,oc delete Barny`\n'
|
||||||
|
'Bot: Are you sure? Type YES to confirm.\n'
|
||||||
|
'User: `YES`\n'
|
||||||
|
'Bot: Character Barny deleted.',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_list(self, ctx):
|
||||||
|
"""Help for listing characters"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Listing Your Characters',
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc list`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What does it show?',
|
||||||
|
value='A list of all your characters with their triggers',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Example Output',
|
||||||
|
value='**Your Characters**\n'
|
||||||
|
'Barny\n'
|
||||||
|
'Trigger: `,ba`\n\n'
|
||||||
|
'Sarah\n'
|
||||||
|
'Trigger: `,sa`\n\n'
|
||||||
|
'Knight\n'
|
||||||
|
'Trigger: `,kn`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_info(self, ctx):
|
||||||
|
"""Help for info command"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Character Information',
|
||||||
|
color=discord.Color.blurple()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc info <character_name>`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What does it show?',
|
||||||
|
value='Detailed information about a character:\n'
|
||||||
|
'• Name\n'
|
||||||
|
'• Trigger code\n'
|
||||||
|
'• Avatar image\n'
|
||||||
|
'• Description\n'
|
||||||
|
'• Creation date\n'
|
||||||
|
'• Last time used',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Example',
|
||||||
|
value='`,oc info Barny`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def help_preview(self, ctx):
|
||||||
|
"""Help for preview command"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='Character Preview',
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Command',
|
||||||
|
value='`,oc preview <character_name>`',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='What does it do?',
|
||||||
|
value='Shows you exactly how your character will appear when sending messages.\n'
|
||||||
|
'Useful to check:\n'
|
||||||
|
'• Avatar displays correctly\n'
|
||||||
|
'• Name looks good\n'
|
||||||
|
'• Everything works as expected',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Example',
|
||||||
|
value='`,oc preview Barny`\n\n'
|
||||||
|
'Bot sends a sample message with Barny\'s avatar and name',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Help(bot))
|
||||||
377
cogs/oc.py
Normal file
377
cogs/oc.py
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
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))
|
||||||
119
cogs/utils.py
Normal file
119
cogs/utils.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import platform
|
||||||
|
|
||||||
|
class Utils(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(name='ping')
|
||||||
|
async def ping(self, ctx):
|
||||||
|
"""Check bot latency"""
|
||||||
|
latency = self.bot.latency * 1000
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🏓 Pong!',
|
||||||
|
description=f'Latency: {latency:.2f}ms',
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name='stats')
|
||||||
|
async def stats(self, ctx):
|
||||||
|
"""Show bot statistics"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='📊 Bot Statistics',
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Latency',
|
||||||
|
value=f'{self.bot.latency * 1000:.2f}ms',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Servers',
|
||||||
|
value=str(len(self.bot.guilds)),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Python Version',
|
||||||
|
value=platform.python_version(),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='discord.py Version',
|
||||||
|
value=discord.__version__,
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name='invite')
|
||||||
|
async def invite(self, ctx):
|
||||||
|
"""Get bot invite link"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🔗 Invite OC Bot',
|
||||||
|
description='Use this link to invite the bot to your server:',
|
||||||
|
color=discord.Color.blurple()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate invite URL with proper permissions
|
||||||
|
permissions = discord.Permissions(
|
||||||
|
send_messages=True,
|
||||||
|
manage_messages=True,
|
||||||
|
manage_webhooks=True,
|
||||||
|
read_message_history=True,
|
||||||
|
embed_links=True
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_url = discord.utils.oauth_url(
|
||||||
|
self.bot.user.id,
|
||||||
|
permissions=permissions
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Invite Link',
|
||||||
|
value=f'[Click here]({invite_url})',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name='about')
|
||||||
|
async def about(self, ctx):
|
||||||
|
"""About the bot"""
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🎭 About OC Bot',
|
||||||
|
description='A bot for managing and roleplaying as your original characters',
|
||||||
|
color=discord.Color.purple()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Features',
|
||||||
|
value='✅ Create and manage original characters\n'
|
||||||
|
'✅ Send messages as your characters\n'
|
||||||
|
'✅ Customize avatars and descriptions\n'
|
||||||
|
'✅ User-specific triggers\n'
|
||||||
|
'✅ Full character management',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Prefix',
|
||||||
|
value='`,` (comma)',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name='Help',
|
||||||
|
value='Use `,help` to get started',
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
await bot.add_cog(Utils(bot))
|
||||||
237
database.py
Normal file
237
database.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import Error
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self):
|
||||||
|
self.connection = None
|
||||||
|
self.cursor = None
|
||||||
|
self.trigger_cache = {} # Cache for (user_id, trigger) -> character_id lookups
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Connect to MariaDB"""
|
||||||
|
try:
|
||||||
|
self.connection = mysql.connector.connect(
|
||||||
|
host=os.getenv('DB_HOST', 'localhost'),
|
||||||
|
user=os.getenv('DB_USER', 'root'),
|
||||||
|
password=os.getenv('DB_PASSWORD', ''),
|
||||||
|
database=os.getenv('DB_NAME', 'rp_bot')
|
||||||
|
)
|
||||||
|
self.cursor = self.connection.cursor(dictionary=True)
|
||||||
|
logger.info('Connected to MariaDB')
|
||||||
|
await self.initialize_tables()
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Database connection error: {e}')
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def initialize_tables(self):
|
||||||
|
"""Create necessary tables if they don't exist"""
|
||||||
|
try:
|
||||||
|
# Users table
|
||||||
|
self.cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
user_id BIGINT PRIMARY KEY,
|
||||||
|
username VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Characters table
|
||||||
|
self.cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS characters (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
trigger VARCHAR(100) NOT NULL,
|
||||||
|
avatar_url TEXT,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(user_id),
|
||||||
|
UNIQUE KEY unique_user_trigger (user_id, trigger)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Index for fast trigger lookups
|
||||||
|
self.cursor.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_trigger
|
||||||
|
ON characters(user_id, trigger)
|
||||||
|
''')
|
||||||
|
|
||||||
|
self.connection.commit()
|
||||||
|
logger.info('Database tables initialized')
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Table initialization error: {e}')
|
||||||
|
|
||||||
|
async def get_character_by_trigger(self, user_id, trigger):
|
||||||
|
"""Get character by user_id and trigger (O(1) lookup with caching)"""
|
||||||
|
cache_key = (user_id, trigger)
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
if cache_key in self.trigger_cache:
|
||||||
|
char_id = self.trigger_cache[cache_key]
|
||||||
|
return await self.get_character_by_id(char_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
'SELECT * FROM characters WHERE user_id = %s AND trigger = %s',
|
||||||
|
(user_id, trigger)
|
||||||
|
)
|
||||||
|
result = self.cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
# Cache the result
|
||||||
|
self.trigger_cache[cache_key] = result['id']
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Get character error: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_character_by_id(self, char_id):
|
||||||
|
"""Get character by ID"""
|
||||||
|
try:
|
||||||
|
self.cursor.execute('SELECT * FROM characters WHERE id = %s', (char_id,))
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Get character by ID error: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def create_character(self, user_id, name, trigger, avatar_url, description=''):
|
||||||
|
"""Create a new character"""
|
||||||
|
try:
|
||||||
|
# Ensure user exists
|
||||||
|
self.cursor.execute(
|
||||||
|
'INSERT IGNORE INTO users (user_id) VALUES (%s)',
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert character
|
||||||
|
self.cursor.execute(
|
||||||
|
'''INSERT INTO characters
|
||||||
|
(user_id, name, trigger, avatar_url, description)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)''',
|
||||||
|
(user_id, name, trigger, avatar_url, description)
|
||||||
|
)
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
# Clear cache for this user's trigger
|
||||||
|
cache_key = (user_id, trigger)
|
||||||
|
if cache_key in self.trigger_cache:
|
||||||
|
del self.trigger_cache[cache_key]
|
||||||
|
|
||||||
|
return self.cursor.lastrowid
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Create character error: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_characters(self, user_id):
|
||||||
|
"""Get all characters for a user"""
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
'SELECT * FROM characters WHERE user_id = %s ORDER BY created_at DESC',
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
return self.cursor.fetchall()
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Get user characters error: {e}')
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def update_character(self, char_id, **kwargs):
|
||||||
|
"""Update character fields"""
|
||||||
|
try:
|
||||||
|
allowed_fields = ['name', 'trigger', 'avatar_url', 'description']
|
||||||
|
updates = {k: v for k, v in kwargs.items() if k in allowed_fields}
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get old trigger for cache clearing
|
||||||
|
self.cursor.execute('SELECT user_id, trigger FROM characters WHERE id = %s', (char_id,))
|
||||||
|
old_char = self.cursor.fetchone()
|
||||||
|
|
||||||
|
set_clause = ', '.join([f'{k} = %s' for k in updates.keys()])
|
||||||
|
values = list(updates.values()) + [char_id]
|
||||||
|
|
||||||
|
self.cursor.execute(
|
||||||
|
f'UPDATE characters SET {set_clause} WHERE id = %s',
|
||||||
|
values
|
||||||
|
)
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
if old_char:
|
||||||
|
old_cache_key = (old_char['user_id'], old_char['trigger'])
|
||||||
|
if old_cache_key in self.trigger_cache:
|
||||||
|
del self.trigger_cache[old_cache_key]
|
||||||
|
|
||||||
|
# If trigger changed, clear new trigger cache too
|
||||||
|
if 'trigger' in updates:
|
||||||
|
new_cache_key = (old_char['user_id'], updates['trigger'])
|
||||||
|
if new_cache_key in self.trigger_cache:
|
||||||
|
del self.trigger_cache[new_cache_key]
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Update character error: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def delete_character(self, char_id):
|
||||||
|
"""Delete a character"""
|
||||||
|
try:
|
||||||
|
# Get character info for cache clearing
|
||||||
|
self.cursor.execute('SELECT user_id, trigger FROM characters WHERE id = %s', (char_id,))
|
||||||
|
char = self.cursor.fetchone()
|
||||||
|
|
||||||
|
self.cursor.execute('DELETE FROM characters WHERE id = %s', (char_id,))
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
if char:
|
||||||
|
cache_key = (char['user_id'], char['trigger'])
|
||||||
|
if cache_key in self.trigger_cache:
|
||||||
|
del self.trigger_cache[cache_key]
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Delete character error: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def update_last_used(self, char_id):
|
||||||
|
"""Update last_used timestamp for a character"""
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
'UPDATE characters SET last_used = NOW() WHERE id = %s',
|
||||||
|
(char_id,)
|
||||||
|
)
|
||||||
|
self.connection.commit()
|
||||||
|
return True
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Update last used error: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def is_trigger_available(self, user_id, trigger):
|
||||||
|
"""Check if trigger is available for a user"""
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
'SELECT id FROM characters WHERE user_id = %s AND trigger = %s',
|
||||||
|
(user_id, trigger)
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone() is None
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f'Check trigger availability error: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close database connection"""
|
||||||
|
if self.cursor:
|
||||||
|
self.cursor.close()
|
||||||
|
if self.connection:
|
||||||
|
self.connection.close()
|
||||||
|
logger.info('Database connection closed')
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
discord.py==2.3.2
|
||||||
|
mysql-connector-python==8.2.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
Reference in New Issue
Block a user