- Architecture Overview
- Project Structure
- Database Schema
- Module System
- Configuration Management
- Deployment & Setup
- Development Guidelines
- API References
- Troubleshooting
- Modular Architecture: Clean separation of concerns with independent modules
- Database-Driven: SQLite with async operations for persistence
- Permission-Based: Hierarchical permission system with cross-server support
- Event-Driven: Background tasks for monitoring and automation
- Error-Resilient: Comprehensive error handling and logging
βββ Discord.py 2.3.2+ # Discord API wrapper
βββ Wavelink 3.4.1+ # Lavalink client for music
βββ aiosqlite 0.19.0+ # Async SQLite operations
βββ python-dotenv 1.0.0+ # Environment configuration
βββ aiohttp 3.9.0+ # HTTP client for API calls
βββ typing-extensions # Enhanced type hints
graph TD
A[Discord Event] --> B[Command Processing]
B --> C[Permission Check]
C --> D[Module Router]
D --> E[Business Logic]
E --> F[Database Operation]
F --> G[Response Generation]
G --> H[Discord Response]
RatArt/
βββ main.py # Application entry point
βββ requirements.txt # Python dependencies
βββ .env # Environment configuration
βββ config/
β βββ __init__.py
β βββ settings.py # Configuration management
βββ database/
β βββ __init__.py
β βββ models.py # Database models and operations
β βββ ratart.db # SQLite database (auto-created)
βββ modules/
β βββ __init__.py
β βββ base_module.py # Abstract base module
β βββ admin/
β β βββ __init__.py
β β βββ admin_module.py # Admin commands
β βββ core/
β β βββ __init__.py
β β βββ help_module.py # Help system
β βββ music/
β βββ __init__.py
β βββ music_module.py # Music playback
βββ utils/
β βββ __init__.py
β βββ decorators.py # Permission decorators
β βββ embeds.py # Unified embed system
β βββ logging_config.py # Logging setup
βββ docs/
βββ user-guide.html # End-user documentation
βββ DEVELOPER_GUIDE.md # This file
CREATE TABLE users (
user_id INTEGER PRIMARY KEY, -- Discord user ID
username TEXT, -- Display name for reference
permission_level INTEGER DEFAULT 0, -- 0=User, 1=Mod, 2=Admin, 3=Owner
added_by INTEGER, -- Who granted permissions
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1 -- Soft delete flag
);CREATE TABLE guild_settings (
guild_id INTEGER PRIMARY KEY, -- Discord guild ID
guild_name TEXT, -- Server name for reference
prefix TEXT DEFAULT '!', -- Command prefix
auto_disconnect_minutes INTEGER DEFAULT 10, -- Auto-disconnect timer
max_queue_size INTEGER DEFAULT 100, -- Queue size limit
volume INTEGER DEFAULT 100, -- Default volume
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);CREATE TABLE blacklisted_users (
user_id INTEGER PRIMARY KEY, -- Discord user ID
username TEXT, -- Display name
reason TEXT, -- Blacklist reason
blacklisted_by INTEGER, -- Admin who blacklisted
blacklisted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1 -- Soft delete flag
);CREATE TABLE tracked_users (
guild_id INTEGER PRIMARY KEY, -- One tracked user per guild
user_id INTEGER NOT NULL, -- Discord user ID being tracked
username TEXT, -- Display name
tracked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1 -- Active tracking flag
);# Add/update user permissions
await db.add_user(user_id: int, username: str, level: PermissionLevel, added_by: int) -> bool
# Get user permission level
await db.get_user_permission(user_id: int) -> PermissionLevel
# Remove user permissions
await db.remove_user(user_id: int) -> bool
# List all admin users
await db.get_admin_users() -> List[Dict]# Blacklist a user
await db.add_blacklisted_user(user_id: int, username: str, reason: str, blacklisted_by: int) -> bool
# Check if user is blacklisted
await db.is_user_blacklisted(user_id: int) -> bool
# Remove from blacklist
await db.remove_blacklisted_user(user_id: int) -> bool
# Get all blacklisted users
await db.get_all_blacklisted_users() -> List[Dict]# Set tracked user for guild
await db.set_tracked_user(guild_id: int, user_id: int, username: str) -> bool
# Get currently tracked user
await db.get_tracked_user(guild_id: int) -> Dict | None
# Remove tracking
await db.remove_tracked_user(guild_id: int) -> boolAll modules inherit from BaseModule:
from modules.base_module import BaseModule
from abc import abstractmethod
class BaseModule(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = logging.getLogger(f'modules.{self.__class__.__name__}')
@property
@abstractmethod
def module_name(self) -> str:
"""Human-readable module name"""
pass
@property
@abstractmethod
def description(self) -> str:
"""Module description"""
pass
async def _setup(self):
"""Override for module-specific setup"""
pass
async def _teardown(self):
"""Override for module-specific cleanup"""
passModules are automatically loaded in main.py:
class RatArtBot(commands.Bot):
async def load_modules(self):
"""Load all bot modules"""
modules = [
('modules.admin.admin_module', 'AdminModule'),
('modules.core.help_module', 'HelpModule'),
('modules.music.music_module', 'MusicModule'),
]
for module_path, class_name in modules:
try:
await self.load_extension(module_path)
self.logger.info(f"Loaded module: {class_name}")
except Exception as e:
self.logger.error(f"Failed to load {class_name}: {e}")- Create module directory:
modules/your_module/ - Create
__init__.py: Empty file for Python package - Create module file:
your_module_module.py - Implement BaseModule:
from modules.base_module import BaseModule
from discord.ext import commands
class YourModule(BaseModule):
@property
def module_name(self) -> str:
return "Your Module"
@property
def description(self) -> str:
return "Module description"
async def _setup(self):
"""Module initialization"""
pass
@commands.command()
async def your_command(self, ctx):
"""Your command implementation"""
pass
async def setup(bot):
await bot.add_cog(YourModule(bot))- Register in main.py: Add to modules list
- Update help system: Add commands to
help_module.py
# Discord Configuration
DISCORD_TOKEN=your_bot_token_here
DISCORD_APPLICATION_ID=your_application_id
DISCORD_PREFIX=!
# Lavalink Configuration
LAVALINK_URI=http://localhost:2333
LAVALINK_PASSWORD=youshallnotpass
# Database Configuration
DATABASE_PATH=database/ratart.db
# Feature Flags
FEATURES_AUTO_DISCONNECT=true
FEATURES_ACTIVITY_TRACKING=true
# Music Configuration
MUSIC_AUTO_DISCONNECT_MINUTES=10
MUSIC_MAX_QUEUE_SIZE=100
MUSIC_DEFAULT_VOLUME=100
MUSIC_TRACKS_PER_PAGE=10The BotConfig class in config/settings.py handles configuration:
class BotConfig:
def __init__(self):
load_dotenv()
self.config = {}
self._load_config()
def get(self, key: str, default=None):
"""Get configuration value with dot notation"""
keys = key.split('.')
value = self.config
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
@property
def discord_token(self) -> str:
return os.getenv('DISCORD_TOKEN')
@property
def lavalink_uri(self) -> str:
return os.getenv('LAVALINK_URI', 'http://localhost:2333')
# ... additional properties- Add to .env: Define the environment variable
- Add property to BotConfig: Create getter method
- Update validation: Add to
_validate_config() - Document: Update this guide and user docs
- Python 3.11+
- Discord Bot Token
- Lavalink Server
- SQLite (included with Python)
- Clone repository:
git clone <repository-url>
cd RatArt- Create virtual environment:
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate- Install dependencies:
pip install -r requirements.txt- Configure environment:
cp .env.example .env
# Edit .env with your configuration-
Set up Lavalink:
- Download Lavalink.jar
- Configure
application.yml - Start Lavalink server
-
Run the bot:
python main.py- Create Application: Discord Developer Portal
- Create Bot: Get bot token
- Set Intents: Enable required intents
- Message Content Intent
- Server Members Intent
- Presence Intent
- Invite Bot: Generate invite URL with required permissions
βββ View Channels
βββ Send Messages
βββ Embed Links
βββ Read Message History
βββ Use External Emojis
βββ Add Reactions
βββ Connect (Voice)
βββ Speak (Voice)
βββ Use Voice Activity
βββ Manage Roles (for cross-server role management)
Create application.yml for Lavalink:
server:
port: 2333
address: 0.0.0.0
lavalink:
plugins:
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.0.1"
repository: "https://maven.topi.wtf/releases"
server:
password: "youshallnotpass"
sources:
youtube: true
bandcamp: true
soundcloud: true
twitch: true
vimeo: true
http: true
local: false
plugins:
lavasrc:
providers:
- "ytsearch:\"%ISRC%\""
- "ytsearch:%QUERY%"
sources:
spotify: true
applemusic: false
deezer: false
yandexmusic: false
spotify:
clientId: "your_spotify_client_id"
clientSecret: "your_spotify_client_secret"- PEP 8 compliance: Use consistent formatting
- Type hints: Always provide type annotations
- Docstrings: Document all public methods
- Error handling: Comprehensive try-catch blocks
- Logging: Use structured logging for debugging
@commands.command(name='example')
@not_blacklisted # Always check blacklist
async def example_command(self, ctx: commands.Context, argument: str = None):
"""
Example command with proper structure
Args:
ctx: Discord context
argument: Optional command argument
"""
try:
# Input validation
if not argument:
embed = MusicEmbed.error(
"Missing Argument",
"Please provide an argument for this command."
)
await ctx.send(embed=embed)
return
# Permission check (if needed)
user_permission = await db.get_user_permission(ctx.author.id)
if user_permission.value < PermissionLevel.MODERATOR.value:
embed = MusicEmbed.error(
"Insufficient Permissions",
"This command requires moderator permissions."
)
await ctx.send(embed=embed)
return
# Business logic
result = await self.process_example(argument)
# Success response
embed = MusicEmbed.success(
"Command Success",
f"Processed: {result}"
)
await ctx.send(embed=embed)
except Exception as e:
# Error logging and user feedback
self.logger.error(f"Error in example command: {e}")
embed = MusicEmbed.error("Error", f"Command failed: {str(e)}")
await ctx.send(embed=embed)Always use async database operations:
async def example_database_operation(self, user_id: int):
"""Example of proper database usage"""
try:
# Use database connection
async with self._connection.cursor() as cursor:
await cursor.execute(
'SELECT * FROM users WHERE user_id = ?',
(user_id,)
)
result = await cursor.fetchone()
if result:
# Process result
return {'user_id': result[0], 'username': result[1]}
return None
except Exception as e:
self.logger.error(f"Database error: {e}")
raiseUse discord.py tasks for periodic operations:
from discord.ext import tasks
@tasks.loop(minutes=5) # Run every 5 minutes
async def background_task(self):
"""Example background task"""
try:
# Your periodic logic here
self.logger.debug("Background task executed")
except Exception as e:
self.logger.error(f"Background task error: {e}")
@background_task.before_loop
async def before_background_task(self):
"""Wait for bot to be ready"""
await self.bot.wait_until_ready()
# Start in module setup
async def _setup(self):
self.background_task.start()
# Stop in teardown
async def _teardown(self):
self.background_task.cancel()class PermissionLevel(IntEnum):
USER = 0 # Default user
MODERATOR = 1 # Basic moderation
ADMIN = 2 # Role management, user management
OWNER = 3 # Full bot control@owner_only # Requires OWNER level
@admin_only # Requires ADMIN level or higher
@moderator_only # Requires MODERATOR level or higher
@not_blacklisted # Blocks blacklisted users# Success message (green)
MusicEmbed.success(title: str, description: str, thumbnail: Optional[str] = None)
# Error message (red)
MusicEmbed.error(title: str, description: str)
# Info message (blue)
MusicEmbed.info(title: str, description: str, thumbnail: Optional[str] = None)
# Warning message (orange)
MusicEmbed.warning(title: str, description: str, thumbnail: Optional[str] = None)
# Music-specific embeds
MusicEmbed.music_playing(title: str, url: str, duration: str, requester: str, thumbnail: Optional[str] = None)
MusicEmbed.queue_added(title: str, url: str, position: int, requester: str, thumbnail: Optional[str] = None)
MusicEmbed.playlist_added(playlist_name: str, track_count: int, requester: str, thumbnail: Optional[str] = None)# Get voice client
if not ctx.voice_client:
# Handle no connection
return
vc: wavelink.Player = ctx.voice_client
# Player operations
await vc.play(track) # Play track
await vc.pause(True/False) # Pause/unpause
await vc.stop() # Stop playback
await vc.disconnect() # Leave voice channel
# Player properties
vc.playing # Is playing
vc.paused # Is paused
vc.current # Current track
vc.queue # Queue object
vc.volume # Volume level
vc.position # Current position# Check if user is playing games
for activity in user.activities:
if activity.type == discord.ActivityType.playing:
# User is playing a game
game_name = activity.name
break# Set tracked user
await db.set_tracked_user(guild_id, user_id, username)
# Check tracked user
tracked = await db.get_tracked_user(guild_id)
if tracked:
user_id = tracked['user_id']
# Process tracking logic
# Remove tracking
await db.remove_tracked_user(guild_id)# Issue: Database locked
# Solution: Ensure proper async context usage
async with self._connection.cursor() as cursor:
# All operations here
# Issue: Table doesn't exist
# Solution: Check _create_tables() method
await self._create_tables()# Issue: Cannot connect to Lavalink
# Check: Lavalink server running on correct port
# Check: application.yml configuration
# Check: Firewall/network connectivity
# Debug connection
node = wavelink.Node(uri=config.lavalink_uri, password=config.lavalink_password)
try:
await wavelink.Pool.connect(client=self.bot, nodes=[node])
self.logger.info("Connected to Lavalink")
except Exception as e:
self.logger.error(f"Lavalink connection failed: {e}")# Issue: Cannot access member data
# Solution: Enable intents in main.py and Discord Developer Portal
intents = discord.Intents.default()
intents.message_content = True # For command processing
intents.members = True # For member data access
intents.presences = True # For activity tracking# Issue: Commands not working for admins
# Check: User has proper permissions in database
user_permission = await db.get_user_permission(user_id)
print(f"User permission level: {user_permission}")
# Check: Decorator hierarchy
@admin_only # This requires ADMIN level or higher
@moderator_only # This requires MODERATOR level or higher# In logging_config.py
logging.basicConfig(level=logging.DEBUG)
# Module-specific debugging
self.logger.debug(f"Processing command: {ctx.command}")
self.logger.debug(f"User permission: {user_permission}")# Open SQLite database
sqlite3 database/ratart.db
# Check tables
.tables
# View user permissions
SELECT * FROM users;
# Check blacklisted users
SELECT * FROM blacklisted_users WHERE is_active = 1;
# View tracking data
SELECT * FROM tracked_users;Use !debug command to get comprehensive bot status including:
- Voice client status
- Queue information
- Connection details
- Guild information
- Database Cleanup: Remove old inactive records
- Log Rotation: Archive or clean old log files
- Dependency Updates: Keep packages up to date
- Lavalink Updates: Update Lavalink server when needed
When adding new features requiring database changes:
- Create migration script:
async def migrate_database():
"""Apply database migrations"""
async with db._connection.cursor() as cursor:
# Add new column
await cursor.execute('ALTER TABLE users ADD COLUMN new_field TEXT')
# Create new table
await cursor.execute('CREATE TABLE new_table (...)')
await db._connection.commit()- Version tracking: Track applied migrations
- Backup first: Always backup database before migrations
- Test thoroughly: Test migrations on copy first
- Connection pooling: Optimize database connections
- Caching: Cache frequently accessed data
- Query optimization: Use indexes and efficient queries
- Memory management: Monitor memory usage patterns
- Web Dashboard: Browser-based configuration interface
- Playlist Management: Save and load custom playlists
- Analytics: Usage statistics and reporting
- Multi-Language: Internationalization support
- Plugin System: Third-party plugin architecture
The modular architecture makes it easy to add:
- New command modules
- Additional music sources
- Custom permission systems
- Enhanced activity tracking
- Integration with other services
- Check logs: Most issues show up in console logs
- Use debug command:
!debugprovides system status - Check configuration: Verify .env and Lavalink setup
- Database inspection: Use SQLite tools to check data
- Fork repository: Create your own fork
- Feature branches: Create branches for new features
- Follow conventions: Match existing code style
- Test thoroughly: Test all new functionality
- Document changes: Update both user and developer docs
- Submit PR: Create pull request with clear description
- Follows PEP 8 style guide
- Includes type hints
- Has comprehensive error handling
- Includes appropriate logging
- Updates relevant documentation
- Tested across different scenarios
- No security vulnerabilities
- Performance considerations addressed
This documentation is maintained alongside the codebase. Last updated: 2025-09-13