Skip to content

Character System Architecture

This document outlines the architecture for the character system in StingbatBot, which allows Discord users to manage their own characters.

Overview

The character system follows a layered architecture pattern:

  1. Schema Layer: Defines the database structure for characters
  2. Context Layer: Handles database operations for characters
  3. Service Layer: Implements business logic for character management
  4. Command Layer: Provides Discord command interfaces for users

This architecture separates concerns, improves testability, and makes the system more maintainable.

Data Model

Characters are uniquely identified by the combination of: - Discord user ID - Character name

The complete character data is stored as a JSONB object in the PostgreSQL database, while key fields are extracted as columns for efficient querying and enforcing uniqueness.

Database Schema

The database schema includes: - A characters table with columns for user_id, name, ancestry, class, and data (JSONB) - A unique index on the combination of user_id and name - An index on user_id for faster lookups - A GIN index on the JSONB data field for efficient querying of JSON contents

Architecture Components

Schema Module

The Schemas.Character module defines the database structure and validation rules. It: - Defines the structure of the characters table - Provides validation for character data - Ensures required fields are present - Enforces uniqueness constraints

Context Module

The Contexts.Characters module handles all database operations, including: - Retrieving characters by ID or identity (user_id, name, ancestry, class) - Creating new characters - Updating existing characters - Deleting characters - Listing characters by user - Performing advanced queries on the JSONB data

The context module serves as the boundary between the database and the business logic.

Service Module

The Services.CharacterService module implements business logic, including: - Importing characters from JSON data - Validating character data during import - Converting between database records and application structs - Enforcing access control (users can only access their own characters) - Formatting character data for presentation - Managing character selection state

Character Module Structure

The character system is organized into several core modules that work together:

Stingbatbot.Character

Responsibility: Domain model for character data

  • Provides the public API for character operations
  • Defines the character data structure
  • Delegates parsing operations to the Parse submodule
  • Accessing character attributes and calculating derived values
  • Exposing functions to retrieve inventory items, weapons, and spells

Key Functions: - parse/1: Delegates to Character.Parse to transform data into a character struct - get_stat_modifier/2: Calculates ability modifiers - get_weapons/1: Returns a list of character's weapons - get_attacks/1: Generates attack structures for a character's weapons - get_spell_castings/1: Generates spell casting structures for a character's spells - get_inventory_slots/1: Returns pre-computed inventory slots with their contents - get_stat_checks/1: Returns a character's stat check modifiers (planned) - get_bonuses/1: Returns all bonuses for a character - get_bonuses_by_name/2: Returns bonuses filtered by name

Stingbatbot.Character.Parse

Responsibility: Parsing character data and applying class/ancestry-specific modifications

  • Separates parsing logic from the main Character module
  • Transforms raw JSON data into structured character structs
  • Pre-computes inventory slots during parsing for better performance
  • Processes inventory items, abilities, talents, and spells
  • Exposes only parse/1 as its public API, with all other functions being private implementation details
  • Applies class and ancestry-specific modifications to characters during parsing
  • Automatically adds required abilities and traits based on character class/ancestry
  • Retrieves spells from SpellList based on character abilities and class

Key Functions: - parse/1 (public): Main function for transforming character data into a Character struct - All other functions are private, ensuring proper encapsulation of implementation details

Character Modification Pattern: - Defines a behavior with an apply_parsing_modification/1 callback - Provides a __using__ macro for easy implementation by class and ancestry modules - Maintains a list of implementation modules that can modify characters - During parsing, each module is given an opportunity to modify the character struct - Uses a pipeline pattern where character data flows through modification functions - Applies modifications only to relevant characters using pattern matching - Enables features like Priests automatically getting "Turn Undead" from SpellList without explicit JSON data

Implementation Example:

# In Priest.ex
use Stingbatbot.Character.Parse

@impl Stingbatbot.Character.Parse
def apply_parsing_modification(%{class: "Priest"} = character) do
  # Check if Turn Undead is already in the character's spells
  already_has_turn_undead? = Enum.any?(character.spells, fn spell ->
    spell.name == "Turn Undead"
  end)

  # If not, add it by retrieving from SpellList
  if already_has_turn_undead? do
    character
  else
    turn_undead = SpellList.get_spell_for_class("Turn Undead", "Priest")
    %{character | spells: [turn_undead | character.spells]}
  end
end
def apply_parsing_modification(character), do: character

Stingbatbot.Character.Bonus

Handles character bonuses (abilities and talents), including: - Converting raw bonus data into structured bonus records - Categorizing bonuses by type (ability or talent) - Providing accessors for bonus properties

Performance Optimizations

Several optimizations improve the performance of character operations:

  • Pre-computed Inventory Slots: Inventory slots are computed once during parsing rather than on each access
  • Structured Data: Character data is transformed into structured records for efficient access
  • Lazy Loading: Some derived properties are computed only when needed

Discord Commands

The character system provides the following Discord commands:

Import Command

The import command allows users to upload a JSON file containing character data:

!import

This command implements an interactive confirmation flow:

  1. Parse and Preview:
  2. The user uploads a JSON file attachment
  3. The system parses the JSON data and validates it contains at least name, ancestry, and class fields
  4. A detailed character sheet is displayed to the user for review

  5. Interactive Confirmation:

  6. The bot sends a message with the character preview
  7. The message includes interactive components:
    • A ✅ (confirm) button or reaction
    • A ❌ (cancel) button or reaction
  8. Only the user who initiated the import can interact with these controls
  9. The bot listens for the user's reaction/button press
  10. A timeout is set (e.g., 5 minutes) after which the import is automatically canceled

  11. Response Handling:

  12. If the user selects confirm: The character is saved to the database and a success message is displayed
  13. If the user selects cancel: The import is aborted and a cancellation message is displayed
  14. If the timeout is reached: The import is aborted and a timeout message is displayed

This interactive approach provides a seamless user experience without requiring additional commands, and the pattern can be reused for other features that require user confirmation.

Character Command

The character command serves as the main interface for character management with several subcommands:

Character Selection

!character <name>

This selects the specified character as the user's active character. All subsequent character-related commands without a specified character will use this selected character.

Character List

!character list

Lists all characters owned by the user.

Character Delete

!character delete <name>

Deletes the specified character owned by the user.

Character Display

!character

When used without arguments, displays the character sheet for the currently selected character. If no character is selected, it prompts the user to select a character.

Sheet Command

The sheet command displays a character sheet:

!sheet

The command displays the sheet for the currently selected character. If no character is selected, it provides an error message prompting the user to select a character using the !character <name> command.

Detail Command

The detail command displays detailed information about a character:

!detail [character_name]

The command displays detailed information for the currently selected character. If no character is selected, it provides an error message prompting the user to select a character using the !character <name> command.

Optionally, a character name can be provided as an argument to override the selected character and display details for a specific character instead. This provides flexibility when users want to quickly view information for a character other than their currently selected one.

User Settings Architecture

The user settings system follows the same architectural pattern as guild settings, providing a consistent approach to managing user-specific preferences.

Character Selection System

Responsibility: Manages user character selection across different context levels

  • Provides a hierarchical selection system (channel > guild > global)
  • Allows users to select different characters at different context levels
  • Tracks character selections persistently in the database
  • Validates character ownership before selection
  • Enforces uniqueness of character names per user (characters are uniquely identified by user_id and name)

Key Components: - select_character/4: Selects a character for a user at a specific context level - get_selected_character/3: Retrieves the most specific character selection - clear_selected_character/3: Removes a character selection - get_hierarchical_settings/3: Implements the context hierarchy lookup

Implementation Pattern: - Empty string defaults for optional context parameters - Foreign key constraints to maintain referential integrity - Pattern matching to validate character ownership - Context hierarchy resolution for finding the most specific selection - Uniqueness validation to ensure character names are unique per user

Character Selection Flow

The character selection system operates through multiple layers:

  1. Command Layer: Users select a character via Discord commands
  2. Service Layer (Settings module): Provides a unified interface for character selection
  3. Context Layer (Users module): Handles the database operations and validation
  4. Schema Layer (UserSettings): Maintains the many-to-one relationship with characters

The system uses a hierarchical approach that allows users to have different character selections at different levels:

  • Global: Default character selection when no context is specified
  • Guild: Character selection for a specific Discord server
  • Channel: Character selection for a specific channel within a server

When retrieving a selected character, the system checks from most specific to least specific context: 1. Check for a channel-specific selection 2. If none exists, check for a guild-specific selection 3. If none exists, check for a global selection 4. If none exists, return an appropriate error

Character Selection Validation

Before a character can be selected, the system performs validation: 1. Checks that the character exists 2. Verifies that the character belongs to the user making the selection 3. Checks that the character name is unique for that user (as characters are identified by user_id and name) 4. If validation passes, creates or updates the user settings entry

Character Reference Integrity

The system maintains referential integrity with characters: 1. Uses foreign key constraints to ensure selections reference valid characters 2. Handles character deletion gracefully by treating the selection as empty 3. Returns appropriate errors when selected characters cannot be found

Integration with Character Commands

The character commands interact with user settings to:

  • Store the selected character when a user runs the character selection command
  • Retrieve the selected character when commands are run without specifying a character name
  • Clear the selected character reference if the selected character is deleted

Security Considerations

User Access Control

  • Characters are associated with Discord user IDs, ensuring users can only access their own characters
  • Commands validate that users can only perform operations on their own characters
  • The system uses proper error handling to prevent information leakage
  • PostgreSQL's JSONB type provides protection against SQL injection for the JSON data

JSON Parsing Security

Processing user-uploaded JSON files presents several security risks that must be mitigated:

1. Denial of Service (DoS) Risks

  • JSON Bombs: Maliciously crafted JSON with deeply nested structures or repeated references that can exhaust system resources
  • Large Files: Excessively large JSON files that consume memory during parsing
  • Mitigation:
  • Implement file size limits (e.g., 100KB maximum)
  • Set depth limits for JSON parsing
  • Implement timeouts for parsing operations
  • Use streaming parsers for large files

2. Code Injection Risks

  • Arbitrary Code Execution: Some JSON parsers with unsafe deserialization can lead to code execution
  • Mitigation:
  • Use safe JSON parsing libraries (Jason in Elixir is safe by default)
  • Avoid using Code.eval_* functions on any part of the parsed JSON
  • Never use String.to_atom/1 on user input (use String.to_existing_atom/1 if needed)

3. Data Validation

  • Schema Validation: Ensure the JSON conforms to the expected structure
  • Data Sanitization: Clean and validate all fields before storing or using them
  • Mitigation:
  • Implement strict schema validation for all JSON imports
  • Validate all required fields are present and of the correct type
  • Sanitize string fields to prevent XSS if they will be displayed in web interfaces
  • Set reasonable limits on string lengths and array sizes

4. Database Security

  • NoSQL Injection: Even with JSONB, complex queries can be vulnerable
  • Mitigation:
  • Use parameterized queries when searching JSONB data
  • Validate and sanitize any user input used in JSONB queries
  • Use the principle of least privilege for database access

5. Implementation Approach

  • Safe Parsing: Use a two-step approach where JSON is first parsed safely, then validated
  • Isolated Processing: Process JSON in isolated contexts with resource limits
  • Preview Before Storage: The confirmation step provides an opportunity to review data before committing it
  • Audit Logging: Log all import attempts, including failures, for security monitoring

Discord-Specific Security

  • Message Rate Limiting: Implement rate limiting to prevent command spam
  • Reaction Authorization: Ensure only the command initiator can interact with confirmation controls
  • Timeout Handling: Automatically clean up and cancel operations that aren't completed
  • Error Handling: Provide clear error messages without exposing system details

JSON Schema Validation

Since the character JSON files are expected to come from a known source with a predictable structure, we can implement strict schema validation to ensure data integrity and security.

JSON Schema Definition

The system will use a predefined schema that specifies the exact structure expected for character data:

  1. Required Fields: Define which fields must be present (name, ancestry, class, etc.)
  2. Field Types: Specify the expected data type for each field (string, number, boolean, object, array)
  3. Value Constraints: Define constraints like minimum/maximum values, string patterns, or enum values
  4. Nested Structures: Define the structure of nested objects and arrays

Validation Schema vs. Database Schema

It's important to understand the distinction between the two schemas in the system:

Validation Schema

  • Purpose: Used only for parsing and validating incoming JSON data
  • Not Backed by Database: Exists purely in code, not as a database table
  • Temporary: Used during the import process, before data is stored
  • Complete Structure: Defines the entire expected JSON structure in detail
  • Focused on Validation: Includes rules for data validation and constraints

Database Schema

  • Purpose: Defines how data is stored in the PostgreSQL database
  • Backed by Database: Directly corresponds to database tables and columns
  • Persistent: Defines the long-term storage structure
  • Selective Columns: Extracts key fields as columns, stores complete data as JSONB
  • Focused on Storage: Optimized for querying and data relationships

Implementation Options

Several approaches are available for implementing JSON schema validation in Elixir:

1. ExJsonSchema

The ex_json_schema library provides JSON Schema validation following the JSON Schema specification:

  • Define a schema that matches the expected character data structure
  • Validate incoming JSON against this schema before processing
  • Receive detailed error messages for validation failures

2. Ecto Changesets

Leverage Ecto's changeset functionality for validation:

  • Define a virtual struct (not backed by a database table) that matches the expected JSON structure
  • Create a changeset function with validation rules
  • Cast the JSON data to the struct and apply validations
  • Use similar validation logic to what's used for database operations, but without database persistence

3. Pattern Matching

For simpler schemas, use Elixir's pattern matching:

  • Define the expected structure using pattern matching
  • Extract and validate fields in a single step
  • Provide custom error messages for missing or invalid fields

Data Flow

The validation process creates a clear pipeline for data:

  1. JSON File → Parse with Jason → Raw Map
  2. Raw Map → Validate against schema → Validated Map
  3. Validated Map → Extract key fields → Database Record
  4. Database Record → Store in database → Persisted Character

This separation ensures that only valid, well-structured data enters the database.

Character Schema Example

The character data will be validated against a schema that includes:

  • Basic character information (name, ancestry, class, level)
  • Character statistics (strength, dexterity, etc.)
  • Skills, abilities, and inventory items
  • Nested structures for complex data like spells or equipment

Validation Process

  1. Initial Parsing: Parse the JSON safely using Jason
  2. Schema Validation: Validate the parsed data against the predefined schema
  3. Custom Validation: Apply additional business rules not covered by the schema
  4. Error Handling: Generate user-friendly error messages for validation failures
  5. Preview Generation: Only generate a preview for the user after validation passes

Benefits of Schema Validation

  • Data Integrity: Ensures all required data is present and correctly formatted
  • Security: Prevents malformed or malicious data from entering the system
  • User Experience: Provides clear feedback about validation errors
  • Maintainability: Centralizes validation logic in a single location
  • Documentation: The schema serves as documentation for the expected data format

Storage Strategy

The character system uses PostgreSQL as the sole storage mechanism:

  1. All characters are stored in the database with user association
  2. Each Discord user can import and manage their own collection of characters
  3. The database is the only storage option for characters
  4. Users need to import their characters via the import command

PostgreSQL JSONB Benefits

Using PostgreSQL's JSONB type for character data provides several advantages:

  1. Efficient Storage: JSONB is stored in a binary format, which is more compact than text-based JSON
  2. Indexing: Supports GIN indexes for efficient querying of JSON contents
  3. Query Capabilities: Allows for complex queries into the JSON structure using PostgreSQL's JSON operators
  4. Performance: Better performance for both storage and retrieval compared to regular JSON
  5. Flexibility: Schema can evolve without requiring database migrations for the character data structure

Character Schema

The character schema includes several fields for tracking character data:

  • Basic Information:
  • name: String
  • ancestry: String
  • class: String
  • user_id: Integer (Discord user ID, stored as :bigint in database)
  • data: JSONB (raw character data)