mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Switch from ajv to io-ts for config validation; validate configs on save in the API/dashboard; start work on creating io-ts schemas for all plugins
This commit is contained in:
parent
b230a73a6f
commit
da114c0e60
14 changed files with 256 additions and 41 deletions
|
@ -3,6 +3,19 @@ const apiUrl = process.env.API_URL;
|
||||||
|
|
||||||
type QueryParamObject = { [key: string]: string | null };
|
type QueryParamObject = { [key: string]: string | null };
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
protected body: object;
|
||||||
|
protected status: number;
|
||||||
|
protected res: Response;
|
||||||
|
|
||||||
|
constructor(message: string, body: object, status: number, res: Response) {
|
||||||
|
super(message);
|
||||||
|
this.body = body;
|
||||||
|
this.status = status;
|
||||||
|
this.res = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildQueryString(params: QueryParamObject) {
|
function buildQueryString(params: QueryParamObject) {
|
||||||
if (Object.keys(params).length === 0) return "";
|
if (Object.keys(params).length === 0) return "";
|
||||||
|
|
||||||
|
@ -15,7 +28,14 @@ function buildQueryString(params: QueryParamObject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function request(resource, fetchOpts: RequestInit = {}) {
|
export function request(resource, fetchOpts: RequestInit = {}) {
|
||||||
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(res => res.json());
|
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(async res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
throw new ApiError(res.statusText, body, res.status, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get(resource: string, params: QueryParamObject = {}) {
|
export function get(resource: string, params: QueryParamObject = {}) {
|
||||||
|
@ -32,7 +52,7 @@ export function post(resource: string, params: QueryParamObject = {}) {
|
||||||
const headers: Record<string, string> = RootStore.state.auth.apiKey
|
const headers: Record<string, string> = RootStore.state.auth.apiKey
|
||||||
? { "X-Api-Key": RootStore.state.auth.apiKey }
|
? { "X-Api-Key": RootStore.state.auth.apiKey }
|
||||||
: {};
|
: {};
|
||||||
return request(resource + buildQueryString(params), {
|
return request(resource, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -5,8 +5,17 @@
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="title is-1">Config for {{ guild.name }}</h2>
|
<h2 class="title is-1">Config for {{ guild.name }}</h2>
|
||||||
<codemirror v-model="editableConfig" :options="cmConfig"></codemirror>
|
<codemirror v-model="editableConfig" :options="cmConfig"></codemirror>
|
||||||
<button class="button" v-on:click="save" :disabled="saving">Save</button>
|
<div class="editor-footer">
|
||||||
<span v-if="saving">Saving...</span>
|
<div class="actions">
|
||||||
|
<button class="button" v-on:click="save" :disabled="saving">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="status">
|
||||||
|
<div class="status-message" v-if="saving">Saving...</div>
|
||||||
|
<div class="status-message error" v-if="errors.length">
|
||||||
|
<div v-for="error in errors">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -26,6 +35,7 @@
|
||||||
import "codemirror/lib/codemirror.css";
|
import "codemirror/lib/codemirror.css";
|
||||||
import "codemirror/theme/oceanic-next.css";
|
import "codemirror/theme/oceanic-next.css";
|
||||||
import "codemirror/mode/yaml/yaml.js";
|
import "codemirror/mode/yaml/yaml.js";
|
||||||
|
import {ApiError} from "../api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -47,6 +57,7 @@
|
||||||
loading: true,
|
loading: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
editableConfig: null,
|
editableConfig: null,
|
||||||
|
errors: [],
|
||||||
cmConfig: {
|
cmConfig: {
|
||||||
indentWithTabs: false,
|
indentWithTabs: false,
|
||||||
indentUnit: 2,
|
indentUnit: 2,
|
||||||
|
@ -69,10 +80,23 @@
|
||||||
methods: {
|
methods: {
|
||||||
async save() {
|
async save() {
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
await this.$store.dispatch("guilds/saveConfig", {
|
this.errors = [];
|
||||||
guildId: this.$route.params.guildId,
|
|
||||||
config: this.editableConfig,
|
try {
|
||||||
});
|
await this.$store.dispatch("guilds/saveConfig", {
|
||||||
|
guildId: this.$route.params.guildId,
|
||||||
|
config: this.editableConfig,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError && (e.status === 400 || e.status === 422)) {
|
||||||
|
this.errors = e.body.errors || ['Error while saving config'];
|
||||||
|
this.saving = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
this.$router.push("/dashboard");
|
this.$router.push("/dashboard");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,6 @@ Vue.mixin({
|
||||||
|
|
||||||
import App from "./components/App.vue";
|
import App from "./components/App.vue";
|
||||||
import Login from "./components/Login.vue";
|
import Login from "./components/Login.vue";
|
||||||
import { get } from "./api";
|
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
router,
|
router,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get, hasApiKey, post, setApiKey } from "../api";
|
import { get, post } from "../api";
|
||||||
import { ActionTree, Module } from "vuex";
|
import { ActionTree, Module } from "vuex";
|
||||||
import { AuthState, RootState } from "./types";
|
import { AuthState, RootState } from "./types";
|
||||||
|
|
||||||
|
@ -16,13 +16,16 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
|
|
||||||
const storedKey = localStorage.getItem("apiKey");
|
const storedKey = localStorage.getItem("apiKey");
|
||||||
if (storedKey) {
|
if (storedKey) {
|
||||||
const result = await post("auth/validate-key", { key: storedKey });
|
try {
|
||||||
if (result.valid) {
|
const result = await post("auth/validate-key", { key: storedKey });
|
||||||
await dispatch("setApiKey", storedKey);
|
if (result.valid) {
|
||||||
} else {
|
await dispatch("setApiKey", storedKey);
|
||||||
console.log("Unable to validate key, removing from localStorage");
|
return;
|
||||||
localStorage.removeItem("apiKey");
|
}
|
||||||
}
|
} catch (e) {}
|
||||||
|
|
||||||
|
console.log("Unable to validate key, removing from localStorage");
|
||||||
|
localStorage.removeItem("apiKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
commit("markInitialAuthLoaded");
|
commit("markInitialAuthLoaded");
|
||||||
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -1904,6 +1904,12 @@
|
||||||
"integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==",
|
"integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/js-yaml": {
|
||||||
|
"version": "3.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz",
|
||||||
|
"integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.110",
|
"version": "4.14.110",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.110.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.110.tgz",
|
||||||
|
@ -2057,6 +2063,7 @@
|
||||||
"version": "6.7.0",
|
"version": "6.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
|
||||||
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
|
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^2.0.1",
|
"fast-deep-equal": "^2.0.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
@ -3860,12 +3867,14 @@
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-levenshtein": {
|
"fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
|
@ -4002,6 +4011,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||||
},
|
},
|
||||||
|
"fp-ts": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-tw/L1f6hd29bYk3IlhA3qdnqz9LJ13yeecjk0XqC8g9E76P5ZXzNcjXA5zQ0zNlsGhdVhDIHRJB738oGpB8PnA=="
|
||||||
|
},
|
||||||
"fragment-cache": {
|
"fragment-cache": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
|
||||||
|
@ -5057,6 +5071,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
|
||||||
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
|
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
|
||||||
},
|
},
|
||||||
|
"io-ts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-6i8PKyNR/dvEbUU9uE+v4iVFU7l674ZEGQsh92y6xEZF/rj46fXbPy+uPPXJEsCP0J0X3UpzXAxp04K4HR2jVw=="
|
||||||
|
},
|
||||||
"ipaddr.js": {
|
"ipaddr.js": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
||||||
|
@ -7771,7 +7790,8 @@
|
||||||
"json-schema-traverse": {
|
"json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"json-stringify-safe": {
|
"json-stringify-safe": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
@ -9196,7 +9216,8 @@
|
||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
|
@ -10780,6 +10801,7 @@
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
"test": "jest src"
|
"test": "jest src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.7.0",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
|
@ -26,7 +25,9 @@
|
||||||
"eris": "^0.10.0",
|
"eris": "^0.10.0",
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
"express": "^4.17.0",
|
"express": "^4.17.0",
|
||||||
|
"fp-ts": "^2.0.1",
|
||||||
"humanize-duration": "^3.15.0",
|
"humanize-duration": "^3.15.0",
|
||||||
|
"io-ts": "^2.0.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"knub": "^20.1.0",
|
"knub": "^20.1.0",
|
||||||
"last-commit-log": "^2.1.0",
|
"last-commit-log": "^2.1.0",
|
||||||
|
@ -57,6 +58,7 @@
|
||||||
"@types/cors": "^2.8.5",
|
"@types/cors": "^2.8.5",
|
||||||
"@types/express": "^4.16.1",
|
"@types/express": "^4.16.1",
|
||||||
"@types/jest": "^24.0.11",
|
"@types/jest": "^24.0.11",
|
||||||
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/lodash.at": "^4.6.3",
|
"@types/lodash.at": "^4.6.3",
|
||||||
"@types/moment-timezone": "^0.5.6",
|
"@types/moment-timezone": "^0.5.6",
|
||||||
"@types/node": "^10.12.0",
|
"@types/node": "^10.12.0",
|
||||||
|
|
|
@ -3,9 +3,11 @@ import passport from "passport";
|
||||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
import { requireAPIToken } from "./auth";
|
import { requireAPIToken } from "./auth";
|
||||||
import { ApiPermissions } from "../data/ApiPermissions";
|
import { ApiPermissions } from "../data/ApiPermissions";
|
||||||
import { clientError, ok, unauthorized } from "./responses";
|
import { clientError, error, ok, serverError, unauthorized } from "./responses";
|
||||||
import { Configs } from "../data/Configs";
|
import { Configs } from "../data/Configs";
|
||||||
import { ApiRoles } from "../data/ApiRoles";
|
import { ApiRoles } from "../data/ApiRoles";
|
||||||
|
import { validateGuildConfig } from "../configValidator";
|
||||||
|
import yaml, { YAMLException } from "js-yaml";
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const guildAPIRouter = express.Router();
|
const guildAPIRouter = express.Router();
|
||||||
|
@ -35,6 +37,28 @@ export function initGuildsAPI(app: express.Express) {
|
||||||
const config = req.body.config;
|
const config = req.body.config;
|
||||||
if (config == null) return clientError(res, "No config supplied");
|
if (config == null) return clientError(res, "No config supplied");
|
||||||
|
|
||||||
|
// Validate config
|
||||||
|
let parsedConfig;
|
||||||
|
try {
|
||||||
|
parsedConfig = yaml.safeLoad(config);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof YAMLException) {
|
||||||
|
return error(res, e.message, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Error when loading YAML: " + e.message);
|
||||||
|
return serverError(res, "Server error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedConfig == null) {
|
||||||
|
parsedConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = validateGuildConfig(parsedConfig);
|
||||||
|
if (errors) {
|
||||||
|
return res.status(422).json({ errors });
|
||||||
|
}
|
||||||
|
|
||||||
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId);
|
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId);
|
||||||
ok(res);
|
ok(res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function error(res: Response, message: string, statusCode: number = 500)
|
||||||
res.status(statusCode).json({ error: message });
|
res.status(statusCode).json({ error: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serverError(res: Response, message: string) {
|
export function serverError(res: Response, message = "Server error") {
|
||||||
error(res, message, 500);
|
error(res, message, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
src/configValidator.ts
Normal file
51
src/configValidator.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import * as t from "io-ts";
|
||||||
|
import { IPluginOptions } from "knub";
|
||||||
|
import { pipe } from "fp-ts/lib/pipeable";
|
||||||
|
import { fold } from "fp-ts/lib/Either";
|
||||||
|
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||||
|
import { availablePlugins } from "./plugins/availablePlugins";
|
||||||
|
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||||
|
import { validateStrict } from "./validatorUtils";
|
||||||
|
|
||||||
|
const pluginNameToClass = new Map<string, typeof ZeppelinPlugin>();
|
||||||
|
for (const pluginClass of availablePlugins) {
|
||||||
|
// @ts-ignore
|
||||||
|
pluginNameToClass.set(pluginClass.pluginName, pluginClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildConfigRootSchema = t.type({
|
||||||
|
prefix: t.string,
|
||||||
|
levels: t.record(t.string, t.number),
|
||||||
|
plugins: t.record(t.string, t.unknown),
|
||||||
|
});
|
||||||
|
const partialGuildConfigRootSchema = t.exact(t.partial(guildConfigRootSchema.props));
|
||||||
|
|
||||||
|
const globalConfigRootSchema = t.type({
|
||||||
|
url: t.string,
|
||||||
|
owners: t.array(t.string),
|
||||||
|
plugins: t.record(t.string, t.unknown),
|
||||||
|
});
|
||||||
|
|
||||||
|
const partialMegaTest = t.partial({ name: t.string });
|
||||||
|
|
||||||
|
export function validateGuildConfig(config: any): string[] | null {
|
||||||
|
const rootErrors = validateStrict(partialGuildConfigRootSchema, config);
|
||||||
|
if (rootErrors) return rootErrors;
|
||||||
|
|
||||||
|
if (config.plugins) {
|
||||||
|
for (const [pluginName, pluginOptions] of Object.entries(config.plugins)) {
|
||||||
|
if (!pluginNameToClass.has(pluginName)) {
|
||||||
|
return [`Unknown plugin: ${pluginName}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginClass = pluginNameToClass.get(pluginName);
|
||||||
|
let pluginErrors = pluginClass.validateOptions(pluginOptions);
|
||||||
|
if (pluginErrors) {
|
||||||
|
pluginErrors = pluginErrors.map(err => `${pluginName}: ${err}`);
|
||||||
|
return pluginErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -5,20 +5,23 @@ import { GuildAutoReactions } from "../data/GuildAutoReactions";
|
||||||
import { Message } from "eris";
|
import { Message } from "eris";
|
||||||
import { customEmojiRegex, errorMessage, isEmoji, successMessage } from "../utils";
|
import { customEmojiRegex, errorMessage, isEmoji, successMessage } from "../utils";
|
||||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
|
||||||
interface IAutoReactionsPluginConfig {
|
const configSchema = t.type({
|
||||||
can_manage: boolean;
|
can_manage: t.boolean,
|
||||||
}
|
});
|
||||||
|
type TConfigSchema = t.TypeOf<typeof configSchema>;
|
||||||
|
|
||||||
export class AutoReactionsPlugin extends ZeppelinPlugin<IAutoReactionsPluginConfig> {
|
export class AutoReactionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
||||||
public static pluginName = "auto_reactions";
|
public static pluginName = "auto_reactions";
|
||||||
|
protected static configSchema = configSchema;
|
||||||
|
|
||||||
protected savedMessages: GuildSavedMessages;
|
protected savedMessages: GuildSavedMessages;
|
||||||
protected autoReactions: GuildAutoReactions;
|
protected autoReactions: GuildAutoReactions;
|
||||||
|
|
||||||
private onMessageCreateFn;
|
private onMessageCreateFn;
|
||||||
|
|
||||||
getDefaultOptions(): IPluginOptions<IAutoReactionsPluginConfig> {
|
getDefaultOptions(): IPluginOptions<TConfigSchema> {
|
||||||
return {
|
return {
|
||||||
config: {
|
config: {
|
||||||
can_manage: false,
|
can_manage: false,
|
||||||
|
|
39
src/plugins/GlobalZeppelinPlugin.ts
Normal file
39
src/plugins/GlobalZeppelinPlugin.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { GlobalPlugin, IBasePluginConfig, IPluginOptions, logger } from "knub";
|
||||||
|
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||||
|
import * as t from "io-ts";
|
||||||
|
import { pipe } from "fp-ts/lib/pipeable";
|
||||||
|
import { fold } from "fp-ts/lib/Either";
|
||||||
|
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||||
|
import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils";
|
||||||
|
import { Member, User } from "eris";
|
||||||
|
import { performance } from "perf_hooks";
|
||||||
|
import { validateStrict } from "../validatorUtils";
|
||||||
|
|
||||||
|
const SLOW_RESOLVE_THRESHOLD = 1500;
|
||||||
|
|
||||||
|
export class GlobalZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends GlobalPlugin<TConfig> {
|
||||||
|
protected static configSchema: t.TypeC<any>;
|
||||||
|
public static dependencies = [];
|
||||||
|
|
||||||
|
public static validateOptions(options: IPluginOptions): string[] | null {
|
||||||
|
// Validate config values
|
||||||
|
if (this.configSchema) {
|
||||||
|
if (options.config) {
|
||||||
|
const errors = validateStrict(this.configSchema, options.config);
|
||||||
|
if (errors) return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.overrides) {
|
||||||
|
for (const override of options.overrides) {
|
||||||
|
if (override.config) {
|
||||||
|
const errors = validateStrict(this.configSchema, override.config);
|
||||||
|
if (errors) return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,13 @@ import { Plugin, decorators as d, IPluginOptions } from "knub";
|
||||||
import { GuildChannel, Message, TextChannel } from "eris";
|
import { GuildChannel, Message, TextChannel } from "eris";
|
||||||
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||||
import { successMessage } from "../utils";
|
import { successMessage } from "../utils";
|
||||||
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||||
|
|
||||||
interface IMessageSaverPluginConfig {
|
interface IMessageSaverPluginConfig {
|
||||||
can_manage: boolean;
|
can_manage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessageSaverPlugin extends Plugin<IMessageSaverPluginConfig> {
|
export class MessageSaverPlugin extends ZeppelinPlugin<IMessageSaverPluginConfig> {
|
||||||
public static pluginName = "message_saver";
|
public static pluginName = "message_saver";
|
||||||
|
|
||||||
protected savedMessages: GuildSavedMessages;
|
protected savedMessages: GuildSavedMessages;
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { IBasePluginConfig, IPluginOptions, logger, Plugin } from "knub";
|
import { IBasePluginConfig, IPluginOptions, logger, Plugin } from "knub";
|
||||||
import { PluginRuntimeError } from "../PluginRuntimeError";
|
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||||
import Ajv, { ErrorObject } from "ajv";
|
import * as t from "io-ts";
|
||||||
|
import { pipe } from "fp-ts/lib/pipeable";
|
||||||
|
import { fold } from "fp-ts/lib/Either";
|
||||||
|
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||||
import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils";
|
import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils";
|
||||||
import { Member, User } from "eris";
|
import { Member, User } from "eris";
|
||||||
|
|
||||||
import { performance } from "perf_hooks";
|
import { performance } from "perf_hooks";
|
||||||
|
import { validateStrict } from "../validatorUtils";
|
||||||
|
|
||||||
const SLOW_RESOLVE_THRESHOLD = 1500;
|
const SLOW_RESOLVE_THRESHOLD = 1500;
|
||||||
|
|
||||||
export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plugin<TConfig> {
|
export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plugin<TConfig> {
|
||||||
protected static configSchema: any;
|
protected static configSchema: t.TypeC<any>;
|
||||||
public static dependencies = [];
|
public static dependencies = [];
|
||||||
|
|
||||||
protected throwPluginRuntimeError(message: string) {
|
protected throwPluginRuntimeError(message: string) {
|
||||||
|
@ -26,22 +29,19 @@ export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plug
|
||||||
return ourLevel > memberLevel;
|
return ourLevel > memberLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static validateOptions(options: IPluginOptions): ErrorObject[] | null {
|
public static validateOptions(options: any): string[] | null {
|
||||||
// Validate config values
|
// Validate config values
|
||||||
if (this.configSchema) {
|
if (this.configSchema) {
|
||||||
const ajv = new Ajv();
|
|
||||||
const validate = ajv.compile(this.configSchema);
|
|
||||||
|
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
const isValid = validate(options.config);
|
const errors = validateStrict(this.configSchema, options.config);
|
||||||
if (!isValid) return validate.errors;
|
if (errors) return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.overrides) {
|
if (options.overrides) {
|
||||||
for (const override of options.overrides) {
|
for (const override of options.overrides) {
|
||||||
if (override.config) {
|
if (override.config) {
|
||||||
const isValid = validate(override.config);
|
const errors = validateStrict(this.configSchema, override.config);
|
||||||
if (!isValid) return validate.errors;
|
if (errors) return errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
src/validatorUtils.ts
Normal file
27
src/validatorUtils.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import * as t from "io-ts";
|
||||||
|
import { pipe } from "fp-ts/lib/pipeable";
|
||||||
|
import { fold } from "fp-ts/lib/Either";
|
||||||
|
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given value against the given schema while also disallowing extra properties
|
||||||
|
* See: https://github.com/gcanti/io-ts/issues/322
|
||||||
|
*/
|
||||||
|
export function validateStrict(schema: t.Type<any, any, any>, value: any): string[] | null {
|
||||||
|
const validationResult = schema.decode(value);
|
||||||
|
return pipe(
|
||||||
|
validationResult,
|
||||||
|
fold(
|
||||||
|
err => PathReporter.report(validationResult),
|
||||||
|
result => {
|
||||||
|
// Make sure there are no extra properties
|
||||||
|
if (JSON.stringify(value) !== JSON.stringify(result)) {
|
||||||
|
// TODO: Actually mention what the unknown property is
|
||||||
|
return ["Found unknown properties"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue