fixed, fuully bug free

This commit is contained in:
2026-06-17 14:29:02 -05:00
parent c84acc4564
commit 996f65fcab
3 changed files with 390 additions and 267 deletions

145
bot.py
View File

@@ -4,6 +4,8 @@ import os
from dotenv import load_dotenv
from database import Database
import logging
import traceback
import sys
# OC Bot - Coded by Ball Studios 🎨
# A Discord bot for managing and roleplaying as original characters
@@ -11,10 +13,34 @@ import logging
# Load environment variables
load_dotenv()
# Setup logging
logging.basicConfig(level=logging.INFO)
# Setup logging with more verbosity
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Log startup info
logger.info('=' * 60)
logger.info('Starting OC Bot...')
logger.info(f'Python: {sys.version}')
logger.info(f'discord.py: {discord.__version__}')
# Verify required environment variables
required_env_vars = ['DISCORD_TOKEN', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
if missing_vars:
logger.error(f'❌ Missing required environment variables: {", ".join(missing_vars)}')
for var in missing_vars:
logger.error(f' - {var}')
sys.exit(1)
logger.info('✓ All required environment variables found')
logger.info(f'Database Host: {os.getenv("DB_HOST")}')
logger.info(f'Database Name: {os.getenv("DB_NAME")}')
logger.info(f'Database User: {os.getenv("DB_USER")}')
logger.info('=' * 60)
# Bot setup
intents = discord.Intents.default()
intents.message_content = True
@@ -25,26 +51,53 @@ bot = commands.Bot(command_prefix=',', intents=intents)
# Database instance
db = None
@bot.command(name='divide')
async def divide_command(ctx):
"""Sends a Discord GIF link when you type ,divide"""
gif_url = "https://cdn.discordapp.com/attachments/1478216070660755569/1516884782188069057/togif.gif?ex=6a34448a&is=6a32f30a&hm=5f505a453c242673c212999ed8f2b666bf455678b3762312fa3eda31b761fc73&"
await ctx.send(gif_url)
@bot.event
async def on_ready():
global db
logger.info(f'{bot.user} has connected to Discord!')
print('started')
logger.info(f'✓ Discord connection established: {bot.user}')
if db is None:
logger.info('Initializing database connection...')
db = Database()
await db.connect()
try:
await db.connect()
bot.db = db # Make database accessible to cogs
logger.info('✓✓✓ DATABASE CONNECTION SUCCESSFUL ✓✓✓')
except Exception as e:
logger.critical(f'❌❌❌ FAILED TO CONNECT TO DATABASE ❌❌❌')
logger.critical(f'Error: {type(e).__name__}: {str(e)}')
logger.critical(f'Full traceback:\n{traceback.format_exc()}')
logger.critical('Bot cannot function without database. Exiting...')
await bot.close()
return
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}')
if not os.path.exists(cogs_dir):
logger.warning(f'Cogs directory not found: {cogs_dir}')
return
cog_files = [f for f in os.listdir(cogs_dir) if f.endswith('.py') and not f.startswith('_')]
logger.info(f'Found {len(cog_files)} cog(s) to load')
for filename in cog_files:
try:
cog_name = filename[:-3]
await bot.load_extension(f'cogs.{cog_name}')
logger.info(f'✓ Loaded cog: {cog_name}')
except Exception as e:
logger.error(f'❌ Failed to load cog {filename}:')
logger.error(f' {type(e).__name__}: {str(e)}')
logger.error(f' Traceback:\n{traceback.format_exc()}')
@bot.event
async def on_message(message):
@@ -61,23 +114,34 @@ async def on_message(message):
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']:
# ----- ADDED 'divide' TO THE LIST -----
if trigger in ['oc', 'help', 'ping', 'stats', 'invite', 'support', 'about', 'reload', 'debug', 'admin', 'config', 'divide']:
await bot.process_commands(message)
return
# -------------------------------------
# Try to find a character with this trigger
if db:
if db is None:
logger.warning('Database not connected - cannot process character trigger')
return
try:
character = await db.get_character_by_trigger(message.author.id, trigger)
if character:
logger.info(f'Triggered character: {character.get("name", "Unknown")} (trigger: {trigger}, user: {message.author.id})')
# Delete original message
try:
await message.delete()
except:
pass
except Exception as e:
logger.warning(f'Could not delete message: {e}')
# Create webhook and send message
await send_character_message(message.channel, character, msg_content)
return
except Exception as e:
logger.error(f'Error processing character trigger "{trigger}":')
logger.error(f' {type(e).__name__}: {str(e)}')
logger.error(f' Traceback:\n{traceback.format_exc()}')
# Process other commands
await bot.process_commands(message)
@@ -85,6 +149,8 @@ async def on_message(message):
async def send_character_message(channel, character, content):
"""Send a message through webhook as a character"""
try:
logger.info(f'Sending message as {character.get("name")} to {channel}')
# Get existing webhooks
webhooks = await channel.webhooks()
webhook = None
@@ -97,25 +163,58 @@ async def send_character_message(channel, character, content):
# Create webhook if doesn't exist
if not webhook:
logger.info(f'Creating webhook for channel {channel}')
webhook = await channel.create_webhook(name='OC-Bot')
# Send message through webhook
await webhook.send(
# Get avatar URL safely (fallback to None if not present)
avatar_url = character.get('avatar_url') # can be None
# Send message through webhook, wait=True ensures we get a Message object
msg = await webhook.send(
content=content,
username=character['name'],
avatar_url=character['avatar_url']
avatar_url=avatar_url,
wait=True
)
# Defensive check: webhook.send() might return None in some edge cases
if msg is None:
logger.error(f'❌ webhook.send() returned None for character {character["name"]}')
logger.error(f' content: {content[:50]}, avatar_url: {avatar_url}')
return
logger.info(f'✓ Message sent successfully: {msg.id}')
except discord.HTTPException as e:
# Specific HTTP error from Discord API
logger.error(f'❌ HTTP error sending webhook message: {e.status} {e.text}')
except Exception as e:
logger.error(f'Failed to send character message: {e}')
logger.error(f'Failed to send character message:')
logger.error(f' Character: {character.get("name", "Unknown")}')
logger.error(f' Channel: {channel}')
logger.error(f' {type(e).__name__}: {str(e)}')
logger.error(f' Traceback:\n{traceback.format_exc()}')
@bot.event
async def on_command_error(ctx, error):
"""Error handling for commands"""
logger.error(f'Command error: {error}')
logger.error(f'Command error in {ctx.command}:')
logger.error(f' {type(error).__name__}: {str(error)}')
logger.error(f' Traceback:\n{traceback.format_exc()}')
# 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)
logger.critical('DISCORD_TOKEN not found in .env file')
sys.exit(1)
logger.info('Starting bot with Discord token...')
try:
bot.run(TOKEN)
except KeyboardInterrupt:
logger.info('Bot interrupted by user')
except Exception as e:
logger.critical(f'❌ Fatal error running bot:')
logger.critical(f' {type(e).__name__}: {str(e)}')
logger.critical(f' Traceback:\n{traceback.format_exc()}')
sys.exit(1)

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import discord
from discord.ext import commands
@@ -35,25 +36,25 @@ class Help(commands.Cog):
async def show_main_help(self, ctx):
"""Show main help menu"""
embed = discord.Embed(
title='🎭 Original Character Bot',
title='OC Bot - Character Management',
description='Manage and roleplay as your original characters!',
color=discord.Color.blue()
)
embed.add_field(
name='📚 What are OCs?',
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',
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',
name='Commands',
value='**Character Management:**\n'
'`,oc create` - Create a new character\n'
'`,oc list` - List your characters\n'
@@ -68,7 +69,7 @@ class Help(commands.Cog):
)
embed.add_field(
name='💡 Quick Example',
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',
@@ -76,7 +77,7 @@ class Help(commands.Cog):
)
embed.add_field(
name='📝 Help Topics',
name='Help Topics',
value='`,help create` - Character creation guide\n'
'`,help triggers` - How triggers work\n'
'`,help oc` - OC command overview\n'
@@ -85,7 +86,7 @@ class Help(commands.Cog):
inline=False
)
embed.set_footer(text='Coded by Ball Studios 🎨')
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_create(self, ctx):
@@ -110,9 +111,9 @@ class Help(commands.Cog):
embed.add_field(
name='Step-by-step',
value='1. Run `,oc create`\n'
'2. Enter your character\'s name\n'
'2. Enter your character name\n'
'3. Provide an avatar URL (image link)\n'
'4. Choose a unique trigger (e.g., `ba`, `sa`)\n'
'4. Choose a unique trigger (e.g., ba, sa)\n'
'5. Add an optional description\n'
'6. Confirm!',
inline=False
@@ -120,31 +121,31 @@ class Help(commands.Cog):
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',
value='This should be a direct link to an image.\n'
'OK: https://example.com/avatar.png\n'
'NOT OK: Discord user profile link\n'
'NOT OK: 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'
'You will 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!`',
value='Name: Barny\n'
'Avatar: https://example.com/barny.png\n'
'Trigger: ba\n'
'Final command: `,ba Hello everyone!`',
inline=False
)
embed.set_footer(text='Coded by Ball Studios 🎨')
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_triggers(self, ctx):
@@ -162,19 +163,19 @@ class Help(commands.Cog):
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',
value='If your character has trigger ba:\n'
'`,ba Hello!` - Message sent as your character\n'
'`,ba I am 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)',
value='OK: 1-10 characters\n'
'OK: Lowercase letters (a-z)\n'
'OK: Numbers (0-9)\n'
'NOT OK: Reserved words (help, oc, ping, etc.)\n'
'NOT OK: Must be unique to YOU (no duplicates per user)',
inline=False
)
@@ -195,6 +196,7 @@ class Help(commands.Cog):
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_oc(self, ctx):
@@ -236,10 +238,11 @@ class Help(commands.Cog):
embed.add_field(
name='`,oc preview <name>`',
value='See how a character\'s message will look',
value='See how a character message will look',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_edit(self, ctx):
@@ -257,10 +260,10 @@ class Help(commands.Cog):
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',
value='- Name: Your character name\n'
'- Avatar: Their profile picture URL\n'
'- Trigger: Their command code\n'
'- Description: Background info',
inline=False
)
@@ -276,13 +279,13 @@ class Help(commands.Cog):
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',
'- Old trigger becomes unusable\n'
'- New trigger must be unique\n'
'- You will need to use new command immediately',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_delete(self, ctx):
@@ -299,37 +302,28 @@ class Help(commands.Cog):
)
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',
name='Warning',
value='This action cannot be undone!',
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.',
name='How to delete',
value='1. `,oc delete Barny`\n'
'2. Bot asks for confirmation\n'
'3. Type YES (within 30 seconds)\n'
'4. Character is permanently deleted',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def help_list(self, ctx):
"""Help for listing characters"""
"""Help for list command"""
embed = discord.Embed(
title='Listing Your Characters',
color=discord.Color.blue()
title='List Your Characters',
color=discord.Color.blurple()
)
embed.add_field(
@@ -339,30 +333,27 @@ class Help(commands.Cog):
)
embed.add_field(
name='What does it show?',
value='A list of all your characters with their triggers',
name='What it shows',
value='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`',
name='Example output',
value='Character 1: Barny (trigger: ba)\n'
'Character 2: Dragon (trigger: dr)\n'
'Character 3: Shy (trigger: sh)',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
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()
title='Character Info',
color=discord.Color.dark_blue()
)
embed.add_field(
@@ -372,30 +363,26 @@ class Help(commands.Cog):
)
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',
name='What it shows',
value='Name, trigger, avatar URL, and description',
inline=False
)
embed.add_field(
name='Example',
value='`,oc info Barny`',
value='`,oc info Barny`\n\n'
'Shows all details about your Barny character',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
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()
title='Preview Character',
color=discord.Color.teal()
)
embed.add_field(
@@ -405,22 +392,19 @@ class Help(commands.Cog):
)
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',
name='What it does',
value='Shows how a character message will appear',
inline=False
)
embed.add_field(
name='Example',
value='`,oc preview Barny`\n\n'
'Bot sends a sample message with Barny\'s avatar and name',
'Bot sends a sample message with Barny avatar and name',
inline=False
)
embed.set_footer(text='Coded by Ball Studios')
await ctx.send(embed=embed)
async def setup(bot):

View File

@@ -1,9 +1,9 @@
import mysql.connector
from mysql.connector import Error
import aiomysql
import os
from dotenv import load_dotenv
import logging
from datetime import datetime
import traceback
load_dotenv()
logger = logging.getLogger(__name__)
@@ -11,63 +11,77 @@ 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')
db_host = os.getenv('DB_HOST', 'localhost')
db_user = os.getenv('DB_USER', 'root')
db_password = os.getenv('DB_PASSWORD', '')
db_name = os.getenv('DB_NAME', 'rp_bot')
logger.info(f'Attempting to connect to MariaDB at {db_host}:{db_name} as {db_user}')
self.connection = await aiomysql.create_pool(
host=db_host,
user=db_user,
password=db_password,
db=db_name,
minsize=1,
maxsize=10,
autocommit=True
)
self.cursor = self.connection.cursor(dictionary=True)
logger.info('Connected to MariaDB')
logger.info('✓ Successfully connected to MariaDB')
await self.initialize_tables()
except Error as e:
logger.error(f'Database connection error: {e}')
except Exception as e:
error_msg = f'Database connection failed: {type(e).__name__}: {str(e)}'
logger.error(error_msg)
logger.error(f'Full traceback:\n{traceback.format_exc()}')
logger.error(f'Connection details - Host: {os.getenv("DB_HOST")}, User: {os.getenv("DB_USER")}, DB: {os.getenv("DB_NAME")}')
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
)
''')
async with self.connection.acquire() as conn:
async with conn.cursor() as cursor:
# Users table
await 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`)
)
''')
# Characters table
await 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`)
''')
# Index for fast trigger lookups
await 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}')
logger.info('✓ Database tables initialized successfully')
except Exception as e:
logger.error(f'Table initialization error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
raise
async def get_character_by_trigger(self, user_id, trigger):
"""Get character by user_id and trigger (O(1) lookup with caching)"""
@@ -79,68 +93,80 @@ class Database:
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()
async with self.connection.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
'SELECT * FROM `characters` WHERE user_id = %s AND `trigger` = %s',
(user_id, trigger)
)
result = await cursor.fetchone()
if result:
# Cache the result
self.trigger_cache[cache_key] = result['id']
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 result
except Exception as e:
logger.error(f'Get character error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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}')
async with self.connection.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute('SELECT * FROM `characters` WHERE id = %s', (char_id,))
return await cursor.fetchone()
except Exception as e:
logger.error(f'Get character by ID error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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,)
)
async with self.connection.acquire() as conn:
async with conn.cursor() as cursor:
# Ensure user exists
await 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()
# Insert character
await 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)
)
# 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]
# 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}')
logger.info(f'Created character "{name}" with trigger "{trigger}" for user {user_id}')
return cursor.lastrowid
except Exception as e:
logger.error(f'Create character error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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}')
async with self.connection.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
'SELECT * FROM `characters` WHERE user_id = %s ORDER BY created_at DESC',
(user_id,)
)
return await cursor.fetchall()
except Exception as e:
logger.error(f'Get user characters error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
return []
async def update_character(self, char_id, **kwargs):
@@ -152,86 +178,100 @@ class Database:
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()
async with self.connection.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
# Get old trigger for cache clearing
await cursor.execute('SELECT user_id, `trigger` FROM `characters` WHERE id = %s', (char_id,))
old_char = await cursor.fetchone()
set_clause = ', '.join([f'`{k}` = %s' if k == 'trigger' else f'{k} = %s' for k in updates.keys()])
values = list(updates.values()) + [char_id]
set_clause = ', '.join([f'`{k}` = %s' if k == 'trigger' else 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()
await cursor.execute(
f'UPDATE `characters` SET {set_clause} WHERE id = %s',
values
)
# 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]
# 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]
# 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}')
logger.info(f'Updated character {char_id}: {updates}')
return True
except Exception as e:
logger.error(f'Update character error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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()
async with self.connection.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
# Get character info for cache clearing
await cursor.execute('SELECT user_id, `trigger` FROM `characters` WHERE id = %s', (char_id,))
char = await cursor.fetchone()
self.cursor.execute('DELETE FROM `characters` WHERE id = %s', (char_id,))
self.connection.commit()
await cursor.execute('DELETE FROM `characters` WHERE id = %s', (char_id,))
# Clear cache
if char:
cache_key = (char['user_id'], char['trigger'])
if cache_key in self.trigger_cache:
del self.trigger_cache[cache_key]
# 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}')
logger.info(f'Deleted character {char_id}')
return True
except Exception as e:
logger.error(f'Delete character error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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()
async with self.connection.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
'UPDATE `characters` SET last_used = NOW() WHERE id = %s',
(char_id,)
)
return True
except Error as e:
logger.error(f'Update last used error: {e}')
except Exception as e:
logger.error(f'Update last used error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
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}')
async with self.connection.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
'SELECT id FROM `characters` WHERE user_id = %s AND `trigger` = %s',
(user_id, trigger)
)
return await cursor.fetchone() is None
except Exception as e:
logger.error(f'Check trigger availability error: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')
return False
def close(self):
async def close(self):
"""Close database connection"""
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
logger.info('Database connection closed')
try:
if self.connection:
self.connection.close()
await self.connection.wait_closed()
logger.info('Database connection closed')
except Exception as e:
logger.error(f'Error closing database: {type(e).__name__}: {str(e)}')
logger.error(f'Traceback:\n{traceback.format_exc()}')