diff --git a/backend/src/index.ts b/backend/src/index.ts index dee095cd..ad2b882e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -134,7 +134,7 @@ connect().then(async () => { async getEnabledPlugins(this: Knub, guildId, guildConfig): Promise { const configuredPlugins = guildConfig.plugins || {}; const pluginNames: string[] = Array.from(this.guildPlugins.keys()); - const plugins: ZeppelinPlugin[] = Array.from(this.guildPlugins.values()); + const plugins = Array.from(this.guildPlugins.values()) as ZeppelinPlugin[]; return pluginNames.filter(pluginName => { return configuredPlugins[pluginName] && configuredPlugins[pluginName].enabled !== false; diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 02cf4f42..23b1ca61 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -5,7 +5,7 @@ import { Member } from "eris"; import { CommandContext, configUtils, helpers, PluginBlueprint, PluginData, PluginOptions } from "knub"; import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils"; -import { deepKeyIntersect, errorMessage, successMessage, tNullable } from "./utils"; +import { deepKeyIntersect, errorMessage, successMessage, tDeepPartial, tNullable } from "./utils"; import { ZeppelinPluginBlueprint } from "./plugins/ZeppelinPluginBlueprint"; import { TZeppelinKnub } from "./types"; import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index @@ -57,15 +57,38 @@ export function getPluginConfigPreprocessor( customPreprocessor?: PluginBlueprint["configPreprocessor"], ) { return async (options: PluginOptions) => { + // 1. Validate the basic structure of plugin config const basicOptionsValidation = validate(BasicPluginStructureType, options); if (basicOptionsValidation instanceof StrictValidationError) { throw basicOptionsValidation; } + // 2. Validate config/overrides against *partial* config schema. This ensures valid properties have valid types. + const partialConfigSchema = tDeepPartial(blueprint.configSchema); + // if (blueprint.name === "automod") console.log(partialConfigSchema); + + if (options.config) { + const partialConfigValidation = validate(partialConfigSchema, options.config); + if (partialConfigValidation instanceof StrictValidationError) { + throw partialConfigValidation; + } + } + + if (options.overrides) { + for (const override of options.overrides) { + const partialOverrideConfigValidation = validate(partialConfigSchema, override.config || {}); + if (partialOverrideConfigValidation) { + throw partialOverrideConfigValidation; + } + } + } + + // 3. Run custom preprocessor, if any if (customPreprocessor) { options = await customPreprocessor(options); } + // 4. Merge with default options and validate/decode the entire config let decodedConfig = {}; const decodedOverrides = []; @@ -79,7 +102,7 @@ export function getPluginConfigPreprocessor( } if (options.overrides) { - for (const override of options.overrides || []) { + for (const override of options.overrides) { const overrideConfigMergedWithBaseConfig = configUtils.mergeConfig(options.config, override.config || {}); const decodedOverrideConfig = blueprint.configSchema ? decodeAndValidateStrict(blueprint.configSchema, overrideConfigMergedWithBaseConfig) diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts index 599977db..e7012134 100644 --- a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts +++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts @@ -1,9 +1,11 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; import { ChannelArchiverPluginType } from "./types"; import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd"; +import * as t from "io-ts"; export const ChannelArchiverPlugin = zeppelinPlugin()("channel_archiver", { showInDocs: false, + configSchema: t.type({}), // prettier-ignore commands: [ diff --git a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts index 3265090b..c89d2277 100644 --- a/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts +++ b/backend/src/plugins/GuildConfigReloader/GuildConfigReloaderPlugin.ts @@ -2,8 +2,13 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; import { GuildConfigReloaderPluginType } from "./types"; import { Configs } from "../../data/Configs"; import { reloadChangedGuilds } from "./functions/reloadChangedGuilds"; +import * as t from "io-ts"; export const GuildConfigReloaderPlugin = zeppelinPlugin()("guild_config_reloader", { + showInDocs: false, + + configSchema: t.type({}), + async onLoad(pluginData) { pluginData.state.guildConfigs = new Configs(); pluginData.state.highestConfigId = await pluginData.state.guildConfigs.getHighestId(); diff --git a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts index 29649dab..2d4b4ee3 100644 --- a/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts +++ b/backend/src/plugins/GuildInfoSaver/GuildInfoSaverPlugin.ts @@ -3,10 +3,13 @@ import { PluginData } from "knub"; import { AllowedGuilds } from "src/data/AllowedGuilds"; import { GuildInfoSaverPluginType } from "./types"; import { MINUTES } from "src/utils"; +import * as t from "io-ts"; export const GuildInfoSaverPlugin = zeppelinPlugin()("guild_info_saver", { showInDocs: false, + configSchema: t.type({}), + onLoad(pluginData) { const { state, guild } = pluginData; diff --git a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts index 8f39258d..0eba8df7 100644 --- a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts +++ b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts @@ -46,7 +46,7 @@ export const InitReactionRolesCmd = reactionRolesCmd({ const emojiRolePairs: TReactionRolePair[] = args.reactionRolePairs .trim() .split("\n") - .map(v => v.split("=").map(v => v.trim())) // tslint:disable-line + .map(v => v.split(/(\s|[=,])+/).map(v => v.trim())) // tslint:disable-line .map( (pair): TReactionRolePair => { const customEmojiMatch = pair[0].match(/^$/); diff --git a/backend/src/plugins/ReactionRoles/commands/RefreshReactionRolesCmd.ts b/backend/src/plugins/ReactionRoles/commands/RefreshReactionRolesCmd.ts index 28039503..0a9fe103 100644 --- a/backend/src/plugins/ReactionRoles/commands/RefreshReactionRolesCmd.ts +++ b/backend/src/plugins/ReactionRoles/commands/RefreshReactionRolesCmd.ts @@ -14,7 +14,6 @@ export const RefreshReactionRolesCmd = reactionRolesCmd({ async run({ message: msg, args, pluginData }) { const savedMessage = await pluginData.state.savedMessages.find(args.messageId); if (!savedMessage) { - console.log("ah"); sendErrorMessage(pluginData, msg.channel, "Unknown message"); return; } diff --git a/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts index e5fa8b05..eb8dae81 100644 --- a/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts +++ b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts @@ -3,10 +3,13 @@ import { UsernameHistory } from "src/data/UsernameHistory"; import { Queue } from "src/Queue"; import { UsernameSaverPluginType } from "./types"; import { MessageCreateUpdateUsernameEvt, VoiceChannelJoinUpdateUsernameEvt } from "./events/UpdateUsernameEvts"; +import * as t from "io-ts"; export const UsernameSaverPlugin = zeppelinPlugin()("username_saver", { showInDocs: false, + configSchema: t.type({}), + // prettier-ignore events: [ MessageCreateUpdateUsernameEvt, diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts index 671ada10..e36623b3 100644 --- a/backend/src/plugins/ZeppelinPluginBlueprint.ts +++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts @@ -5,7 +5,7 @@ import { TMarkdown } from "../types"; export interface ZeppelinPluginBlueprint extends PluginBlueprint { - configSchema?: t.TypeC; + configSchema: t.TypeC; showInDocs?: boolean; info?: { @@ -19,21 +19,24 @@ export interface ZeppelinPluginBlueprint>( name: string, blueprint: TPartialBlueprint, -): TPartialBlueprint & { name: string }; +): TPartialBlueprint & { name: string; configPreprocessor: ZeppelinPluginBlueprint["configPreprocessor"] }; export function zeppelinPlugin(): < TPartialBlueprint extends Omit, "name"> >( name: string, blueprint: TPartialBlueprint, -) => TPartialBlueprint & { name: string; configPreprocessor: PluginBlueprint["configPreprocessor"] }; +) => TPartialBlueprint & { + name: string; + configPreprocessor: ZeppelinPluginBlueprint["configPreprocessor"]; +}; export function zeppelinPlugin(...args) { if (args.length) { - const blueprint: ZeppelinPluginBlueprint = plugin(...(args as Parameters)); + const blueprint = (plugin(...(args as Parameters)) as unknown) as ZeppelinPluginBlueprint; blueprint.configPreprocessor = getPluginConfigPreprocessor(blueprint, blueprint.configPreprocessor); return blueprint; } else { - return zeppelinPlugin; + return zeppelinPlugin as (name, blueprint) => ZeppelinPluginBlueprint; } } diff --git a/backend/src/utils.ts b/backend/src/utils.ts index ee225c1e..31b04d8e 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -29,7 +29,7 @@ import https from "https"; import tmp from "tmp"; import { helpers } from "knub"; import { SavedMessage } from "./data/entities/SavedMessage"; -import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; +import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils"; import { either } from "fp-ts/lib/Either"; import moment from "moment-timezone"; import { SimpleCache } from "./SimpleCache"; @@ -103,7 +103,7 @@ export interface TDeepPartialProps

> {} export function tDeepPartial(type: T): TDeepPartial { - if (type instanceof t.InterfaceType) { + if (type instanceof t.InterfaceType || type instanceof t.PartialType) { const newProps = {}; for (const [key, prop] of Object.entries(type.props)) { newProps[key] = tDeepPartial(prop);