import fs from 'fs'; import path from 'path'; import {fileURLToPath} from 'url'; import Discord from 'discord.js'; import sequelize from 'sequelize'; import AwnAPI from './utils/awn-api.js'; import ConfigTools from './utils/config-tools.js'; import Logger from 'core/logger'; import SquadServer from './index.js'; import Plugins from './plugins/index.js'; const { Sequelize } = sequelize; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default class SquadServerFactory { static async buildFromConfig(config) { Logger.verbose('SquadServerFactory', 4, `Logging config:\n${JSON.stringify(config)}`); Logger.setTimeStamps(config.logger.timestamps ? config.logger.timestamps : false); const plugins = await Plugins.getPlugins(); for (const plugin of Object.keys(plugins)) { Logger.setColor(plugin, 'magentaBright'); } // setup logging levels for (const [module, verboseness] of Object.entries(config.logger.verboseness)) { Logger.setVerboseness(module, verboseness); } for (const [module, color] of Object.entries(config.logger.colors)) { Logger.setColor(module, color); } // create SquadServer Logger.verbose('SquadServerFactory', 1, 'Creating SquadServer...'); const server = new SquadServer(config.server); // initialise connectors Logger.verbose('SquadServerFactory', 1, 'Preparing connectors...'); const connectors = {}; for (const pluginConfig of config.plugins) { if (!pluginConfig.enabled) continue; if (!plugins[pluginConfig.plugin]) throw new Error(`Plugin ${pluginConfig.plugin} does not exist.`); const Plugin = plugins[pluginConfig.plugin]; for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) { // ignore non connectors if (!option.connector) continue; // check the connector is listed in the options if (!(optionName in pluginConfig)) throw new Error( `${Plugin.name}: ${optionName} (${option.connector} connector) is missing.` ); // get the name of the connector const connectorName = pluginConfig[optionName]; // skip already created connectors if (connectors[connectorName]) continue; // create the connector connectors[connectorName] = await SquadServerFactory.createConnector( server, option.connector, connectorName, config.connectors[connectorName] ); } } // initialise plugins Logger.verbose('SquadServerFactory', 1, 'Initialising plugins...'); for (const pluginConfig of config.plugins) { if (!pluginConfig.enabled) continue; if (!plugins[pluginConfig.plugin]) throw new Error(`Plugin ${pluginConfig.plugin} does not exist.`); const Plugin = plugins[pluginConfig.plugin]; Logger.verbose('SquadServerFactory', 1, `Initialising ${Plugin.name}...`); const plugin = new Plugin(server, pluginConfig, connectors); // allow the plugin to do any asynchronous work needed before it can be mounted await plugin.prepareToMount(); server.plugins.push(plugin); } return server; } static async createConnector(server, type, connectorName, connectorConfig) { Logger.verbose('SquadServerFactory', 1, `Starting ${type} connector ${connectorName}...`); if (type === 'discord') { Logger.verbose('SquadServerFactory', 1, `Connector is Discord Type`); const connector = new Discord.Client(); Logger.verbose('SquadServerFactory', 1, `Connector created Discord client`); await connector.login(connectorConfig); Logger.verbose('SquadServerFactory', 1, `Connector Logged into Discord`); return connector; } if (type === 'sequelize') { Logger.verbose('SquadServerFactory', 1, `Connector is SQL Type`); let connector; if (typeof connectorConfig === 'string') { connector = new Sequelize(connectorConfig, { define: { charset: 'utf8mb4', collate: 'utf8mb4_unicode_ci' }, logging: (msg) => Logger.verbose('Sequelize', 3, msg) }); } else if (typeof connectorConfig === 'object') { connector = new Sequelize({ ...connectorConfig, logging: (msg) => Logger.verbose('Sequelize', 3, msg) }); } else { throw new Error('Unknown sequelize connector config type.'); } Logger.verbose('SquadServerFactory', 1, `Connector created SQL client`); await connector.authenticate(); Logger.verbose('SquadServerFactory', 1, `Connector Logged into SQL`); return connector; } if (type === 'awnAPI') { const awn = new AwnAPI(connectorConfig); await awn.auth(connectorConfig); return awn; } throw new Error(`${type.connector} is an unsupported connector type.`); } static parseConfig(configString, configfile = "", previousconfigfiles = null) { configfile = path.resolve(configfile); if (!previousconfigfiles) previousconfigfiles = [] if (previousconfigfiles.indexOf(configfile) > -1){ Logger.verbose('SquadServerFactory', 1, `Found circular config with ${previousconfigfiles.slice(1)}`); throw new Error(`Config has circular dependency at ${configfile} in with previous configs ${previousconfigfiles}!`); } else previousconfigfiles.push(configfile) try { const jsonconfig = JSON.parse(configString); const beforeincludes = jsonconfig.baseincludes ? jsonconfig.baseincludes.flatMap((filename) => { try { return SquadServerFactory.parseConfig( SquadServerFactory.readConfigFile( filename, path.dirname(configfile) ), filename, previousconfigfiles ); } catch (err) { throw new Error(`Config file ${filename} from ${configfile} did not load. ${err}`) } }) : []; const overwrites = jsonconfig.overwrites ? jsonconfig.overwrites.flatMap((filename) => SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(filename), filename, previousconfigfiles)) : []; const retconfig = [beforeincludes, jsonconfig, overwrites].flat(1); return retconfig; } catch (err) { throw new Error(`Unable to parse config file. ${err}`); } } static buildFromConfigString(configString) { Logger.verbose('SquadServerFactory', 1, 'Parsing config string...'); return SquadServerFactory.buildFromConfig(SquadServerFactory.parseConfig(configString)); } static readConfigFile(configPath = './config.json', dirlocation = null) { const configLoc = dirlocation ? path.resolve(__dirname, '../', dirlocation, configPath) : path.resolve(__dirname, '../', configPath); if (!fs.existsSync(configLoc)) throw new Error(`Config file does not exist. ${configLoc}`); Logger.verbose('SquadServerFactory', 1, `Reading config file ${configLoc}`); return fs.readFileSync(configLoc, 'utf8'); } static buildFromConfigFile(configPath) { Logger.verbose('SquadServerFactory', 1, 'Reading config file...'); Logger.verbose('SquadServerFactory', 4, JSON.stringify(configPath)); let configs = SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(configPath)); const mergedconfig = ConfigTools.mergeConfigs({}, ...configs); Logger.verbose('SquadServerFactory', 4, `Merged config: ${JSON.stringify(mergedconfig)}`) return SquadServerFactory.buildFromConfig(mergedconfig); } static async buildConfig() { const plugins = await Plugins.getPlugins(); const templatePath = path.resolve(__dirname, './templates/config-template.json'); const templateString = fs.readFileSync(templatePath, 'utf8'); const template = SquadServerFactory.parseConfig(templateString); const pluginKeys = Object.keys(plugins).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0 ); for (const pluginKey of pluginKeys) { const Plugin = plugins[pluginKey]; const pluginConfig = { plugin: Plugin.name, enabled: Plugin.defaultEnabled }; for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) { pluginConfig[optionName] = option.default; } template.plugins.push(pluginConfig); } return template; } static async buildConfigFile() { const configPath = path.resolve(__dirname, '../config.example.json'); const config = await SquadServerFactory.buildConfig(); const configString = JSON.stringify(config, null, 2); fs.writeFileSync(configPath, configString); } static async buildReadmeFile() { const plugins = await Plugins.getPlugins(); const pluginKeys = Object.keys(plugins).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0 ); const pluginInfo = []; for (const pluginName of pluginKeys) { const Plugin = plugins[pluginName]; const options = []; for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) { let optionInfo = `
  • ${optionName}${option.required ? ' (Required)' : ''}

    Description

    ${option.description}

    Default
    ${
                 typeof option.default === 'object'
                   ? JSON.stringify(option.default, null, 2)
                   : option.default
               }
  • `; if (option.example) optionInfo += `
    Example
    ${
                 typeof option.example === 'object'
                   ? JSON.stringify(option.example, null, 2)
                   : option.example
               }
    `; options.push(optionInfo); } pluginInfo.push( `
    ${Plugin.name}

    ${Plugin.name}

    ${Plugin.description}

    Options

    ` ); } const pluginInfoText = pluginInfo.join('\n\n'); const templatePath = path.resolve(__dirname, './templates/readme-template.md'); const template = fs.readFileSync(templatePath, 'utf8'); const readmePath = path.resolve(__dirname, '../README.md'); const readme = template.replace(/\/\/PLUGIN-INFO\/\//, pluginInfoText); fs.writeFileSync(readmePath, readme); } }