Add SQLite database for dynamic guild management

Features:
- SQLite database with better-sqlite3 for guild configurations
- Auto-registration when bot joins new guilds with welcome messages
- Soft delete system preserves settings when bot is removed
- Dynamic configuration via /config slash command with subcommands
- Automatic migration from config.json to database on first run
- Support for scheduled events with timezone preservation

Technical Implementation:
- Node.js 20 for better SQLite compatibility in Docker
- Full Debian base image with npm for reliable native module compilation
- Database persistence via Docker volume (./data)
- Hybrid configuration system (database primary, file fallback)
- JSON storage for complex schedule objects with timezone support

Database Schema:
- guilds table with soft delete (is_active flag)
- scheduled_events table with JSON schedule storage
- bot_config table for global settings
- Auto-initialization and seeding from existing config

Admin Features:
- /config show - View current server settings
- /config subcommands - Update prefix, volume, features, etc.
- Administrator permissions required for configuration changes
- Graceful handling of missing or malformed data

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Ham
2025-08-16 14:02:27 -07:00
parent 9661ba92d5
commit d74aebfda7
12 changed files with 1157 additions and 75 deletions

View File

@@ -1,26 +1,121 @@
const fs = require("fs");
const path = require("path");
// Load config from root directory
const configPath = path.join(__dirname, "..", "..", "config.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
// Validate required config fields
function validateConfig(config) {
if (!config.discord?.token) {
throw new Error("Discord token is required in config.json");
// Dynamic config that combines file-based config with database
class ConfigManager {
constructor() {
this.fileConfig = this.loadFileConfig();
this.databaseService = null; // Will be injected
}
if (!config.discord?.guilds || !Array.isArray(config.discord.guilds)) {
throw new Error("Discord guilds configuration is required");
/**
* Load static configuration from file
*/
loadFileConfig() {
const configPath = path.join(__dirname, "..", "..", "config.json");
if (!fs.existsSync(configPath)) {
console.warn("config.json not found, using environment variables only");
return {
discord: {
token: process.env.DISCORD_TOKEN,
adminUserId: process.env.ADMIN_USER_ID,
}
};
}
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
// Validate required fields
if (!config.discord?.token && !process.env.DISCORD_TOKEN) {
throw new Error("Discord token is required in config.json or DISCORD_TOKEN environment variable");
}
return config;
}
// Ensure guilds is an array (supporting both old object format and new array format)
if (!Array.isArray(config.discord.guilds)) {
config.discord.guilds = Object.values(config.discord.guilds);
/**
* Inject database service (to avoid circular dependency)
*/
setDatabaseService(databaseService) {
this.databaseService = databaseService;
}
return config;
/**
* Get bot configuration (combines file and database)
*/
getBotConfig() {
const fileConfig = this.fileConfig;
const dbConfig = this.databaseService ? this.databaseService.getBotConfiguration() : {};
return {
// Use file config as fallback, database as primary
botName: dbConfig.botName || fileConfig.botName || 'GHBot',
debug: dbConfig.debug !== undefined ? dbConfig.debug : (fileConfig.debug || false),
discord: {
token: fileConfig.discord?.token || process.env.DISCORD_TOKEN,
adminUserId: dbConfig.adminUserId || fileConfig.discord?.adminUserId || process.env.ADMIN_USER_ID,
activities: dbConfig.activities || fileConfig.discord?.activities || ['Playing sounds', 'Serving facts'],
blacklistedUsers: dbConfig.blacklistedUsers || fileConfig.discord?.blacklistedUsers || [],
master: fileConfig.discord?.master !== false, // Default to true
}
};
}
/**
* Get guild configuration (from database primarily, file as fallback)
*/
getGuildConfig(guildId) {
if (this.databaseService) {
const dbConfig = this.databaseService.getGuildConfig(guildId);
if (dbConfig) {
return dbConfig;
}
}
// Fallback to file config for backward compatibility
if (this.fileConfig.discord?.guilds) {
const guilds = Array.isArray(this.fileConfig.discord.guilds)
? this.fileConfig.discord.guilds
: Object.values(this.fileConfig.discord.guilds);
return guilds.find(g => g.id === guildId);
}
// Return default config for new guilds
return {
id: guildId,
name: 'Unknown Guild',
internalName: 'Unknown Guild',
prefix: '!',
enableSfx: true,
allowedSfxChannels: null,
sfxVolume: 0.5,
enableFunFacts: true,
enableHamFacts: true,
allowedRolesForRequest: null,
};
}
/**
* Get all guild configurations
*/
getAllGuildConfigs() {
if (this.databaseService) {
return this.databaseService.getAllGuildConfigs();
}
// Fallback to file config
if (this.fileConfig.discord?.guilds) {
const guilds = Array.isArray(this.fileConfig.discord.guilds)
? this.fileConfig.discord.guilds
: Object.values(this.fileConfig.discord.guilds);
return guilds;
}
return [];
}
}
module.exports = validateConfig(config);
module.exports = new ConfigManager();