ZeppelinPluginBlueprint.configSchema is now required. Validate deep partial config schema before running config preprocessor.
This commit is contained in:
parent
3265a2a8da
commit
f6d55f1060
10 changed files with 50 additions and 12 deletions
|
@ -134,7 +134,7 @@ connect().then(async () => {
|
||||||
async getEnabledPlugins(this: Knub, guildId, guildConfig): Promise<string[]> {
|
async getEnabledPlugins(this: Knub, guildId, guildConfig): Promise<string[]> {
|
||||||
const configuredPlugins = guildConfig.plugins || {};
|
const configuredPlugins = guildConfig.plugins || {};
|
||||||
const pluginNames: string[] = Array.from(this.guildPlugins.keys());
|
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 pluginNames.filter(pluginName => {
|
||||||
return configuredPlugins[pluginName] && configuredPlugins[pluginName].enabled !== false;
|
return configuredPlugins[pluginName] && configuredPlugins[pluginName].enabled !== false;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { Member } from "eris";
|
import { Member } from "eris";
|
||||||
import { CommandContext, configUtils, helpers, PluginBlueprint, PluginData, PluginOptions } from "knub";
|
import { CommandContext, configUtils, helpers, PluginBlueprint, PluginData, PluginOptions } from "knub";
|
||||||
import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils";
|
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 { ZeppelinPluginBlueprint } from "./plugins/ZeppelinPluginBlueprint";
|
||||||
import { TZeppelinKnub } from "./types";
|
import { TZeppelinKnub } from "./types";
|
||||||
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index
|
import { ExtendedMatchParams } from "knub/dist/config/PluginConfigManager"; // TODO: Export from Knub index
|
||||||
|
@ -57,15 +57,38 @@ export function getPluginConfigPreprocessor(
|
||||||
customPreprocessor?: PluginBlueprint<any>["configPreprocessor"],
|
customPreprocessor?: PluginBlueprint<any>["configPreprocessor"],
|
||||||
) {
|
) {
|
||||||
return async (options: PluginOptions<any>) => {
|
return async (options: PluginOptions<any>) => {
|
||||||
|
// 1. Validate the basic structure of plugin config
|
||||||
const basicOptionsValidation = validate(BasicPluginStructureType, options);
|
const basicOptionsValidation = validate(BasicPluginStructureType, options);
|
||||||
if (basicOptionsValidation instanceof StrictValidationError) {
|
if (basicOptionsValidation instanceof StrictValidationError) {
|
||||||
throw basicOptionsValidation;
|
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) {
|
if (customPreprocessor) {
|
||||||
options = await customPreprocessor(options);
|
options = await customPreprocessor(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Merge with default options and validate/decode the entire config
|
||||||
let decodedConfig = {};
|
let decodedConfig = {};
|
||||||
const decodedOverrides = [];
|
const decodedOverrides = [];
|
||||||
|
|
||||||
|
@ -79,7 +102,7 @@ export function getPluginConfigPreprocessor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.overrides) {
|
if (options.overrides) {
|
||||||
for (const override of options.overrides || []) {
|
for (const override of options.overrides) {
|
||||||
const overrideConfigMergedWithBaseConfig = configUtils.mergeConfig(options.config, override.config || {});
|
const overrideConfigMergedWithBaseConfig = configUtils.mergeConfig(options.config, override.config || {});
|
||||||
const decodedOverrideConfig = blueprint.configSchema
|
const decodedOverrideConfig = blueprint.configSchema
|
||||||
? decodeAndValidateStrict(blueprint.configSchema, overrideConfigMergedWithBaseConfig)
|
? decodeAndValidateStrict(blueprint.configSchema, overrideConfigMergedWithBaseConfig)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { ChannelArchiverPluginType } from "./types";
|
import { ChannelArchiverPluginType } from "./types";
|
||||||
import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd";
|
import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
|
||||||
export const ChannelArchiverPlugin = zeppelinPlugin<ChannelArchiverPluginType>()("channel_archiver", {
|
export const ChannelArchiverPlugin = zeppelinPlugin<ChannelArchiverPluginType>()("channel_archiver", {
|
||||||
showInDocs: false,
|
showInDocs: false,
|
||||||
|
configSchema: t.type({}),
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
commands: [
|
commands: [
|
||||||
|
|
|
@ -2,8 +2,13 @@ import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { GuildConfigReloaderPluginType } from "./types";
|
import { GuildConfigReloaderPluginType } from "./types";
|
||||||
import { Configs } from "../../data/Configs";
|
import { Configs } from "../../data/Configs";
|
||||||
import { reloadChangedGuilds } from "./functions/reloadChangedGuilds";
|
import { reloadChangedGuilds } from "./functions/reloadChangedGuilds";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
|
||||||
export const GuildConfigReloaderPlugin = zeppelinPlugin<GuildConfigReloaderPluginType>()("guild_config_reloader", {
|
export const GuildConfigReloaderPlugin = zeppelinPlugin<GuildConfigReloaderPluginType>()("guild_config_reloader", {
|
||||||
|
showInDocs: false,
|
||||||
|
|
||||||
|
configSchema: t.type({}),
|
||||||
|
|
||||||
async onLoad(pluginData) {
|
async onLoad(pluginData) {
|
||||||
pluginData.state.guildConfigs = new Configs();
|
pluginData.state.guildConfigs = new Configs();
|
||||||
pluginData.state.highestConfigId = await pluginData.state.guildConfigs.getHighestId();
|
pluginData.state.highestConfigId = await pluginData.state.guildConfigs.getHighestId();
|
||||||
|
|
|
@ -3,10 +3,13 @@ import { PluginData } from "knub";
|
||||||
import { AllowedGuilds } from "src/data/AllowedGuilds";
|
import { AllowedGuilds } from "src/data/AllowedGuilds";
|
||||||
import { GuildInfoSaverPluginType } from "./types";
|
import { GuildInfoSaverPluginType } from "./types";
|
||||||
import { MINUTES } from "src/utils";
|
import { MINUTES } from "src/utils";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
|
||||||
export const GuildInfoSaverPlugin = zeppelinPlugin<GuildInfoSaverPluginType>()("guild_info_saver", {
|
export const GuildInfoSaverPlugin = zeppelinPlugin<GuildInfoSaverPluginType>()("guild_info_saver", {
|
||||||
showInDocs: false,
|
showInDocs: false,
|
||||||
|
|
||||||
|
configSchema: t.type({}),
|
||||||
|
|
||||||
onLoad(pluginData) {
|
onLoad(pluginData) {
|
||||||
const { state, guild } = pluginData;
|
const { state, guild } = pluginData;
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const InitReactionRolesCmd = reactionRolesCmd({
|
||||||
const emojiRolePairs: TReactionRolePair[] = args.reactionRolePairs
|
const emojiRolePairs: TReactionRolePair[] = args.reactionRolePairs
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.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(
|
.map(
|
||||||
(pair): TReactionRolePair => {
|
(pair): TReactionRolePair => {
|
||||||
const customEmojiMatch = pair[0].match(/^<a?:(.*?):(\d+)>$/);
|
const customEmojiMatch = pair[0].match(/^<a?:(.*?):(\d+)>$/);
|
||||||
|
|
|
@ -14,7 +14,6 @@ export const RefreshReactionRolesCmd = reactionRolesCmd({
|
||||||
async run({ message: msg, args, pluginData }) {
|
async run({ message: msg, args, pluginData }) {
|
||||||
const savedMessage = await pluginData.state.savedMessages.find(args.messageId);
|
const savedMessage = await pluginData.state.savedMessages.find(args.messageId);
|
||||||
if (!savedMessage) {
|
if (!savedMessage) {
|
||||||
console.log("ah");
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Unknown message");
|
sendErrorMessage(pluginData, msg.channel, "Unknown message");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,13 @@ import { UsernameHistory } from "src/data/UsernameHistory";
|
||||||
import { Queue } from "src/Queue";
|
import { Queue } from "src/Queue";
|
||||||
import { UsernameSaverPluginType } from "./types";
|
import { UsernameSaverPluginType } from "./types";
|
||||||
import { MessageCreateUpdateUsernameEvt, VoiceChannelJoinUpdateUsernameEvt } from "./events/UpdateUsernameEvts";
|
import { MessageCreateUpdateUsernameEvt, VoiceChannelJoinUpdateUsernameEvt } from "./events/UpdateUsernameEvts";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
|
||||||
export const UsernameSaverPlugin = zeppelinPlugin<UsernameSaverPluginType>()("username_saver", {
|
export const UsernameSaverPlugin = zeppelinPlugin<UsernameSaverPluginType>()("username_saver", {
|
||||||
showInDocs: false,
|
showInDocs: false,
|
||||||
|
|
||||||
|
configSchema: t.type({}),
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
events: [
|
events: [
|
||||||
MessageCreateUpdateUsernameEvt,
|
MessageCreateUpdateUsernameEvt,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TMarkdown } from "../types";
|
||||||
|
|
||||||
export interface ZeppelinPluginBlueprint<TPluginType extends BasePluginType = BasePluginType>
|
export interface ZeppelinPluginBlueprint<TPluginType extends BasePluginType = BasePluginType>
|
||||||
extends PluginBlueprint<TPluginType> {
|
extends PluginBlueprint<TPluginType> {
|
||||||
configSchema?: t.TypeC<any>;
|
configSchema: t.TypeC<any>;
|
||||||
showInDocs?: boolean;
|
showInDocs?: boolean;
|
||||||
|
|
||||||
info?: {
|
info?: {
|
||||||
|
@ -19,21 +19,24 @@ export interface ZeppelinPluginBlueprint<TPluginType extends BasePluginType = Ba
|
||||||
export function zeppelinPlugin<TPartialBlueprint extends Omit<ZeppelinPluginBlueprint, "name">>(
|
export function zeppelinPlugin<TPartialBlueprint extends Omit<ZeppelinPluginBlueprint, "name">>(
|
||||||
name: string,
|
name: string,
|
||||||
blueprint: TPartialBlueprint,
|
blueprint: TPartialBlueprint,
|
||||||
): TPartialBlueprint & { name: string };
|
): TPartialBlueprint & { name: string; configPreprocessor: ZeppelinPluginBlueprint["configPreprocessor"] };
|
||||||
|
|
||||||
export function zeppelinPlugin<TPluginType extends BasePluginType>(): <
|
export function zeppelinPlugin<TPluginType extends BasePluginType>(): <
|
||||||
TPartialBlueprint extends Omit<ZeppelinPluginBlueprint<TPluginType>, "name">
|
TPartialBlueprint extends Omit<ZeppelinPluginBlueprint<TPluginType>, "name">
|
||||||
>(
|
>(
|
||||||
name: string,
|
name: string,
|
||||||
blueprint: TPartialBlueprint,
|
blueprint: TPartialBlueprint,
|
||||||
) => TPartialBlueprint & { name: string; configPreprocessor: PluginBlueprint<TPluginType>["configPreprocessor"] };
|
) => TPartialBlueprint & {
|
||||||
|
name: string;
|
||||||
|
configPreprocessor: ZeppelinPluginBlueprint<TPluginType>["configPreprocessor"];
|
||||||
|
};
|
||||||
|
|
||||||
export function zeppelinPlugin(...args) {
|
export function zeppelinPlugin(...args) {
|
||||||
if (args.length) {
|
if (args.length) {
|
||||||
const blueprint: ZeppelinPluginBlueprint = plugin(...(args as Parameters<typeof plugin>));
|
const blueprint = (plugin(...(args as Parameters<typeof plugin>)) as unknown) as ZeppelinPluginBlueprint;
|
||||||
blueprint.configPreprocessor = getPluginConfigPreprocessor(blueprint, blueprint.configPreprocessor);
|
blueprint.configPreprocessor = getPluginConfigPreprocessor(blueprint, blueprint.configPreprocessor);
|
||||||
return blueprint;
|
return blueprint;
|
||||||
} else {
|
} else {
|
||||||
return zeppelinPlugin;
|
return zeppelinPlugin as (name, blueprint) => ZeppelinPluginBlueprint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import https from "https";
|
||||||
import tmp from "tmp";
|
import tmp from "tmp";
|
||||||
import { helpers } from "knub";
|
import { helpers } from "knub";
|
||||||
import { SavedMessage } from "./data/entities/SavedMessage";
|
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 { either } from "fp-ts/lib/Either";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { SimpleCache } from "./SimpleCache";
|
import { SimpleCache } from "./SimpleCache";
|
||||||
|
@ -103,7 +103,7 @@ export interface TDeepPartialProps<P extends t.Props>
|
||||||
> {}
|
> {}
|
||||||
|
|
||||||
export function tDeepPartial<T>(type: T): TDeepPartial<T> {
|
export function tDeepPartial<T>(type: T): TDeepPartial<T> {
|
||||||
if (type instanceof t.InterfaceType) {
|
if (type instanceof t.InterfaceType || type instanceof t.PartialType) {
|
||||||
const newProps = {};
|
const newProps = {};
|
||||||
for (const [key, prop] of Object.entries(type.props)) {
|
for (const [key, prop] of Object.entries(type.props)) {
|
||||||
newProps[key] = tDeepPartial(prop);
|
newProps[key] = tDeepPartial(prop);
|
||||||
|
|
Loading…
Add table
Reference in a new issue