Skip to content

07. Developer Guide

ad1107 edited this page May 24, 2025 · 1 revision

Developer Guide

Complete guide for developers who want to contribute to UtilsBot+ or understand its internal architecture.

Table of Contents


Development Environment Setup

Prerequisites

  • Python 3.11+ (Python 3.12 recommended)
  • Git for version control
  • Code editor (VS Code recommended with Python extensions)
  • Discord Bot Token for testing
  • Google Gemini API Key for AI features

Setup Steps

# Clone the repository
git clone https://github.com/ad1107/utils-bot-plus.git
cd utils-bot-plus

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install development dependencies
pip install -r requirements-dev.txt
pip install -r requirements.txt

# Run setup script
chmod +x setup.sh
./setup.sh

# Initialize database
python migrations/init_db.py

Development Configuration

Create a .env file for development:

# Development configuration
DEBUG=true
LOG_LEVEL=DEBUG

# Required credentials
BOT_TOKEN=your_dev_bot_token
DEV_IDS=your_discord_id
DEV_GUILD_ID=your_test_server_id
GEMINI_API_KEY=your_api_key
SECRET_KEY=generated_secret

# Development features
AUTO_SYNC_COMMANDS=true
ENABLE_HOT_RELOAD=true

Development Commands

# Start with hot reload
make run-dev

# Format code
make format

# Type checking
make type-check

# Run tests
make test

# Check code quality
make lint

Project Structure

utils-bot-plus/
├── main.py                 # Application entry point
├── pyproject.toml         # Modern Python packaging
├── requirements.txt       # Production dependencies
├── requirements-dev.txt   # Development dependencies
├── Makefile              # Development commands
│
├── config/               # Configuration management
│   ├── __init__.py
│   └── settings.py       # Pydantic settings with validation
│
├── core/                 # Core bot functionality
│   ├── __init__.py
│   ├── bot.py           # Main UtilsBotPlus class
│   └── logger.py        # Structured logging setup
│
├── cogs/                 # Feature modules (slash commands)
│   ├── ai.py            # Google Gemini integration
│   ├── games.py         # Interactive games (Wordle)
│   ├── info.py          # Bot info & help system
│   ├── network.py       # Network utilities
│   ├── system.py        # Developer commands
│   └── tools.py         # Security & utility tools
│
├── models/               # Data models & database
│   ├── __init__.py
│   └── database.py      # SQLAlchemy models & management
│
├── utils/                # Utility functions
│   ├── __init__.py
│   ├── checks.py        # Permission decorators
│   ├── embeds.py        # Discord embed helpers
│   ├── health.py        # Health monitoring
│   └── screenshot.py    # Screenshot service
│
├── assets/               # Static resources
│   └── games/
│       └── wordle_words.txt
│
├── migrations/           # Database setup scripts
│   ├── init_db.py       # Database initialization
│   └── populate_data.py # Sample data population
│
├── data/                 # SQLite database storage
├── logs/                 # Application logs
└── tests/               # Test files

Code Architecture

Core Design Principles

  1. Slash Commands Only - No prefix commands for better UX
  2. Async First - Full async/await architecture
  3. Type Safety - Complete type hints and validation
  4. Modular Design - Feature separation via cogs
  5. Error Resilience - Comprehensive error handling
  6. Security First - Permission-based access control

Bot Core (core/bot.py)

The main bot class extends commands.Bot:

class UtilsBotPlus(commands.Bot):
    def __init__(self):
        super().__init__(
            command_prefix="$disabled$",  # Unused
            intents=discord.Intents.default(),
            help_command=None
        )
        
        # Initialize components
        self.db = Database()
        self.logger = get_logger(__name__)
        self.start_time = datetime.now()

Key Features:

  • Automatic cog loading from cogs/ directory
  • Database initialization and management
  • Global error handling for slash commands
  • Command syncing (global and guild-specific)
  • Uptime and performance tracking

Cog System

Commands are organized into cogs (modules) for better organization:

from discord.ext import commands
from discord import app_commands

class ExampleCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.logger = get_logger(__name__)
    
    @app_commands.command(name="example", description="Example command")
    async def example_command(self, interaction: discord.Interaction):
        await interaction.response.send_message("Hello!")

async def setup(bot):
    await bot.add_cog(ExampleCog(bot))

Database Layer

SQLAlchemy 2.0 with async support:

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

class Database:
    def __init__(self, database_url: str = None):
        self.engine = create_async_engine(database_url or settings.database_url)
        self.async_session = sessionmaker(
            self.engine, class_=AsyncSession, expire_on_commit=False
        )
    
    async def get_user(self, discord_id: int) -> Optional[User]:
        async with self.async_session() as session:
            result = await session.get(User, discord_id)
            return result

Adding New Commands

Step 1: Choose the Right Cog

Determine which cog your command belongs to:

  • ai.py: AI and machine learning features
  • games.py: Interactive games and entertainment
  • info.py: Bot information and help commands
  • network.py: Network and web utilities
  • system.py: Developer and system commands
  • tools.py: Security and utility tools

Step 2: Implement the Command

@app_commands.command(name="mycommand", description="Description of my command")
@app_commands.describe(
    param1="Description of parameter 1",
    param2="Description of parameter 2"
)
@requires_whitelist()  # Add appropriate permission checks
@cooldown(rate=5, per=60)  # Add rate limiting
async def my_command(
    self, 
    interaction: discord.Interaction, 
    param1: str,
    param2: Optional[int] = None
):
    """
    Command implementation with proper type hints.
    
    Args:
        interaction: Discord interaction object
        param1: Required string parameter
        param2: Optional integer parameter
    """
    try:
        # Log command usage
        self.logger.info(
            f"Command executed",
            extra={
                "user_id": interaction.user.id,
                "guild_id": interaction.guild_id if interaction.guild else None,
                "command": "mycommand"
            }
        )
        
        # Command logic here
        result = await some_async_operation(param1, param2)
        
        # Create success response
        embed = create_success_embed(
            title="Success",
            description=f"Operation completed: {result}"
        )
        
        await interaction.response.send_message(embed=embed)
        
    except ValueError as e:
        # Handle specific errors
        embed = create_error_embed("Invalid Input", str(e))
        await interaction.response.send_message(embed=embed, ephemeral=True)
        
    except Exception as e:
        # Log unexpected errors
        self.logger.error(f"Command error: {e}", exc_info=True)
        
        embed = create_error_embed(
            "Command Error", 
            "An unexpected error occurred. Please try again."
        )
        await interaction.response.send_message(embed=embed, ephemeral=True)

Step 3: Add Error Handling

Use the standard error handling pattern:

from utils.embeds import create_success_embed, create_error_embed

# Success response
embed = create_success_embed("Title", "Description")
await interaction.response.send_message(embed=embed)

# Error response
embed = create_error_embed("Error Title", "Error description")
await interaction.response.send_message(embed=embed, ephemeral=True)

Step 4: Add Permission Checks

Use decorators for permission control:

from utils.checks import dev_only, requires_whitelist

@dev_only()  # Developer only
@requires_whitelist()  # Requires whitelist (if beta mode)
@cooldown(rate=3, per=60)  # Rate limiting

Step 5: Update Documentation

  • Add command to this wiki's Commands Reference
  • Update the main README.md
  • Add docstrings and type hints

Database Development

Model Definition

Define models using SQLAlchemy:

from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    discord_id = Column(String(20), unique=True, nullable=False)
    username = Column(String(100))
    is_whitelisted = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

Database Operations

# Create user
async def create_user(self, discord_id: int, username: str) -> User:
    async with self.async_session() as session:
        user = User(discord_id=discord_id, username=username)
        session.add(user)
        await session.commit()
        await session.refresh(user)
        return user

# Update user
async def update_user(self, discord_id: int, **kwargs) -> Optional[User]:
    async with self.async_session() as session:
        user = await session.get(User, discord_id)
        if user:
            for key, value in kwargs.items():
                setattr(user, key, value)
            user.updated_at = datetime.utcnow()
            await session.commit()
            return user
        return None

Migrations

For database schema changes:

# migrations/add_new_table.py
from models.database import Base, engine

async def migrate():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

if __name__ == "__main__":
    import asyncio
    asyncio.run(migrate())

Testing

Unit Tests

import pytest
from unittest.mock import AsyncMock, MagicMock
from cogs.example import ExampleCog

@pytest.mark.asyncio
async def test_example_command():
    # Setup
    bot = MagicMock()
    cog = ExampleCog(bot)
    interaction = AsyncMock()
    
    # Execute
    await cog.example_command(interaction, "test_input")
    
    # Assert
    interaction.response.send_message.assert_called_once()

Integration Tests

@pytest.mark.asyncio
async def test_database_integration():
    # Test database operations
    db = Database(":memory:")  # In-memory SQLite
    await db.initialize()
    
    user = await db.create_user(123456789, "testuser")
    assert user.discord_id == 123456789
    
    retrieved = await db.get_user(123456789)
    assert retrieved.username == "testuser"

Running Tests

# Run all tests
make test

# Run specific test file
pytest tests/test_commands.py -v

# Run with coverage
pytest --cov=cogs tests/

Code Style Guidelines

Python Style

Follow PEP 8 and use modern Python features:

# Type hints
from typing import Optional, List, Dict, Any, Union

async def example_function(
    param1: str,
    param2: Optional[int] = None,
    param3: List[str] = None
) -> Dict[str, Any]:
    """Function with proper type hints and docstring."""
    if param3 is None:
        param3 = []
    
    return {"result": param1, "count": param2, "items": param3}

Async Patterns

Use proper async/await patterns:

# Good: Proper async context
async with self.bot.db.async_session() as session:
    result = await session.execute(query)

# Good: Async error handling
try:
    result = await async_operation()
except asyncio.TimeoutError:
    await interaction.response.send_message("Operation timed out")

Logging

Use structured logging:

self.logger.info(
    "User action completed",
    extra={
        "user_id": interaction.user.id,
        "action": "command_execution",
        "command": "example",
        "success": True
    }
)

Documentation

Document all public functions:

async def process_data(self, data: List[str]) -> Dict[str, int]:
    """
    Process a list of strings and return statistics.
    
    Args:
        data: List of strings to process
        
    Returns:
        Dictionary with processing statistics
        
    Raises:
        ValueError: If data is empty
        ProcessingError: If processing fails
    """

Contributing Workflow

1. Fork and Clone

# Fork the repository on GitHub
git clone https://github.com/utils-bot/utils-bot-plus.git
cd utils-bot-plus
git remote add upstream https://github.com/utils-bot/utils-bot-plus.git

2. Create Feature Branch

git checkout -b feature/your-feature-name

3. Make Changes

  • Follow code style guidelines
  • Add tests for new features
  • Update documentation
  • Ensure all tests pass

4. Commit Changes

git add .
git commit -m "feat: add new command for example feature

- Implement /example command with proper validation
- Add tests and documentation
- Update command reference
"

5. Submit Pull Request

git push origin feature/your-feature-name
# Create PR on GitHub

Commit Message Format

Use conventional commits:

feat: add new feature
fix: resolve bug in command
docs: update documentation
test: add test coverage
refactor: improve code structure

Debugging

Development Debugging

# Enable debug mode
echo "DEBUG=true" >> .env
echo "LOG_LEVEL=DEBUG" >> .env

# Run with detailed logging
python main.py

# Check logs
tail -f logs/bot.log

Interactive Debugging

Use the /eval command for live debugging:

# Check bot state
/eval code:"len(bot.guilds)"

# Inspect user data
/eval code:"await bot.db.get_user(interaction.user.id)"

# Test functions
/eval code:"import math; math.sqrt(16)"

Common Debug Scenarios

Commands Not Syncing:

# Check command tree
/eval code:"[cmd.name for cmd in bot.tree.get_commands()]"

# Manual sync
/sync scope:"guild"

Database Issues:

# Test database connection
/eval code:"await bot.db.get_user(123456789)"

# Check table existence
/eval code:"bot.db.engine.table_names()"

Performance Monitoring

# Add timing to commands
import time

start_time = time.time()
# Command logic
execution_time = (time.time() - start_time) * 1000
self.logger.info(f"Command executed in {execution_time:.2f}ms")

Best Practices

Security

  • Always validate user input
  • Use ephemeral responses for sensitive data
  • Implement proper rate limiting
  • Never log sensitive information

Performance

  • Use async/await properly
  • Implement connection pooling for external APIs
  • Cache frequently accessed data
  • Monitor memory usage

Error Handling

  • Provide user-friendly error messages
  • Log detailed errors for debugging
  • Use appropriate HTTP status codes for APIs
  • Implement retry logic for external services

Code Organization

  • Keep cogs focused on specific functionality
  • Use utility functions for common operations
  • Separate business logic from Discord interface
  • Write comprehensive tests

Navigation: ← Commands Reference | Database Schema →

Related Pages: Architecture Overview | API Integrations | Security Guide

Clone this wiki locally