SquadJS/squad-server/index.js

664 lines
21 KiB
JavaScript
Raw Normal View History

2020-05-15 12:42:39 -05:00
import EventEmitter from 'events';
2020-10-05 12:52:01 -05:00
import fs from 'fs';
import { fileURLToPath } from 'url';
import path from 'path';
2020-05-15 12:42:39 -05:00
2020-10-23 05:38:06 -05:00
import axios from 'axios';
2020-10-05 12:52:01 -05:00
import Discord from 'discord.js';
2020-05-15 12:42:39 -05:00
import Gamedig from 'gamedig';
2020-10-05 12:52:01 -05:00
import mysql from 'mysql';
2020-05-15 12:42:39 -05:00
2020-10-25 09:24:48 -05:00
import Logger from 'core/logger';
import { SQUADJS_API_DOMAIN } from 'core/constants';
2020-10-05 12:52:01 -05:00
import LogParser from 'log-parser';
2020-10-25 19:11:47 -05:00
import Rcon from 'rcon/squad';
2020-05-15 12:42:39 -05:00
2020-10-25 09:24:48 -05:00
import { SQUADJS_VERSION } from './utils/constants.js';
import { SquadLayers } from './utils/squad-layers.js';
2020-10-23 05:38:06 -05:00
2020-10-05 12:52:01 -05:00
import plugins from './plugins/index.js';
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default class SquadServer extends EventEmitter {
2020-05-15 12:42:39 -05:00
constructor(options = {}) {
super();
2020-10-05 12:52:01 -05:00
for (const option of ['host', 'queryPort'])
if (!(option in options)) throw new Error(`${option} must be specified.`);
2020-05-15 12:42:39 -05:00
this.options = options;
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
this.layerHistory = [];
2020-05-15 12:42:39 -05:00
this.layerHistoryMaxLength = options.layerHistoryMaxLength || 20;
this.players = [];
2020-10-05 12:52:01 -05:00
this.plugins = [];
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
this.squadLayers = new SquadLayers(options.squadLayersSource);
2020-05-15 12:42:39 -05:00
this.setupRCON();
this.setupLogParser();
this.updatePlayerList = this.updatePlayerList.bind(this);
this.updatePlayerListInterval = 30 * 1000;
this.updatePlayerListTimeout = null;
this.updateLayerInformation = this.updateLayerInformation.bind(this);
this.updateLayerInformationInterval = 30 * 1000;
this.updateLayerInformationTimeout = null;
this.updateA2SInformation = this.updateA2SInformation.bind(this);
this.updateA2SInformationInterval = 30 * 1000;
this.updateA2SInformationTimeout = null;
2020-10-23 05:38:06 -05:00
this.pingSquadJSAPI = this.pingSquadJSAPI.bind(this);
this.pingSquadJSAPIInterval = 5 * 60 * 1000;
this.pingSquadJSAPITimeout = null;
}
2020-11-04 17:17:21 -06:00
async watch() {
await this.squadLayers.pull();
await this.rcon.connect();
await this.logParser.watch();
await this.updatePlayerList();
await this.updateLayerInformation();
await this.updateA2SInformation();
Logger.verbose('SquadServer', 1, `Watching ${this.serverName}...`);
await this.pingSquadJSAPI();
}
async unwatch() {
await this.rcon.disconnect();
await this.logParser.unwatch();
}
setupRCON() {
this.rcon = new Rcon({
host: this.options.host,
port: this.options.rconPort,
password: this.options.rconPassword,
autoReconnectInterval: this.options.rconAutoReconnectInterval
});
this.rcon.on('CHAT_MESSAGE', async (data) => {
data.player = await this.getPlayerBySteamID(data.steamID);
this.emit('CHAT_MESSAGE', data);
const command = data.message.match(/!([^ ]+) ?(.*)/);
if (command)
this.emit(`CHAT_COMMAND:${command[1].toLowerCase()}`, {
...data,
message: command[2].trim()
});
});
this.rcon.on('RCON_ERROR', (data) => {
this.emit('RCON_ERROR', data);
});
}
async restartRCON() {
try {
await this.rcon.disconnect();
} catch (err) {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Failed to stop RCON instance when restarting.', err);
}
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Setting up new RCON instance...');
this.setupRCON();
await this.rcon.connect();
}
setupLogParser() {
2020-10-05 12:52:01 -05:00
this.logParser = new LogParser({
mode: this.options.logReaderMode,
logDir: this.options.logDir,
host: this.options.ftpHost || this.options.host,
port: this.options.ftpPort,
user: this.options.ftpUser,
password: this.options.ftpPassword,
secure: this.options.ftpSecure,
timeout: this.options.ftpTimeout,
verbose: this.options.ftpVerbose,
fetchInterval: this.options.ftpFetchInterval,
2020-10-22 12:18:44 -05:00
maxTempFileSize: this.options.ftpMaxTempFileSize,
// enable this for FTP servers that do not support SIZE
useListForSize: this.options.ftpUseListForSize
2020-10-05 12:52:01 -05:00
});
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
this.logParser.on('ADMIN_BROADCAST', (data) => {
this.emit('ADMIN_BROADCAST', data);
});
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
this.logParser.on('NEW_GAME', (data) => {
let layer;
if (data.layer) layer = this.squadLayers.getLayerByLayerName(data.layer);
else layer = this.squadLayers.getLayerByLayerClassname(data.layerClassname);
this.layerHistory.unshift({ ...layer, time: data.time });
this.layerHistory = this.layerHistory.slice(0, this.layerHistoryMaxLength);
this.emit('NEW_GAME', data);
});
this.logParser.on('PLAYER_CONNECTED', async (data) => {
data.player = await this.getPlayerBySteamID(data.steamID);
if (data.player) data.player.suffix = data.playerSuffix;
delete data.steamID;
delete data.playerSuffix;
this.emit('PLAYER_CONNECTED', data);
});
this.logParser.on('PLAYER_DAMAGED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
2020-10-21 16:49:07 -05:00
if (data.victim && data.attacker)
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.steamID !== data.attacker.steamID;
2020-10-05 12:52:01 -05:00
delete data.victimName;
delete data.attackerName;
this.emit('PLAYER_DAMAGED', data);
});
this.logParser.on('PLAYER_WOUNDED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
2020-10-21 16:49:07 -05:00
if (data.victim && data.attacker)
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.steamID !== data.attacker.steamID;
2020-10-05 12:52:01 -05:00
delete data.victimName;
delete data.attackerName;
this.emit('PLAYER_WOUNDED', data);
if (data.teamkill) this.emit('TEAMKILL', data);
});
this.logParser.on('PLAYER_DIED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
2020-10-21 16:49:07 -05:00
if (data.victim && data.attacker)
data.teamkill =
data.victim.teamID === data.attacker.teamID &&
data.victim.steamID !== data.attacker.steamID;
2020-10-05 12:52:01 -05:00
delete data.victimName;
delete data.attackerName;
this.emit('PLAYER_DIED', data);
});
this.logParser.on('PLAYER_REVIVED', async (data) => {
data.victim = await this.getPlayerByName(data.victimName);
data.attacker = await this.getPlayerByName(data.attackerName);
data.reviver = await this.getPlayerByName(data.reviverName);
delete data.victimName;
delete data.attackerName;
delete data.reviverName;
this.emit('PLAYER_REVIVED', data);
});
this.logParser.on('PLAYER_POSSESS', async (data) => {
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
if (data.player) data.player.possessClassname = data.possessClassname;
delete data.playerSuffix;
this.emit('PLAYER_POSSESS', data);
});
this.logParser.on('PLAYER_UNPOSSESS', async (data) => {
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
delete data.playerSuffix;
this.emit('PLAYER_UNPOSSESS', data);
});
this.logParser.on('TICK_RATE', (data) => {
this.emit('TICK_RATE', data);
});
}
2020-10-05 12:52:01 -05:00
async restartLogParser() {
try {
await this.logParser.unwatch();
} catch (err) {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Failed to stop LogParser instance when restarting.', err);
}
2020-05-15 12:42:39 -05:00
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Setting up new LogParser instance...');
this.setupLogParser();
await this.logParser.watch();
2020-05-15 12:42:39 -05:00
}
2020-10-30 16:17:53 -05:00
async setupAdminList(remoteAdminLists) {
2020-10-27 16:31:05 -05:00
try {
const remoteAdmins = {
admins: {},
whitelist: {},
groups: {}
2020-10-30 16:17:53 -05:00
};
for (let idx = 0; idx < remoteAdminLists.length; idx++) {
2020-10-30 16:17:53 -05:00
const list = remoteAdminLists[idx];
2020-10-27 16:31:05 -05:00
const resp = await axios({
method: 'GET',
url: `${list}`
});
const rawData = resp.data;
const groupRgx = /(?<=Group=)(.*?):(.*)(?=\n)/g;
const adminRgx = /(?<=Admin=)(\d+):(\S+)(?=\s)/g;
/* eslint-disable no-unused-vars */
2020-10-29 12:14:20 -05:00
for (const [match, groupID, groupPerms] of rawData.matchAll(groupRgx)) {
2020-10-30 16:17:53 -05:00
remoteAdmins.groups[`${idx}-${groupID}`] = groupPerms.split(',');
2020-10-29 12:14:20 -05:00
}
for (const [match, steamID, groupID] of rawData.matchAll(adminRgx)) {
2020-10-30 16:17:53 -05:00
const perms = remoteAdmins.groups[`${idx}-${groupID}`];
2020-10-27 16:31:05 -05:00
if (!(perms.includes('reserve') && perms.length === 1)) {
2020-10-30 16:17:53 -05:00
remoteAdmins.admins[steamID] = `${idx}-${groupID}`;
} else {
2020-10-30 16:17:53 -05:00
remoteAdmins.whitelist[steamID] = `${idx}-${groupID}`;
2020-10-27 16:31:05 -05:00
}
}
/* eslint-enable no-unused-vars */
}
2020-10-30 16:17:53 -05:00
Logger.verbose('SquadServer', 3, 'RemoteAdmins:', remoteAdmins);
return remoteAdmins;
2020-10-27 16:31:05 -05:00
} catch (err) {
console.log(err);
}
}
2020-10-05 12:52:01 -05:00
async updatePlayerList() {
if (this.updatePlayerListTimeout) clearTimeout(this.updatePlayerListTimeout);
2020-05-15 12:42:39 -05:00
try {
const oldPlayerInfo = {};
for (const player of this.players) {
oldPlayerInfo[player.steamID] = player;
}
this.players = (await this.rcon.getListPlayers()).map((player) => ({
...oldPlayerInfo[player.steamID],
...player
}));
for (const player of this.players) {
2020-10-27 16:31:05 -05:00
if (typeof oldPlayerInfo[player.steamID] === 'undefined') continue;
if (player.teamID !== oldPlayerInfo[player.steamID].teamID)
this.emit('PLAYER_TEAM_CHANGE', player);
if (player.squadID !== oldPlayerInfo[player.steamID].squadID)
this.emit('PLAYER_SQUAD_CHANGE', player);
}
} catch (err) {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Failed to update player list.', err);
2020-05-15 12:42:39 -05:00
}
2020-10-05 12:52:01 -05:00
this.updatePlayerListTimeout = setTimeout(this.updatePlayerList, this.updatePlayerListInterval);
}
async updateLayerInformation() {
if (this.updateLayerInformationTimeout) clearTimeout(this.updateLayerInformationTimeout);
try {
const layerInfo = await this.rcon.getLayerInfo();
2020-10-05 12:52:01 -05:00
if (this.layerHistory.length === 0) {
const layer = this.squadLayers.getLayerByLayerName(layerInfo.currentLayer);
2020-10-05 12:52:01 -05:00
this.layerHistory.unshift({ ...layer, time: Date.now() });
this.layerHistory = this.layerHistory.slice(0, this.layerHistoryMaxLength);
}
2020-05-15 12:42:39 -05:00
this.nextLayer = layerInfo.nextLayer;
} catch (err) {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Failed to update layer information.', err);
}
2020-10-05 12:52:01 -05:00
this.updateLayerInformationTimeout = setTimeout(
this.updateLayerInformation,
this.updateLayerInformationInterval
);
}
async updateA2SInformation() {
if (this.updateA2SInformationTimeout) clearTimeout(this.updateA2SInformationTimeout);
try {
const data = await Gamedig.query({
type: 'squad',
host: this.options.host,
port: this.options.queryPort
});
2020-10-05 12:52:01 -05:00
this.serverName = data.name;
2020-10-05 12:52:01 -05:00
this.maxPlayers = parseInt(data.maxplayers);
this.publicSlots = parseInt(data.raw.rules.NUMPUBCONN);
this.reserveSlots = parseInt(data.raw.rules.NUMPRIVCONN);
2020-10-05 12:52:01 -05:00
this.a2sPlayerCount = parseInt(data.raw.rules.PlayerCount_i);
this.publicQueue = parseInt(data.raw.rules.PublicQueue_i);
this.reserveQueue = parseInt(data.raw.rules.ReservedQueue_i);
2020-10-05 12:52:01 -05:00
this.matchTimeout = parseFloat(data.raw.rules.MatchTimeout_f);
this.gameVersion = data.raw.version;
} catch (err) {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err);
}
2020-10-05 12:52:01 -05:00
this.updateA2SInformationTimeout = setTimeout(
this.updateA2SInformation,
this.updateA2SInformationInterval
);
}
async getPlayerByCondition(condition, retry = true) {
let matches;
matches = this.players.filter(condition);
if (matches.length === 1) return matches[0];
if (!retry) return null;
await this.updatePlayerList();
matches = this.players.filter(condition);
if (matches.length === 1) return matches[0];
return null;
2020-05-15 12:42:39 -05:00
}
async getPlayerBySteamID(steamID) {
2020-10-05 12:52:01 -05:00
return this.getPlayerByCondition((player) => player.steamID === steamID);
}
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
async getPlayerByName(name) {
return this.getPlayerByCondition((player) => player.name === name);
}
async getPlayerByNameSuffix(suffix) {
return this.getPlayerByCondition((player) => player.suffix === suffix, false);
}
2020-11-04 17:17:21 -06:00
async pingSquadJSAPI() {
if (this.pingSquadJSAPITimeout) clearTimeout(this.pingSquadJSAPITimeout);
2020-10-05 12:52:01 -05:00
2020-11-04 17:17:21 -06:00
Logger.verbose('SquadServer', 1, 'Pinging SquadJS API...');
2020-05-15 12:42:39 -05:00
2020-11-04 17:17:21 -06:00
const config = {
// send minimal information on server
server: {
host: this.options.host,
queryPort: this.options.queryPort,
logReaderMode: this.options.logReaderMode
},
2020-10-14 11:01:19 -05:00
2020-11-04 17:17:21 -06:00
// we send all plugin information as none of that is sensitive.
plugins: this.plugins.map((plugin) => ({
...plugin.optionsRaw,
plugin: plugin.constructor.name
})),
2020-10-23 05:38:06 -05:00
2020-11-04 17:17:21 -06:00
// send additional information about SquadJS
version: SQUADJS_VERSION
};
2020-10-05 12:52:01 -05:00
2020-11-04 17:17:21 -06:00
try {
const { data } = await axios.post(SQUADJS_API_DOMAIN + '/api/v1/ping', { config });
if (data.error)
Logger.verbose(
'SquadServer',
1,
`Successfully pinged the SquadJS API. Got back error: ${data.error}`
);
else
Logger.verbose(
'SquadServer',
1,
`Successfully pinged the SquadJS API. Got back message: ${data.message}`
);
} catch (err) {
Logger.verbose('SquadServer', 1, 'Failed to ping the SquadJS API: ', err);
}
this.pingSquadJSAPITimeout = setTimeout(this.pingSquadJSAPI, this.pingSquadJSAPIInterval);
2020-10-05 12:52:01 -05:00
}
2020-11-04 17:17:21 -06:00
/// ///////////////////////////////////////////////////////////////////////
// Should consider moving the following to a factory class of some kind. //
// ////////////////////////////////////////////////////////////////////////
static async buildFromConfig(config) {
2020-10-25 09:24:48 -05:00
// Setup logging levels
2020-10-25 09:28:36 -05:00
for (const [module, verboseness] of Object.entries(config.verboseness)) {
2020-10-25 09:24:48 -05:00
Logger.setVerboseness(module, verboseness);
}
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Creating SquadServer...');
2020-10-05 12:52:01 -05:00
const server = new SquadServer(config.server);
// pull layers read to use to create layer filter connectors
await server.squadLayers.pull();
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Preparing connectors...');
2020-10-05 12:52:01 -05:00
const connectors = {};
for (const pluginConfig of config.plugins) {
2020-10-05 13:01:43 -05:00
if (!pluginConfig.enabled) continue;
if (!(pluginConfig.plugin in plugins))
throw new Error(`Plugin "${pluginConfig.plugin}" in config file is not found.`);
2020-10-05 12:52:01 -05:00
const Plugin = plugins[pluginConfig.plugin];
for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) {
// ignore non connectors
if (!option.connector) continue;
2020-10-05 12:52:01 -05:00
if (!(optionName in pluginConfig))
throw new Error(
`${Plugin.name}: ${optionName} (${option.connector} connector) is missing.`
);
const connectorName = pluginConfig[optionName];
// skip already created connectors
if (connectors[connectorName]) continue;
const connectorConfig = config.connectors[connectorName];
if (option.connector === 'discord') {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, `Starting discord connector ${connectorName}...`);
2020-10-05 12:52:01 -05:00
connectors[connectorName] = new Discord.Client();
await connectors[connectorName].login(connectorConfig);
} else if (option.connector === 'mysql') {
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, `Starting mysqlPool connector ${connectorName}...`);
2020-10-05 12:52:01 -05:00
connectors[connectorName] = mysql.createPool(connectorConfig);
} else if (option.connector === 'squadlayerpool') {
2020-10-25 09:28:36 -05:00
Logger.verbose(
'SquadServer',
1,
`Starting squadlayerfilter connector ${connectorName}...`
);
2020-10-05 12:52:01 -05:00
connectors[connectorName] = server.squadLayers[connectorConfig.type](
connectorConfig.filter,
connectorConfig.activeLayerFilter
);
} else if (option.connector === 'remoteAdminLists') {
2020-10-30 16:17:53 -05:00
Logger.verbose('SquadServer', 1, `Starting remoteAdminList connector...`);
connectors[connectorName] = await server.setupAdminList(connectorConfig);
2020-10-05 12:52:01 -05:00
} else {
throw new Error(`${option.connector} is an unsupported connector type.`);
}
2020-05-15 12:42:39 -05:00
}
}
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, 'Applying plugins to SquadServer...');
2020-10-05 12:52:01 -05:00
for (const pluginConfig of config.plugins) {
if (!pluginConfig.enabled) continue;
2020-10-05 12:52:01 -05:00
if (!plugins[pluginConfig.plugin])
throw new Error(`Plugin ${pluginConfig.plugin} does not exist.`);
const Plugin = plugins[pluginConfig.plugin];
2020-10-25 08:59:57 -05:00
Logger.verbose('SquadServer', 1, `Initialising ${Plugin.name}...`);
2020-10-05 12:52:01 -05:00
const options = {};
for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) {
if (option.connector) {
options[optionName] = connectors[pluginConfig[optionName]];
} else {
if (option.required) {
if (!(optionName in pluginConfig))
throw new Error(`${Plugin.name}: ${optionName} is required but missing.`);
if (option.default === pluginConfig[optionName])
throw new Error(
`${Plugin.name}: ${optionName} is required but is the default value.`
);
}
options[optionName] = pluginConfig[optionName] || option.default;
}
}
2020-10-23 05:38:06 -05:00
server.plugins.push(new Plugin(server, options, pluginConfig));
2020-10-05 12:52:01 -05:00
}
2020-05-15 12:42:39 -05:00
2020-10-05 12:52:01 -05:00
return server;
2020-05-15 12:42:39 -05:00
}
2020-10-14 11:01:19 -05:00
2020-11-04 17:17:21 -06:00
static parseConfig(configString) {
try {
2020-11-04 17:17:21 -06:00
return JSON.parse(configString);
} catch (err) {
throw new Error('Unable to parse config file.');
}
2020-11-04 17:17:21 -06:00
}
2020-11-04 17:17:21 -06:00
static buildFromConfigString(configString) {
Logger.verbose('SquadServer', 1, 'Parsing config string...');
return SquadServer.buildFromConfig(SquadServer.parseConfig(configString));
}
2020-11-04 17:17:21 -06:00
static readConfigFile(configPath = './config.json') {
configPath = path.resolve(__dirname, '../', configPath);
if (!fs.existsSync(configPath)) throw new Error('Config file does not exist.');
2020-11-04 17:17:21 -06:00
return fs.readFileSync(configPath, 'utf8');
}
2020-11-04 17:17:21 -06:00
static buildFromConfigFile(configPath) {
Logger.verbose('SquadServer', 1, 'Reading config file...');
return SquadServer.buildFromConfigString(SquadServer.readConfigFile(configPath));
}
2020-11-04 17:17:21 -06:00
static buildConfig() {
const templatePath = path.resolve(__dirname, './templates/config-template.json');
const templateString = fs.readFileSync(templatePath, 'utf8');
const template = SquadServer.parseConfig(templateString);
2020-10-23 05:38:06 -05:00
2020-11-04 17:17:21 -06:00
const pluginKeys = Object.keys(plugins).sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
);
2020-10-23 05:38:06 -05:00
2020-11-04 17:17:21 -06:00
for (const pluginKey of pluginKeys) {
const Plugin = plugins[pluginKey];
2020-10-23 05:38:06 -05:00
2020-11-04 17:17:21 -06:00
const pluginConfig = { plugin: Plugin.name, enabled: Plugin.defaultEnabled };
for (const [optionName, option] of Object.entries(Plugin.optionsSpecification)) {
pluginConfig[optionName] = option.default;
}
2020-10-25 08:42:34 -05:00
2020-11-04 17:17:21 -06:00
template.plugins.push(pluginConfig);
}
2020-10-23 05:38:06 -05:00
2020-11-04 17:17:21 -06:00
return template;
}
2020-10-25 08:59:57 -05:00
2020-11-04 17:17:21 -06:00
static buildConfigFile() {
const configPath = path.resolve(__dirname, '../config.json');
const config = JSON.stringify(SquadServer.buildConfig(), null, 2);
fs.writeFileSync(configPath, config);
}
static buildReadmeFile() {
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 = `<h4>${optionName}${option.required ? ' (Required)' : ''}</h4>
<h6>Description</h6>
<p>${option.description}</p>
<h6>Default</h6>
<pre><code>${
typeof option.default === 'object'
? JSON.stringify(option.default, null, 2)
: option.default
}</code></pre>`;
if (option.example)
optionInfo += `<h6>Example</h6>
<pre><code>${
typeof option.example === 'object'
? JSON.stringify(option.example, null, 2)
: option.example
}</code></pre>`;
options.push(optionInfo);
}
pluginInfo.push(
`<details>
<summary>${Plugin.name}</summary>
<h2>${Plugin.name}</h2>
<p>${Plugin.description}</p>
<h3>Options</h3>
${options.join('\n')}
</details>`
);
2020-10-23 05:38:06 -05:00
}
2020-11-04 17:17:21 -06:00
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);
2020-10-23 05:38:06 -05:00
}
2020-05-15 12:42:39 -05:00
}