3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Merge branch 'dashboard'

This commit is contained in:
Dragory 2019-06-22 18:52:54 +03:00
commit 1dae3019c4
50 changed files with 9552 additions and 57 deletions

2
.gitignore vendored
View file

@ -71,7 +71,9 @@ desktop.ini
# Compiled files
/dist
/dist-frontend
# Misc
/convert.js
/startscript.js
/.cache

8
dashboard/.babelrc Normal file
View file

@ -0,0 +1,8 @@
{
"plugins": [
["transform-runtime", {
"regenerator": true
}],
"transform-object-rest-spread"
]
}

3
dashboard/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.cache
/dist
/node_modules

7668
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
dashboard/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "zeppelin-dashboard",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"build": "rimraf dist && parcel build src/index.html --out-dir dist",
"watch": "parcel src/index.html"
},
"devDependencies": {
"@vue/component-compiler-utils": "^3.0.0",
"babel-core": "^6.26.3",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"parcel-bundler": "^1.12.3",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"js-cookie": "^2.2.0",
"vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.3",
"vue-router": "^3.0.6"
},
"browserslist": [
"last 2 Chrome versions"
]
}

53
dashboard/src/api.ts Normal file
View file

@ -0,0 +1,53 @@
const apiUrl = process.env.API_URL;
type QueryParamObject = { [key: string]: string | null };
function buildQueryString(params: QueryParamObject) {
if (Object.keys(params).length === 0) return "";
return (
"?" +
Array.from(Object.entries(params))
.map(pair => `${encodeURIComponent(pair[0])}=${encodeURIComponent(pair[1] || "")}`)
.join("&")
);
}
let apiKey = null;
export function setApiKey(newKey) {
apiKey = newKey;
}
export function hasApiKey() {
return apiKey != null;
}
export function resetApiKey() {
apiKey = null;
}
export function request(resource, fetchOpts: RequestInit = {}) {
return fetch(`${apiUrl}/${resource}`, fetchOpts)
.then(res => res.json())
.catch(err => {
console.log(err);
});
}
export function get(resource: string, params: QueryParamObject = {}) {
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
return request(resource + buildQueryString(params), {
method: "GET",
headers,
});
}
export function post(resource: string, params: QueryParamObject = {}) {
const headers: Record<string, string> = apiKey ? { "X-Api-Key": (apiKey as unknown) as string } : {};
return request(resource + buildQueryString(params), {
method: "POST",
body: JSON.stringify(params),
headers: {
...headers,
"Content-Type": "application/json",
},
});
}

15
dashboard/src/auth.ts Normal file
View file

@ -0,0 +1,15 @@
import { NavigationGuard } from "vue-router";
import { RootStore } from "./store";
export const authGuard: NavigationGuard = async (to, from, next) => {
if (RootStore.state.auth.apiKey) return next(); // We have an API key -> authenticated
if (RootStore.state.auth.loadedInitialAuth) return next("/login"); // No API key and initial auth data was already loaded -> not authenticated
await RootStore.dispatch("auth/loadInitialAuth"); // Initial auth data wasn't loaded yet (per above check) -> load it now
if (RootStore.state.auth.apiKey) return next();
next("/login"); // Still no API key -> not authenticated
};
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
next("/dashboard");
};

View file

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

View file

@ -0,0 +1,36 @@
<template>
<div v-if="loading">
Loading...
</div>
<div v-else>
<h1>Guilds</h1>
<table v-for="guild in guilds">
<tr>
<td>{{ guild.id }}</td>
<td>{{ guild.name }}</td>
<td>
<a v-bind:href="'/dashboard/guilds/' + guild.id + '/config'">Config</a>
</td>
</tr>
</table>
</div>
</template>
<script>
import {mapGetters, mapState} from "vuex";
import {LoadStatus} from "../store/types";
export default {
mounted() {
this.$store.dispatch("guilds/loadAvailableGuilds");
},
computed: {
loading() {
return this.$state.guilds.availableGuildsLoadStatus !== LoadStatus.Done;
},
...mapState({
guilds: 'guilds/available',
}),
},
};
</script>

View file

@ -0,0 +1,11 @@
<template>
<p>Redirecting...</p>
</template>
<script>
export default {
created() {
this.$router.push('/login');
}
};
</script>

View file

@ -0,0 +1,14 @@
<template>
<a v-bind:href="env.API_URL + '/auth/login'">Login</a>
</template>
<script>
export default {
computed: {
blah() {
return this.$state.apiKey;
}
}
};
</script>

15
dashboard/src/index.html Normal file
View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Zeppelin Dashboard</title>
</head>
<body>
<div id="app"></div>
<script src="./main.ts"></script>
</body>
</html>

31
dashboard/src/main.ts Normal file
View file

@ -0,0 +1,31 @@
import Vue from "vue";
import { RootStore } from "./store";
import { router } from "./routes";
get("/foo", { bar: "baz" });
// Set up a read-only global variable to access specific env vars
Vue.mixin({
data() {
return {
get env() {
return Object.freeze({
API_URL: process.env.API_URL,
});
},
};
},
});
import App from "./components/App.vue";
import Login from "./components/Login.vue";
import { get } from "./api";
const app = new Vue({
router,
store: RootStore,
el: "#app",
render(h) {
return h(App);
},
});

27
dashboard/src/routes.ts Normal file
View file

@ -0,0 +1,27 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Index from "./components/Index.vue";
import Login from "./components/Login.vue";
import LoginCallback from "./components/LoginCallback.vue";
import Dashboard from "./components/Dashboard.vue";
import store from "./store";
import { authGuard, loginCallbackGuard } from "./auth";
Vue.use(VueRouter);
const publicRoutes: RouteConfig[] = [
{ path: "/", component: Index },
{ path: "/login", component: Login },
{ path: "/login-callback", beforeEnter: loginCallbackGuard },
];
const authenticatedRoutes: RouteConfig[] = [{ path: "/dashboard", component: Dashboard }];
authenticatedRoutes.forEach(route => {
route.beforeEnter = authGuard;
});
export const router = new VueRouter({
mode: "history",
routes: [...publicRoutes, ...authenticatedRoutes],
});

View file

@ -0,0 +1,46 @@
import { get, hasApiKey, post, setApiKey } from "../api";
import { ActionTree, Module } from "vuex";
import { AuthState, RootState } from "./types";
export const AuthStore: Module<AuthState, RootState> = {
namespaced: true,
state: {
apiKey: null,
loadedInitialAuth: false,
},
actions: {
async loadInitialAuth({ dispatch, commit, state }) {
if (state.loadedInitialAuth) return;
const storedKey = localStorage.getItem("apiKey");
if (storedKey) {
console.log("key?", storedKey);
const result = await post("auth/validate-key", { key: storedKey });
if (result.isValid) {
await dispatch("setApiKey", storedKey);
} else {
localStorage.removeItem("apiKey");
}
}
commit("markInitialAuthLoaded");
},
setApiKey({ commit, state }, newKey: string) {
localStorage.setItem("apiKey", newKey);
commit("setApiKey", newKey);
},
},
mutations: {
setApiKey(state: AuthState, key) {
state.apiKey = key;
},
markInitialAuthLoaded(state: AuthState) {
state.loadedInitialAuth = true;
},
},
};

View file

@ -0,0 +1,34 @@
import { get } from "../api";
import { Module } from "vuex";
import { GuildState, LoadStatus, RootState } from "./types";
export const GuildStore: Module<GuildState, RootState> = {
namespaced: true,
state: {
availableGuildsLoadStatus: LoadStatus.None,
available: [],
configs: {},
},
actions: {
async loadAvailableGuilds({ dispatch, commit, state }) {
if (state.availableGuildsLoadStatus !== LoadStatus.None) return;
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
const availableGuilds = await get("guilds/available");
commit("setAvailableGuilds", availableGuilds);
},
},
mutations: {
setAvailableGuildsLoadStatus(state: GuildState, status: LoadStatus) {
state.availableGuildsLoadStatus = status;
},
setAvailableGuilds(state: GuildState, guilds) {
state.available = guilds;
state.availableGuildsLoadStatus = LoadStatus.Done;
},
},
};

View file

@ -0,0 +1,28 @@
import Vue from "vue";
import Vuex, { Store } from "vuex";
Vue.use(Vuex);
import { RootState } from "./types";
import { AuthStore } from "./auth";
import { GuildStore } from "./guilds";
export const RootStore = new Vuex.Store<RootState>({
modules: {
auth: AuthStore,
guilds: GuildStore,
},
});
// Set up typings so Vue/our components know about the state's types
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<RootState>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<RootState>;
}
}

View file

@ -0,0 +1,27 @@
export enum LoadStatus {
None = 1,
Loading,
Done,
}
export interface AuthState {
apiKey: string | null;
loadedInitialAuth: boolean;
}
export interface GuildState {
availableGuildsLoadStatus: LoadStatus;
available: Array<{
guild_id: string;
name: string;
icon: string | null;
}>;
configs: {
[key: string]: string;
};
}
export type RootState = {
auth: AuthState;
guilds: GuildState;
};

4
dashboard/ts-vue-shim.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

28
dashboard/tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2015",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "esnext",
"strict": true,
"lib": [
"es2017",
"esnext",
"dom"
],
"baseUrl": "./",
"resolveJsonModule": true,
"esModuleInterop": true,
"outDir": "./dist"
},
"include": [
"src/**/*.ts",
"src/**/*.vue"
],
"files": [
"ts-vue-shim.d.ts"
]
}

5
nodemon-api.json Normal file
View file

@ -0,0 +1,5 @@
{
"watch": "src",
"ext": "ts",
"exec": "ts-node ./src/api/index.ts"
}

563
package-lock.json generated
View file

@ -1834,6 +1834,55 @@
"@babel/types": "^7.3.0"
}
},
"@types/body-parser": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
"integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
"integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-GmK8AKu8i+s+EChK/uZ5IbrXPcPaQKWaNSGevDT/7o3gFObwSUQwqb1jMqxuo+YPvj0ckGzINI+EO7EHcmJjKg==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/express": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz",
"integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.6.tgz",
"integrity": "sha512-8wr3CA/EMybyb6/V8qvTRKiNkPmgUA26uA9XWD6hlA0yFDuqi4r2L0C2B0U2HAYltJamoYJszlkaWM31vrKsHg==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/range-parser": "*"
}
},
"@types/istanbul-lib-coverage": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz",
@ -1858,20 +1907,29 @@
"@types/lodash": {
"version": "4.14.110",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.110.tgz",
"integrity": "sha512-iXYLa6olt4tnsCA+ZXeP6eEW3tk1SulWeYyP/yooWfAtXjozqXgtX4+XUtMuOCfYjKGz3F34++qUc3Q+TJuIIw=="
"integrity": "sha512-iXYLa6olt4tnsCA+ZXeP6eEW3tk1SulWeYyP/yooWfAtXjozqXgtX4+XUtMuOCfYjKGz3F34++qUc3Q+TJuIIw==",
"dev": true
},
"@types/lodash.at": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/@types/lodash.at/-/lodash.at-4.6.3.tgz",
"integrity": "sha512-I9uzVJxGEvHRTsXI/1AUjkaJOxWdYRE5pOoUUJxfbq5N4y071Y2CyGDrxmNwfYJosCmX3jF+2Zhrw7A6pQc7pg==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
"dev": true
},
"@types/moment-timezone": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.6.tgz",
"integrity": "sha512-rMZjLmXs9sly1UbwxckyAEvQkrwrGqR24nFAjFrndRJBBnUooCCD0LPmdRcf9haHXFnckI9E3ko0oC6LEDk7dw==",
"dev": true,
"requires": {
"moment": ">=2.14.0"
}
@ -1881,6 +1939,51 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.0.tgz",
"integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ=="
},
"@types/oauth": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz",
"integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/passport": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.0.tgz",
"integrity": "sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/passport-oauth2": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.8.tgz",
"integrity": "sha512-tlX16wyFE5YJR2pHpZ308dgB1MV9/Ra2wfQh71eWk+/umPoD1Rca2D4N5M27W7nZm1wqUNGTk1I864nHvEgiFA==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/oauth": "*",
"@types/passport": "*"
}
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -1911,6 +2014,15 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
@ -2058,6 +2170,11 @@
"integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=",
"dev": true
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@ -2376,6 +2493,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -2404,6 +2526,38 @@
"integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
"dev": true
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
"boxen": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
@ -2521,6 +2675,11 @@
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -2867,6 +3026,19 @@
"xdg-basedir": "^3.0.0"
}
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"convert-source-map": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
@ -2876,6 +3048,16 @@
"safe-buffer": "~5.1.1"
}
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@ -2887,6 +3069,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"cosmiconfig": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz",
@ -3117,6 +3308,16 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-newline": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
@ -3187,6 +3388,11 @@
"safer-buffer": "^2.1.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-to-chromium": {
"version": "1.3.116",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz",
@ -3204,6 +3410,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.1.tgz",
"integrity": "sha512-cjx7oFbFIyZMpmWaEBnKeJXWAVzjXwK6yHiz/5X73A2Ww4pnabw+4ZaA/MxLroIQQrB3dL6XzEz8P3aZsSdj8Q=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@ -3213,8 +3424,9 @@
}
},
"eris": {
"version": "github:abalabahaha/eris#0509d444813d49c320be3b38fd2e05f7d4ae2078",
"from": "github:abalabahaha/eris#dev",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/eris/-/eris-0.10.0.tgz",
"integrity": "sha512-xAvmD4wsE5mwuiP+wy8RiarjiuwCylSsglKqru4J4sk0/WGOnSOfEZf43YLx/TcF4J1D4B2VMTq38446Bk1x1Q==",
"requires": {
"opusscript": "^0.0.4",
"tweetnacl": "^1.0.0",
@ -3255,6 +3467,11 @@
"is-symbol": "^1.0.2"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -3305,6 +3522,11 @@
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"event-stream": {
"version": "3.3.4",
"resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
@ -3485,6 +3707,58 @@
}
}
},
"express": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.0.tgz",
"integrity": "sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -3656,6 +3930,30 @@
}
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"find-parent-dir": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz",
@ -3699,6 +3997,11 @@
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@ -3708,6 +4011,11 @@
"map-cache": "^0.2.2"
}
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
@ -4542,6 +4850,18 @@
"whatwg-encoding": "^1.0.1"
}
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -4651,7 +4971,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@ -4738,6 +5057,11 @@
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
},
"ipaddr.js": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
"integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
},
"is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@ -7777,11 +8101,6 @@
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
},
"lodash.has": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
"integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI="
},
"lodash.intersection": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz",
@ -7797,6 +8116,11 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@ -7920,6 +8244,11 @@
"escape-string-regexp": "^1.0.4"
}
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
@ -7928,6 +8257,11 @@
"mimic-fn": "^1.0.0"
}
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"merge-stream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
@ -7937,6 +8271,11 @@
"readable-stream": "^2.0.1"
}
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@ -7958,17 +8297,20 @@
"to-regex": "^3.0.2"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
"dev": true
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"dev": true,
"requires": {
"mime-db": "1.40.0"
}
@ -8098,6 +8440,11 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"neo-async": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
@ -8221,6 +8568,11 @@
"integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==",
"dev": true
},
"oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@ -8297,6 +8649,14 @@
"isobject": "^3.0.1"
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -8488,12 +8848,51 @@
"@types/node": "*"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascalcase": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
"passport": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
"integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-custom": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.0.5.tgz",
"integrity": "sha1-LR2cF0pqRoW/Aom85hCRzV7HsPQ=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-oauth2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz",
"integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==",
"requires": {
"base64url": "3.x.x",
"oauth": "0.9.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
@ -8527,6 +8926,11 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@ -8536,6 +8940,11 @@
"pify": "^3.0.0"
}
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
@ -8737,6 +9146,15 @@
"integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==",
"dev": true
},
"proxy-addr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
"integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.0"
}
},
"ps-tree": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz",
@ -8786,6 +9204,22 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@ -9199,8 +9633,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sane": {
"version": "4.1.0",
@ -9283,6 +9716,59 @@
"semver": "^5.0.3"
}
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@ -9317,6 +9803,11 @@
}
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -9651,6 +10142,11 @@
}
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
@ -9898,6 +10394,11 @@
"repeat-string": "^1.6.1"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
@ -10034,6 +10535,15 @@
"prelude-ls": "~1.1.2"
}
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"typeorm": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.14.tgz",
@ -10094,6 +10604,11 @@
}
}
},
"uid2": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
},
"undefsafe": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz",
@ -10186,6 +10701,11 @@
"crypto-random-string": "^1.0.0"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unset-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
@ -10303,6 +10823,11 @@
"object.getownpropertydescriptors": "^2.0.3"
}
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
@ -10326,6 +10851,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@ -10337,6 +10867,11 @@
"extsprintf": "^1.2.0"
}
},
"vuex": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.1.tgz",
"integrity": "sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg=="
},
"w3c-hr-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",

View file

@ -4,54 +4,64 @@
"description": "",
"private": true,
"scripts": {
"test": "jest src",
"start-dev": "ts-node src/index.ts",
"start-watch": "nodemon",
"format": "prettier --write \"./**/*.ts\"",
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
"start-bot-dev": "ts-node src/index.ts",
"start-bot-prod": "cross-env NODE_ENV=production node dist/index.js",
"watch-bot": "nodemon --config nodemon-bot.json",
"build": "rimraf dist && tsc",
"start-prod": "cross-env NODE_ENV=production node dist/index.js",
"migrate": "npm run typeorm -- migration:run"
"start-api-dev": "ts-node src/api/index.ts",
"start-api-prod": "cross-env NODE_ENV=production node dist/api/index.js",
"watch-api": "nodemon --config nodemon-api.json",
"format": "prettier --write \"./src/**/*.ts\"",
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
"migrate": "npm run typeorm -- migration:run",
"migrate-rollback": "npm run typeorm -- migration:revert",
"test": "jest src"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/lodash.at": "^4.6.3",
"@types/moment-timezone": "^0.5.6",
"ajv": "^6.7.0",
"cors": "^2.8.5",
"cross-env": "^5.2.0",
"dotenv": "^4.0.0",
"emoji-regex": "^7.0.1",
"eris": "github:abalabahaha/eris#dev",
"eris": "^0.10.0",
"escape-string-regexp": "^1.0.5",
"express": "^4.17.0",
"humanize-duration": "^3.15.0",
"js-yaml": "^3.13.1",
"knub": "^20.1.0",
"last-commit-log": "^2.1.0",
"lodash.at": "^4.6.0",
"lodash.chunk": "^4.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.difference": "^4.5.0",
"lodash.has": "^4.5.2",
"lodash.intersection": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.pick": "^4.4.0",
"moment-timezone": "^0.5.21",
"mysql": "^2.16.0",
"passport": "^0.4.0",
"passport-custom": "^1.0.5",
"passport-oauth2": "^1.5.0",
"reflect-metadata": "^0.1.12",
"tlds": "^1.203.1",
"tmp": "0.0.33",
"ts-node": "^3.3.0",
"typeorm": "^0.2.14",
"typescript": "^3.3.3333",
"uuid": "^3.3.2"
"uuid": "^3.3.2",
"vuex": "^3.1.1"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-typescript": "^7.3.3",
"@types/cors": "^2.8.5",
"@types/express": "^4.16.1",
"@types/jest": "^24.0.11",
"@types/lodash.at": "^4.6.3",
"@types/moment-timezone": "^0.5.6",
"@types/node": "^10.12.0",
"@types/passport": "^1.0.0",
"@types/passport-oauth2": "^1.4.8",
"@types/tmp": "0.0.33",
"babel-jest": "^24.5.0",
"husky": "^1.3.1",

View file

@ -3,7 +3,7 @@
{
"name": "zeppelin",
"script": "npm",
"args": "run start-prod",
"args": "run start-bot-prod",
"log_date_format": "YYYY-MM-DD HH:mm:ss"
}
]

119
src/api/auth.ts Normal file
View file

@ -0,0 +1,119 @@
import express, { Request, Response } from "express";
import passport from "passport";
import OAuth2Strategy from "passport-oauth2";
import CustomStrategy from "passport-custom";
import { DashboardLogins, DashboardLoginUserData } from "../data/DashboardLogins";
import pick from "lodash.pick";
import https from "https";
const DISCORD_API_URL = "https://discordapp.com/api";
function simpleDiscordAPIRequest(bearerToken, path): Promise<any> {
return new Promise((resolve, reject) => {
const request = https.get(
`${DISCORD_API_URL}/${path}`,
{
headers: {
Authorization: `Bearer ${bearerToken}`,
},
},
res => {
if (res.statusCode !== 200) {
reject(new Error(`Discord API error ${res.statusCode}`));
return;
}
res.on("data", data => resolve(JSON.parse(data)));
},
);
request.on("error", err => reject(err));
});
}
export function initAuth(app: express.Express) {
app.use(passport.initialize());
if (!process.env.CLIENT_ID) {
throw new Error("Auth: CLIENT ID missing");
}
if (!process.env.CLIENT_SECRET) {
throw new Error("Auth: CLIENT SECRET missing");
}
if (!process.env.OAUTH_CALLBACK_URL) {
throw new Error("Auth: OAUTH CALLBACK URL missing");
}
if (!process.env.DASHBOARD_URL) {
throw new Error("DASHBOARD_URL missing!");
}
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
const dashboardLogins = new DashboardLogins();
// Initialize API tokens
passport.use(
"api-token",
new CustomStrategy(async (req, cb) => {
console.log("in api-token strategy");
const apiKey = req.header("X-Api-Key");
if (!apiKey) return cb();
const userId = await dashboardLogins.getUserIdByApiKey(apiKey);
if (userId) {
cb(null, { userId });
}
cb();
}),
);
// Initialize OAuth2 for Discord login
passport.use(
new OAuth2Strategy(
{
authorizationURL: "https://discordapp.com/api/oauth2/authorize",
tokenURL: "https://discordapp.com/api/oauth2/token",
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.OAUTH_CALLBACK_URL,
scope: ["identify"],
},
async (accessToken, refreshToken, profile, cb) => {
const user = await simpleDiscordAPIRequest(accessToken, "users/@me");
const userData = pick(user, ["username", "discriminator", "avatar"]) as DashboardLoginUserData;
const apiKey = await dashboardLogins.addLogin(user.id, userData);
// TODO: Revoke access token, we don't need it anymore
console.log("done, calling cb with", apiKey);
cb(null, { apiKey });
},
),
);
app.get("/auth/login", passport.authenticate("oauth2"));
app.get(
"/auth/oauth-callback",
passport.authenticate("oauth2", { failureRedirect: "/", session: false }),
(req, res) => {
console.log("redirecting to a non-existent page haHAA");
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
},
);
app.post("/auth/validate-key", async (req: Request, res: Response) => {
const key = req.params.key || req.query.key;
if (!key) {
return res.status(400).json({ error: "No key supplied" });
}
const userId = await dashboardLogins.getUserIdByApiKey(key);
if (!userId) {
return res.status(403).json({ error: "Invalid key" });
}
res.json({ status: "ok" });
});
}

15
src/api/guilds.ts Normal file
View file

@ -0,0 +1,15 @@
import express from "express";
import passport from "passport";
import { AllowedGuilds } from "../data/AllowedGuilds";
export function initGuildsAPI(app: express.Express) {
const guildAPIRouter = express.Router();
guildAPIRouter.use(passport.authenticate("api-token"));
const allowedGuilds = new AllowedGuilds();
guildAPIRouter.get("/guilds/available", async (req, res) => {
const guilds = await allowedGuilds.getForDashboardUser(req.user.userId);
res.end(guilds);
});
}

29
src/api/index.ts Normal file
View file

@ -0,0 +1,29 @@
require("dotenv").config();
import express from "express";
import cors from "cors";
import { initAuth } from "./auth";
import { initGuildsAPI } from "./guilds";
import { connect } from "../data/db";
console.log("Connecting to database...");
connect().then(() => {
const app = express();
app.use(
cors({
origin: process.env.DASHBOARD_URL,
}),
);
app.use(express.json());
initAuth(app);
initGuildsAPI(app);
app.get("/", (req, res) => {
res.end({ status: "cookies" });
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`API server listening on port ${port}`));
});

41
src/data/AllowedGuilds.ts Normal file
View file

@ -0,0 +1,41 @@
import { AllowedGuild } from "./entities/AllowedGuild";
import {
getConnection,
getRepository,
Repository,
Transaction,
TransactionManager,
TransactionRepository,
} from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { BaseRepository } from "./BaseRepository";
export class AllowedGuilds extends BaseRepository {
private allowedGuilds: Repository<AllowedGuild>;
constructor() {
super();
this.allowedGuilds = getRepository(AllowedGuild);
}
async isAllowed(guildId) {
const count = await this.allowedGuilds.count({
where: {
guild_id: guildId,
},
});
return count !== 0;
}
getForDashboardUser(userId) {
return this.allowedGuilds
.createQueryBuilder("allowed_guilds")
.innerJoin(
"dashboard_users",
"dashboard_users",
"dashboard_users.guild_id = allowed_guilds.guild_id AND dashboard_users.user_id = :userId",
{ userId },
)
.getMany();
}
}

View file

@ -7,5 +7,5 @@ export enum CaseTypes {
Mute,
Unmute,
Expunged,
Softban
Softban,
}

49
src/data/Configs.ts Normal file
View file

@ -0,0 +1,49 @@
import { Config } from "./entities/Config";
import {
getConnection,
getRepository,
Repository,
Transaction,
TransactionManager,
TransactionRepository,
} from "typeorm";
import { BaseGuildRepository } from "./BaseGuildRepository";
import { connection } from "./db";
import { BaseRepository } from "./BaseRepository";
export class Configs extends BaseRepository {
private configs: Repository<Config>;
constructor() {
super();
this.configs = getRepository(Config);
}
getActiveByKey(key) {
return this.configs.findOne({
where: {
key,
is_active: true,
},
});
}
async hasConfig(key) {
return (await this.getActiveByKey(key)) != null;
}
async saveNewRevision(key, config, editedBy) {
return connection.transaction(async entityManager => {
const repo = entityManager.getRepository(Config);
// Mark all old revisions inactive
await repo.update({ key }, { is_active: false });
// Add new, active revision
await repo.insert({
key,
config,
is_active: true,
edited_by: editedBy,
});
});
}
}

View file

@ -0,0 +1,85 @@
import { getRepository, Repository } from "typeorm";
import { DashboardLogin } from "./entities/DashboardLogin";
import { BaseRepository } from "./BaseRepository";
import crypto from "crypto";
import moment from "moment-timezone";
// tslint:disable-next-line:no-submodule-imports
import uuidv4 from "uuid/v4";
import { DBDateFormat } from "../utils";
import { log } from "util";
export interface DashboardLoginUserData {
username: string;
discriminator: string;
avatar: string;
}
export class DashboardLogins extends BaseRepository {
private dashboardLogins: Repository<DashboardLogin>;
constructor() {
super();
this.dashboardLogins = getRepository(DashboardLogin);
}
async getUserIdByApiKey(apiKey: string): Promise<string | null> {
const [loginId, token] = apiKey.split(".");
if (!loginId || !token) {
return null;
}
const login = await this.dashboardLogins
.createQueryBuilder()
.where("id = :id", { id: loginId })
.andWhere("expires_at > NOW()")
.getOne();
if (!login) {
return null;
}
const hash = crypto.createHash("sha256");
hash.update(loginId + token); // Remember to use loginId as the salt
const hashedToken = hash.digest("hex");
if (hashedToken !== login.token) {
return null;
}
return login.user_id;
}
async addLogin(userId: string, userData: DashboardLoginUserData): Promise<string> {
// Generate random login id
let loginId;
while (true) {
loginId = uuidv4();
const existing = await this.dashboardLogins.findOne({
where: {
id: loginId,
},
});
if (!existing) break;
}
// Generate token
const token = uuidv4();
const hash = crypto.createHash("sha256");
hash.update(loginId + token); // Use loginId as a salt
const hashedToken = hash.digest("hex");
// Save this to the DB
await this.dashboardLogins.insert({
id: loginId,
token: hashedToken,
user_id: userId,
user_data: userData,
logged_in_at: moment().format(DBDateFormat),
expires_at: moment()
.add(1, "day")
.format(DBDateFormat),
});
return `${loginId}.${token}`;
}
}

View file

@ -0,0 +1,6 @@
export enum DashboardRoles {
Viewer = 1,
Editor,
Manager,
ServerOwner,
}

View file

@ -0,0 +1,12 @@
import { getRepository, Repository } from "typeorm";
import { DashboardUser } from "./entities/DashboardUser";
import { BaseRepository } from "./BaseRepository";
export class DashboardUsers extends BaseRepository {
private dashboardUsers: Repository<DashboardUser>;
constructor() {
super();
this.dashboardUsers = getRepository(DashboardUser);
}
}

View file

@ -201,7 +201,7 @@ export class GuildSavedMessages extends BaseGuildRepository {
const deleted = await this.messages
.createQueryBuilder()
.where("id IN (:ids)", { ids })
.where("deleted_at = :deletedAt", { deletedAt })
.andWhere("deleted_at = :deletedAt", { deletedAt })
.getMany();
if (deleted.length) {

View file

@ -0,0 +1,14 @@
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
@Entity("allowed_guilds")
export class AllowedGuild {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
name: string;
@Column()
icon: string;
}

View file

@ -0,0 +1,23 @@
import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm";
@Entity("configs")
export class Config {
@Column()
@PrimaryColumn()
id: number;
@Column()
key: string;
@Column()
config: string;
@Column()
is_active: boolean;
@Column()
edited_by: string;
@Column()
edited_at: string;
}

View file

@ -0,0 +1,24 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
import { DashboardLoginUserData } from "../DashboardLogins";
@Entity("dashboard_logins")
export class DashboardLogin {
@Column()
@PrimaryColumn()
id: string;
@Column()
token: string;
@Column()
user_id: string;
@Column("simple-json")
user_data: DashboardLoginUserData;
@Column()
logged_in_at: string;
@Column()
expires_at: string;
}

View file

@ -0,0 +1,18 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity("dashboard_users")
export class DashboardUser {
@Column()
@PrimaryColumn()
guild_id: string;
@Column()
@PrimaryColumn()
user_id: string;
@Column()
username: string;
@Column()
role: string;
}

View file

@ -10,6 +10,8 @@ import { SimpleError } from "./SimpleError";
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
import DiscordHTTPError from "eris/lib/errors/DiscordHTTPError"; // tslint:disable-line
import { Configs } from "./data/Configs";
require("dotenv").config();
// Error handling
@ -71,8 +73,8 @@ import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
import { customArgumentTypes } from "./customArgumentTypes";
import { errorMessage, successMessage } from "./utils";
import { startUptimeCounter } from "./uptime";
import { AllowedGuilds } from "./data/AllowedGuilds";
// Run latest database migrations
logger.info("Connecting to database");
connect().then(async conn => {
const client = new Client(`Bot ${process.env.TOKEN}`, {
@ -87,18 +89,25 @@ connect().then(async conn => {
}
});
const allowedGuilds = new AllowedGuilds();
const guildConfigs = new Configs();
const bot = new Knub(client, {
plugins: availablePlugins,
globalPlugins: availableGlobalPlugins,
options: {
canLoadGuild(guildId): Promise<boolean> {
return allowedGuilds.isAllowed(guildId);
},
/**
* Plugins are enabled if they...
* - are base plugins, i.e. always enabled, or
* - are dependencies of other enabled plugins, or
* - are explicitly enabled in the guild config
*/
getEnabledPlugins(guildId, guildConfig): string[] {
async getEnabledPlugins(guildId, guildConfig): Promise<string[]> {
const configuredPlugins = guildConfig.plugins || {};
const pluginNames: string[] = Array.from(this.plugins.keys());
const plugins: Array<typeof Plugin> = Array.from(this.plugins.values());
@ -125,22 +134,15 @@ connect().then(async conn => {
return Array.from(finalEnabledPlugins.values());
},
/**
* Loads the requested config file from the config dir
* TODO: Move to the database
*/
async getConfig(id) {
const configFile = id ? `${id}.yml` : "global.yml";
const configPath = path.join("config", configFile);
try {
await fsp.access(configPath);
} catch (e) {
return {};
const key = id === "global" ? "global" : `guild-${id}`;
const row = await guildConfigs.getActiveByKey(key);
if (row) {
return yaml.safeLoad(row.config);
}
const yamlString = await fsp.readFile(configPath, { encoding: "utf8" });
return yaml.safeLoad(yamlString);
logger.warn(`No config with key "${key}"`);
return {};
},
logFn: (level, msg) => {

39
src/migrateConfigsToDB.ts Normal file
View file

@ -0,0 +1,39 @@
import { connect } from "./data/db";
import { Configs } from "./data/Configs";
import path from "path";
import * as _fs from "fs";
const fs = _fs.promises;
const authorId = process.argv[2];
if (!authorId) {
console.error("No author id specified");
process.exit(1);
}
console.log("Connecting to database");
connect().then(async () => {
const configs = new Configs();
console.log("Loading config files");
const configDir = path.join(__dirname, "..", "config");
const configFiles = await fs.readdir(configDir);
console.log("Looping through config files");
for (const configFile of configFiles) {
const parts = configFile.split(".");
const ext = parts[parts.length - 1];
if (ext !== "yml") continue;
const id = parts.slice(0, -1).join(".");
const key = id === "global" ? "global" : `guild-${id}`;
if (await configs.hasConfig(key)) continue;
const content = await fs.readFile(path.join(configDir, configFile), { encoding: "utf8" });
console.log(`Migrating config for ${key}`);
await configs.saveNewRevision(key, content, authorId);
}
console.log("Done!");
process.exit(0);
});

View file

@ -0,0 +1,54 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateDashboardLoginsTable1558804433320 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(
new Table({
name: "dashboard_logins",
columns: [
{
name: "id",
type: "varchar",
length: "36",
isPrimary: true,
collation: "ascii_bin",
},
{
name: "token",
type: "varchar",
length: "64",
collation: "ascii_bin",
},
{
name: "user_id",
type: "bigint",
},
{
name: "user_data",
type: "text",
},
{
name: "logged_in_at",
type: "DATETIME",
},
{
name: "expires_at",
type: "DATETIME",
},
],
indices: [
{
columnNames: ["user_id"],
},
{
columnNames: ["expires_at"],
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable("dashboard_logins", true);
}
}

View file

@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class CreateDashboardUsersTable1558804449510 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(
new Table({
name: "dashboard_users",
columns: [
{
name: "guild_id",
type: "bigint",
},
{
name: "user_id",
type: "bigint",
},
{
name: "username",
type: "varchar",
length: "255",
},
{
name: "role",
type: "varchar",
length: "32",
},
],
}),
);
await queryRunner.createPrimaryKey("dashboard_users", ["guild_id", "user_id"]);
await queryRunner.createIndex(
"dashboard_users",
new TableIndex({
columnNames: ["user_id"],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable("dashboard_users", true);
}
}

View file

@ -0,0 +1,51 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class CreateConfigsTable1561111990357 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(
new Table({
name: "configs",
columns: [
{
name: "id",
type: "int",
isPrimary: true,
isGenerated: true,
generationStrategy: "increment",
},
{
name: "key",
type: "varchar",
length: "48",
},
{
name: "config",
type: "mediumtext",
},
{
name: "is_active",
type: "tinyint",
},
{
name: "edited_by",
type: "bigint",
},
{
name: "edited_at",
type: "datetime",
default: "now()",
},
],
indices: [
{
columnNames: ["key", "is_active"],
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable("configs", true);
}
}

View file

@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateAllowedGuildsTable1561117545258 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(
new Table({
name: "allowed_guilds",
columns: [
{
name: "guild_id",
type: "bigint",
isPrimary: true,
},
{
name: "name",
type: "varchar",
length: "255",
},
{
name: "icon",
type: "varchar",
length: "255",
collation: "ascii_general_ci",
isNullable: true,
},
{
name: "owner_id",
type: "bigint",
},
],
indices: [{ columnNames: ["owner_id"] }],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropTable("allowed_guilds", true);
}
}

190
src/plugins/CustomEvents.ts Normal file
View file

@ -0,0 +1,190 @@
import { ZeppelinPlugin } from "./ZeppelinPlugin";
import { IPluginOptions } from "knub";
import { Message, TextChannel, VoiceChannel } from "eris";
import { renderTemplate } from "../templateFormatter";
import { stripObjectToScalars } from "../utils";
import { CasesPlugin } from "./Cases";
import { CaseTypes } from "../data/CaseTypes";
// Triggers
type CommandTrigger = {
type: "command";
name: string;
params: string;
can_use: boolean;
};
type AnyTrigger = CommandTrigger;
// Actions
type AddRoleAction = {
type: "add_role";
target: string;
role: string | string[];
};
type CreateCaseAction = {
type: "create_case";
case_type: string;
mod: string;
target: string;
reason: string;
};
type MoveToVoiceChannelAction = {
type: "move_to_vc";
target: string;
channel: string;
};
type MessageAction = {
type: "message";
channel: string;
content: string;
};
type AnyAction = AddRoleAction | CreateCaseAction | MoveToVoiceChannelAction | MessageAction;
// Event
type CustomEvent = {
name: string;
trigger: AnyTrigger;
actions: AnyAction[];
};
interface ICustomEventsPluginConfig {
events: {
[key: string]: CustomEvent;
};
}
class ActionError extends Error {}
export class CustomEventsPlugin extends ZeppelinPlugin<ICustomEventsPluginConfig> {
public static pluginName = "custom_events";
private clearTriggers: () => void;
public static dependencies = ["cases"];
getDefaultOptions(): IPluginOptions<ICustomEventsPluginConfig> {
return {
config: {
events: {},
},
};
}
onLoad() {
for (const [key, event] of Object.entries(this.getConfig().events)) {
if (event.trigger.type === "command") {
this.commands.add(
event.trigger.name,
event.trigger.params,
(msg, args) => {
const strippedMsg = stripObjectToScalars(msg, ["channel", "author"]);
this.runEvent(event, { msg, args }, { args, msg: strippedMsg });
},
{
requiredPermission: `events.${key}.trigger.can_use`,
locks: [],
},
);
}
}
}
onUnload() {
// TODO: Run this.clearTriggers() once we actually have something there
}
async runEvent(event: CustomEvent, eventData: any, values: any) {
try {
for (const action of event.actions) {
if (action.type === "add_role") {
await this.addRoleAction(action, values, event, eventData);
} else if (action.type === "create_case") {
await this.createCaseAction(action, values, event, eventData);
} else if (action.type === "move_to_vc") {
await this.moveToVoiceChannelAction(action, values, event, eventData);
} else if (action.type === "message") {
await this.messageAction(action, values);
}
}
} catch (e) {
if (e instanceof ActionError) {
if (event.trigger.type === "command") {
this.sendErrorMessage((eventData.msg as Message).channel, e.message);
} else {
// TODO: Where to log action errors from other kinds of triggers?
}
return;
}
throw e;
}
}
async addRoleAction(action: AddRoleAction, values: any, event: CustomEvent, eventData: any) {
const targetId = await renderTemplate(action.target, values, false);
const target = await this.getMember(targetId);
if (!target) throw new ActionError(`Unknown target member: ${targetId}`);
if (event.trigger.type === "command" && !this.canActOn((eventData.msg as Message).member, target)) {
throw new ActionError("Missing permissions");
}
const rolesToAdd = Array.isArray(action.role) ? action.role : [action.role];
await target.edit({
roles: Array.from(new Set([...target.roles, ...rolesToAdd])),
});
}
async createCaseAction(action: CreateCaseAction, values: any, event: CustomEvent, eventData: any) {
const modId = await renderTemplate(action.mod, values, false);
const targetId = await renderTemplate(action.target, values, false);
const reason = await renderTemplate(action.reason, values, false);
if (CaseTypes[action.case_type] == null) {
throw new ActionError(`Invalid case type: ${action.type}`);
}
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
await casesPlugin.createCase({
userId: targetId,
modId: modId,
type: CaseTypes[action.case_type],
reason: `__[${event.name}]__ ${reason}`,
});
}
async moveToVoiceChannelAction(action: MoveToVoiceChannelAction, values: any, event: CustomEvent, eventData: any) {
const targetId = await renderTemplate(action.target, values, false);
const target = await this.getMember(targetId);
if (!target) throw new ActionError("Unknown target member");
if (event.trigger.type === "command" && !this.canActOn((eventData.msg as Message).member, target)) {
throw new ActionError("Missing permissions");
}
const targetChannelId = await renderTemplate(action.channel, values, false);
const targetChannel = this.guild.channels.get(targetChannelId);
if (!targetChannel) throw new ActionError("Unknown target channel");
if (!(targetChannel instanceof VoiceChannel)) throw new ActionError("Target channel is not a voice channel");
if (!target.voiceState.channelID) return;
await target.edit({
channelID: targetChannel.id,
});
}
async messageAction(action: MessageAction, values: any) {
const targetChannelId = await renderTemplate(action.channel, values, false);
const targetChannel = this.guild.channels.get(targetChannelId);
if (!targetChannel) throw new ActionError("Unknown target channel");
if (!(targetChannel instanceof TextChannel)) throw new ActionError("Target channel is not a text channel");
await targetChannel.createMessage({ content: action.content });
}
}

View file

@ -75,8 +75,8 @@ export class MutesPlugin extends ZeppelinPlugin<IMutesPluginConfig> {
dm_on_mute: false,
message_on_mute: false,
message_channel: null,
mute_message: "You have been muted on {guildName}. Reason given: {reason}",
timed_mute_message: "You have been muted on {guildName} for {time}. Reason given: {reason}",
mute_message: "You have been muted on the {guildName} server. Reason given: {reason}",
timed_mute_message: "You have been muted on the {guildName} server for {time}. Reason given: {reason}",
can_view_list: false,
can_cleanup: false,

View file

@ -21,6 +21,7 @@ import { WelcomeMessagePlugin } from "./WelcomeMessage";
import { BotControlPlugin } from "./BotControl";
import { LogServerPlugin } from "./LogServer";
import { UsernameSaver } from "./UsernameSaver";
import { CustomEventsPlugin } from "./CustomEvents";
/**
* Plugins available to be loaded for individual guilds
@ -46,6 +47,7 @@ export const availablePlugins = [
SelfGrantableRolesPlugin,
RemindersPlugin,
WelcomeMessagePlugin,
CustomEventsPlugin,
];
/**

View file

@ -1,5 +1,4 @@
import has from "lodash.has";
import at from "lodash.at";
import { has, get } from "./utils";
const TEMPLATE_CACHE_SIZE = 200;
const templateCache: Map<string, ParsedTemplate> = new Map();
@ -219,7 +218,7 @@ export function parseTemplate(str: string): ParsedTemplate {
}
async function evaluateTemplateVariable(theVar: ITemplateVar, values) {
let value = has(values, theVar.identifier) ? at(values, theVar.identifier)[0] : undefined;
const value = has(values, theVar.identifier) ? get(values, theVar.identifier) : undefined;
if (typeof value === "function") {
const args = [];

View file

@ -5,7 +5,6 @@ import {
Guild,
GuildAuditLogEntry,
Member,
MessageContent,
TextableChannel,
TextChannel,
User,
@ -61,8 +60,19 @@ export function errorMessage(str) {
return `${str}`;
}
export function uclower(str) {
return str[0].toLowerCase() + str.slice(1);
export function get(obj, path, def?): any {
let cursor = obj;
const pathParts = path.split(".");
for (const part of pathParts) {
cursor = cursor[part];
if (cursor === undefined) return def;
if (cursor == null) return null;
}
return cursor;
}
export function has(obj, path): boolean {
return get(obj, path) !== undefined;
}
export function stripObjectToScalars(obj, includedNested: string[] = []) {

View file

@ -20,6 +20,7 @@
"severity": "warning"
},
"no-bitwise": false,
"interface-over-type-literal": false
"interface-over-type-literal": false,
"interface-name": false
}
}