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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user