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

Auto-generate plugin docs (WIP)

This commit is contained in:
Dragory 2019-08-22 01:22:26 +03:00
parent 6bdb05e678
commit ee6d622941
44 changed files with 599 additions and 150 deletions

View file

@ -1267,7 +1267,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@ -4778,7 +4777,6 @@
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -4787,8 +4785,7 @@
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"dev": true
} }
} }
}, },
@ -4987,6 +4984,11 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"marked": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -6949,8 +6951,7 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true
}, },
"sshpk": { "sshpk": {
"version": "1.16.1", "version": "1.16.1",

View file

@ -23,6 +23,8 @@
"bulma": "^0.7.5", "bulma": "^0.7.5",
"bulmaswatch": "^0.7.2", "bulmaswatch": "^0.7.2",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"js-yaml": "^3.13.1",
"marked": "^0.7.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
"vue-highlightjs": "^1.3.3", "vue-highlightjs": "^1.3.3",

View file

@ -0,0 +1,64 @@
<template>
<div>
<h1 class="z-title is-1 mb-1">Argument Types</h1>
<p class="mb-1">
This page details the different argument types available for commands.
</p>
<h2 id="delay" class="z-title is-2 mt-2 mb-1">Delay</h2>
<p class="mb-1">
A delay is used to specify an amount of time. It uses simple letters to specify time durations.<br>
For example, <code>2d15h27m3s</code> would be 2 days, 15 hours, 27 minutes and 3 seconds.
</p>
<p class="mb-1">
Note that the delay should always be written as 1 word, without spaces!
</p>
<b-collapse :open="false" class="card mb-1">
<div slot="trigger" slot-scope="props" class="card-header" role="button">
<p class="card-header-title">Additional Information</p>
<a class="card-header-icon">
<b-icon :icon="props.open ? 'menu-down' : 'menu-up'"></b-icon>
</a>
</div>
<div class="card-content">
<div class="content">
Durations:
<ul>
<li>
<code>d</code> Day
</li>
<li>
<code>h</code> Hour
</li>
<li>
<code>m</code> Minute
</li>
<li>
<code>s</code> Seconds
</li>
</ul>
</div>
</div>
</b-collapse>
<h2 id="string" class="z-title is-2 mb-1">String</h2>
<h2 id="user" class="z-title is-2 mt-2 mb-1">User</h2>
<p class="mb-1">
Anything that uniquely identifies a user. This includes:
</p>
<ul class="z-list z-ul mb-1">
<li>User ID <code>108552944961454080</code></li>
<li>User Mention <code>@Dark#1010</code></li>
<li>Loose user mention <code>Dark#1010</code></li>
</ul>
</div>
</template>
<script>
import CodeBlock from "./CodeBlock";
export default {
components: { CodeBlock },
};
</script>

View file

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<h1>Configuration format</h1> <h1 class="z-title is-1 mb-1">Configuration format</h1>
<p> <p class="mb-1">
This is the basic format of the bot configuration for a guild. The basic breakdown is: This is the basic format of the bot configuration for a guild. The basic breakdown is:
</p> </p>
<ol> <ol class="z-list mb-1">
<li>Prefix (i.e. what character is preceding each command)</li> <li>Prefix (i.e. what character is preceding each command)</li>
<li>Permission levels (see <router-link to="/docs/permissions">Permissions</router-link> for more info)</li> <li>Permission levels (see <router-link to="/docs/permissions">Permissions</router-link> for more info)</li>
<li>Plugin-specific configuration (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)</li> <li>Plugin-specific configuration (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)</li>

View file

@ -1,29 +1,29 @@
<template> <template>
<div> <div>
<h1>Introduction</h1> <h1 class="z-title is-1 mb-1">Introduction</h1>
<p> <p class="mb-1">
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind. Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
</p> </p>
<h2>Getting the bot</h2> <h2 class="z-title is-2 mt-2 mb-1">Getting the bot</h2>
<p> <p class="mb-1">
Since the bot is currently private, access to the bot is granted on a case by case basis.<br> Since the bot is currently private, access to the bot is granted on a case by case basis.<br>
There are plans to streamline this process in the future. There are plans to streamline this process in the future.
</p> </p>
<h2>Configuration</h2> <h2 class="z-title is-2 mt-2 mb-1">Configuration</h2>
<p> <p class="mb-1">
All Zeppelin configuration is done through the dashboard by editing a YAML config file. By default, only the server All Zeppelin configuration is done through the dashboard by editing a YAML config file. By default, only the server
owner has access to this, but they can give other users access as they see fit. See <router-link to="/docs/configuration-format">Configuration format</router-link> for more details. owner has access to this, but they can give other users access as they see fit. See <router-link to="/docs/configuration-format">Configuration format</router-link> for more details.
</p> </p>
<h2>Plugins</h2> <h2 class="z-title is-2 mt-2 mb-1">Plugins</h2>
<p> <p class="mb-1">
Zeppelin is divided into plugins: grouped functionality that can be enabled/disabled as needed, and that have their own configurations. Zeppelin is divided into plugins: grouped functionality that can be enabled/disabled as needed, and that have their own configurations.
</p> </p>
<h2>Commands</h2> <h2 class="z-title is-2 mt-2 mb-1">Commands</h2>
<p> <p class="mb-1">
The commands for each plugin are listed on the plugin's page (see "Plugins" on the menu). On these pages, the command prefix is assumed to be <code>!</code> but this can be changed on a per-server basis. The commands for each plugin are listed on the plugin's page (see "Plugins" on the menu). On these pages, the command prefix is assumed to be <code>!</code> but this can be changed on a per-server basis.
</p> </p>
</div> </div>

View file

@ -41,14 +41,15 @@
<p class="menu-label">Plugins</p> <p class="menu-label">Plugins</p>
<ul class="menu-list"> <ul class="menu-list">
<li><router-link to="/docs/plugins/locate-user">Locate user</router-link></li> <li v-for="plugin in plugins">
<li><router-link to="/docs/plugins/mod-actions">Mod actions</router-link></li> <router-link :to="'/docs/plugins/' + plugin.name">{{ plugin.info.prettyName || plugin.name }}</router-link>
</li>
</ul> </ul>
</aside> </aside>
</div> </div>
</div> </div>
<div class="docs-main content"> <div class="docs-main">
<router-view></router-view> <router-view :key="$route.fullPath"></router-view>
</div> </div>
</div> </div>
</div> </div>
@ -132,9 +133,22 @@
<script> <script>
import Vue from "vue"; import Vue from "vue";
import VueHighlightJS from "vue-highlightjs"; import VueHighlightJS from "vue-highlightjs";
import {mapState} from "vuex";
import "../../directives/trim-code"; import "../../directives/trim-code";
import "highlight.js/styles/ocean.css"; import "highlight.js/styles/ocean.css";
import "../../style/docs.scss"; import "../../style/docs.scss";
Vue.use(VueHighlightJS); Vue.use(VueHighlightJS);
export default {
async mounted() {
await this.$store.dispatch("docs/loadAllPlugins");
},
computed: {
...mapState('docs', {
plugins: 'allPlugins',
}),
},
};
</script> </script>

View file

@ -1,23 +1,23 @@
<template> <template>
<div> <div>
<h1>Permissions</h1> <h1 class="z-title is-1 mb-1">Permissions</h1>
<p> <p class="mb-1">
Permissions in Zeppelin are simply values in plugin configuration that are checked when the command is used. Permissions in Zeppelin are simply values in plugin configuration that are checked when the command is used.
These values can be changed with overrides (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info) These values can be changed with overrides (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)
and can depend on e.g. user id, role id, channel id, category id, or <strong>permission level</strong>. and can depend on e.g. user id, role id, channel id, category id, or <strong>permission level</strong>.
</p> </p>
<h2>Permission levels</h2> <h2 class="z-title is-2 mt-2 mb-1">Permission levels</h2>
<p> <p class="mb-1">
The simplest way to control access to bot commands and features is via permission levels. The simplest way to control access to bot commands and features is via permission levels.
These levels are simply a number (usually between 0 and 100), based on the user's roles or user id, that can then These levels are simply a number (usually between 0 and 100), based on the user's roles or user id, that can then
be used in permission overrides. By default, several commands are "moderator only" (level 50 and up) or "admin only" (level 100 and up). be used in permission overrides. By default, several commands are "moderator only" (level 50 and up) or "admin only" (level 100 and up).
</p> </p>
<p> <p class="mb-1">
Additionally, having a higher permission level means that certain commands (such as !ban) can't be used against Additionally, having a higher permission level means that certain commands (such as !ban) can't be used against
you by users with a lower or equal permission level (so e.g. moderators can't ban each other or admins above them). you by users with a lower or equal permission level (so e.g. moderators can't ban each other or admins above them).
</p> </p>
<p> <p class="mb-1">
Permission levels are defined in the config in the <strong>levels</strong> section. For example: Permission levels are defined in the config in the <strong>levels</strong> section. For example:
</p> </p>
@ -28,14 +28,14 @@
"172950000412655616": 50 # Example mod "172950000412655616": 50 # Example mod
</CodeBlock> </CodeBlock>
<h2>Examples</h2> <h2 class="z-title is-2 mt-2 mb-1">Examples</h2>
<h3>Basic overrides</h3> <h3 class="z-title is-3 mb-1">Basic overrides</h3>
<p> <p class="mb-1">
For this example, let's assume we have a plugin called <code>cats</code> which has a command <code>!cat</code> locked behind the permission <code>can_cat</code>. For this example, let's assume we have a plugin called <code>cats</code> which has a command <code>!cat</code> locked behind the permission <code>can_cat</code>.
Let's say that by default, the plugin allows anyone to use <code>!cat</code>, but we want to restrict it to moderators only. Let's say that by default, the plugin allows anyone to use <code>!cat</code>, but we want to restrict it to moderators only.
</p> </p>
<p> <p class="mb-1">
Here's what the configuration for this would look like: Here's what the configuration for this would look like:
</p> </p>
@ -51,15 +51,15 @@
can_cat: true can_cat: true
</CodeBlock> </CodeBlock>
<h3>Replacing defaults</h3> <h3 class="z-title is-3 mt-2 mb-1">Replacing defaults</h3>
<p> <p class="mb-1">
In this example, let's assume you don't want to use the default permission levels of 50 and 100 for mods and admins respectively. In this example, let's assume you don't want to use the default permission levels of 50 and 100 for mods and admins respectively.
Let's say you're using various incremental levels instead: 10, 20, 30, 40, 50...<br> Let's say you're using various incremental levels instead: 10, 20, 30, 40, 50...<br>
We want to make it so moderator commands are available starting at level 70. We want to make it so moderator commands are available starting at level 70.
Additionally, we'd like to reserve banning for levels 90+ only. Additionally, we'd like to reserve banning for levels 90+ only.
To do this, we need to <strong>replace</strong> the default overrides that enable moderator commands at level 50. To do this, we need to <strong>replace</strong> the default overrides that enable moderator commands at level 50.
</p> </p>
<p> <p class="mb-1">
Here's what the configuration for this would look like: Here's what the configuration for this would look like:
</p> </p>

View file

@ -0,0 +1,77 @@
<template>
<div v-if="loading">
Loading...
</div>
<div v-else>
<h1 class="z-title is-1 mb-1">{{ data.info.prettyName || data.name }}</h1>
<p class="mb-1">
Name in config: <code>{{ data.name }}</code>
</p>
<div v-if="data.info.description">
<h2 class="z-title is-2 mt-2 mb-1">Description</h2>
<div class="content" v-html="renderMarkdown(data.info.description)"></div>
</div>
<h2 class="z-title is-2 mt-2 mb-1">Default configuration</h2>
<CodeBlock lang="yaml">{{ renderConfiguration(data.options) }}</CodeBlock>
<div v-if="data.commands.length">
<h2 class="z-title is-2 mt-2 mb-1">Commands</h2>
<div v-for="command in data.commands">
<h3 class="z-title is-3 mt-2 mb-1">!{{ command.trigger }}</h3>
<div v-if="command.config.requiredPermission">
Permission: <code>{{ command.config.requiredPermission }}</code>
</div>
<div v-if="command.config.info && command.config.info.basicUsage">
Basic usage: <code>{{ command.config.info.basicUsage }}</code>
</div>
<div v-if="command.config.aliases && command.config.aliases.length">
Shortcut:
<code style="margin-right: 4px" v-for="alias in command.config.aliases">!{{ alias }}</code>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import {mapState} from "vuex";
import marked from "marked";
import yaml from "js-yaml";
import CodeBlock from "./CodeBlock";
export default {
components: { CodeBlock },
async mounted() {
this.loading = true;
await this.$store.dispatch("docs/loadPluginData", this.pluginName);
this.loading = false;
},
methods: {
renderMarkdown(str) {
return marked(str);
},
renderConfiguration(options) {
return yaml.safeDump({
[this.pluginName]: options,
});
},
},
data() {
return {
loading: true,
pluginName: this.$route.params.pluginName,
};
},
computed: {
...mapState("docs", {
data(state) {
return state.plugins[this.pluginName];
},
}),
},
}
</script>

View file

@ -1,20 +1,20 @@
<template> <template>
<div> <div>
<h1>Plugin configuration</h1> <h1 class="z-title is-1 mb-1">Plugin configuration</h1>
<p> <p class="mb-1">
Each plugin in Zeppelin has its own configuration options. In the config editor, you can both set the default config Each plugin in Zeppelin has its own configuration options. In the config editor, you can both set the default config
and overrides based on specific conditions. Permissions are also just values in the plugin's config, and thus follow and overrides based on specific conditions. Permissions are also just values in the plugin's config, and thus follow
the same rules with overrides etc. as other options (see <router-link to="/docs/permissions">Permissions</router-link> for more info). the same rules with overrides etc. as other options (see <router-link to="/docs/permissions">Permissions</router-link> for more info).
</p> </p>
<p> <p class="mb-1">
Information about each plugin's options can be found on the plugin's page on the sidebar. See <router-link to="/docs/configuration-format">Configuration format</router-link> for an example of a full config. Information about each plugin's options can be found on the plugin's page on the sidebar. See <router-link to="/docs/configuration-format">Configuration format</router-link> for an example of a full config.
</p> </p>
<h2>Overrides</h2> <h2 class="z-title is-2 mt-2 mb-1">Overrides</h2>
<p> <p class="mb-1">
Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc. Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc.
</p> </p>
<p> <p class="mb-1">
Here's an example demonstrating different types of overrides: Here's an example demonstrating different types of overrides:
</p> </p>

View file

@ -36,28 +36,12 @@ export const router = new VueRouter({
component: () => import("./components/docs/PluginConfiguration.vue"), component: () => import("./components/docs/PluginConfiguration.vue"),
}, },
{ {
path: "descriptions", path: "descriptions/argument-types",
component: () => import("./components/docs/descriptions/Layout.vue"), component: () => import("./components/docs/ArgumentTypes.vue"),
children: [
{
path: "argument-types",
component: () => import("./components/docs/descriptions/ArgumentTypes.vue"),
},
],
}, },
{ {
path: "plugins", path: "plugins/:pluginName",
component: () => import("./components/docs/plugins/Layout.vue"), component: () => import("./components/docs/Plugin.vue"),
children: [
{
path: "mod-actions",
component: () => import("./components/docs/plugins/ModActions.vue"),
},
{
path: "locate-user",
component: () => import("./components/docs/plugins/LocateUser.vue"),
},
],
}, },
], ],
}, },
@ -80,11 +64,15 @@ export const router = new VueRouter({
}, },
], ],
scrollBehavior(to) { scrollBehavior(to, from, savedPosition) {
if (to.hash) { if (to.hash) {
return { return {
selector: to.hash, selector: to.hash,
}; };
} else if (savedPosition) {
return savedPosition;
} else {
return { x: 0, y: 0 };
} }
}, },
}); });

View file

@ -0,0 +1,54 @@
import { get } from "../api";
import { Module } from "vuex";
import { DocsState, RootState } from "./types";
export const DocsStore: Module<DocsState, RootState> = {
namespaced: true,
state: {
allPlugins: [],
loadingAllPlugins: false,
plugins: {},
},
actions: {
async loadAllPlugins({ state, commit }) {
if (state.loadingAllPlugins) return;
commit("setAllPluginLoadStatus", true);
const plugins = await get("docs/plugins");
plugins.sort((a, b) => {
const aName = (a.info.prettyName || a.name).toLowerCase();
const bName = (b.info.prettyName || b.name).toLowerCase();
if (aName > bName) return 1;
if (aName < bName) return -1;
return 0;
});
commit("setAllPlugins", plugins);
commit("setAllPluginLoadStatus", false);
},
async loadPluginData({ state, commit }, name) {
if (state.plugins[name]) return;
const data = await get(`docs/plugins/${name}`);
commit("setPluginData", { name, data });
},
},
mutations: {
setAllPluginLoadStatus(state: DocsState, status: boolean) {
state.loadingAllPlugins = status;
},
setAllPlugins(state: DocsState, plugins) {
state.allPlugins = plugins;
},
setPluginData(state: DocsState, { name, data }) {
state.plugins[name] = data;
},
},
};

View file

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

View file

@ -21,7 +21,29 @@ export interface GuildState {
}; };
} }
export interface ThinDocsPlugin {
name: string;
info: {
name: string;
description?: string;
};
}
export interface DocsPlugin extends ThinDocsPlugin {
commands: any[];
}
export interface DocsState {
allPlugins: ThinDocsPlugin[];
loadingAllPlugins: boolean;
plugins: {
[key: string]: DocsPlugin;
};
}
export type RootState = { export type RootState = {
auth: AuthState; auth: AuthState;
guilds: GuildState; guilds: GuildState;
docs: DocsState;
}; };

View file

@ -1,6 +1,12 @@
@import url('https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css'); @import url('https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css');
$family-primary: 'Open Sans', sans-serif; $family-primary: 'Open Sans', sans-serif;
$list-background-color: transparent;
$size-1: 2.5rem;
$size-2: 2rem;
$size-3: 1.5rem;
$size-4: 1.25rem;
@import "~bulmaswatch/superhero/_variables"; @import "~bulmaswatch/superhero/_variables";
@import "~bulma/bulma"; @import "~bulma/bulma";
@ -9,3 +15,30 @@ $family-primary: 'Open Sans', sans-serif;
.docs-cloak { .docs-cloak {
visibility: visible !important; visibility: visible !important;
} }
.z-title {
line-height: 1.125;
&.is-1 { font-size: $size-1; }
&.is-2 { font-size: $size-2; }
&.is-3 { font-size: $size-3; }
&.is-4 { font-size: $size-4; }
&.is-5 { font-size: $size-5; }
&.is-6 { font-size: $size-6; }
}
.z-list {
margin-left: 1.5rem;
}
.z-ul {
list-style: disc;
}
.mt-1 { margin-top: 1rem; }
.mt-2 { margin-top: 1.5rem; }
.mt-3 { margin-top: 2rem; }
.mb-1 { margin-bottom: 1rem; }
.mb-2 { margin-bottom: 1.5rem; }
.mb-3 { margin-bottom: 2rem; }

70
src/api/docs.ts Normal file
View file

@ -0,0 +1,70 @@
import express from "express";
import { availablePlugins } from "../plugins/availablePlugins";
import { ZeppelinPlugin } from "../plugins/ZeppelinPlugin";
import { notFound } from "./responses";
import { CommandManager, ICommandConfig } from "knub/dist/CommandManager";
const commandManager = new CommandManager();
export function initDocs(app: express.Express) {
const docsPlugins = availablePlugins.filter(pluginClass => pluginClass.showInDocs);
app.get("/docs/plugins", (req: express.Request, res: express.Response) => {
res.json(
docsPlugins.map(pluginClass => {
const thinInfo = pluginClass.pluginInfo ? { prettyName: pluginClass.pluginInfo.prettyName } : {};
return {
name: pluginClass.pluginName,
info: thinInfo,
};
}),
);
});
app.get("/docs/plugins/:pluginName", (req: express.Request, res: express.Response) => {
const pluginClass = docsPlugins.find(obj => obj.pluginName === req.params.pluginName);
if (!pluginClass) {
return notFound(res);
}
const props = Reflect.ownKeys(pluginClass.prototype);
const commands = props.reduce((arr, prop) => {
if (typeof prop !== "string") return arr;
const propCommands = Reflect.getMetadata("commands", pluginClass.prototype, prop);
if (propCommands) {
arr.push(
...propCommands.map(cmd => {
const trigger = typeof cmd.command === "string" ? cmd.command : cmd.command.source;
const parameters = cmd.parameters
? typeof cmd.parameters === "string"
? commandManager.parseParameterString(cmd.parameters)
: cmd.parameters
: [];
const config: ICommandConfig = cmd.options || {};
if (config.overloads) {
config.overloads = config.overloads.map(overload => {
return typeof overload === "string" ? commandManager.parseParameterString(overload) : overload;
});
}
return {
trigger,
parameters,
config,
};
}),
);
}
return arr;
}, []);
const options = (pluginClass as typeof ZeppelinPlugin).getStaticDefaultOptions();
res.json({
name: pluginClass.pluginName,
info: pluginClass.pluginInfo || {},
options,
commands,
});
});
}

View file

@ -4,6 +4,7 @@ import cors from "cors";
import { initAuth } from "./auth"; import { initAuth } from "./auth";
import { initGuildsAPI } from "./guilds"; import { initGuildsAPI } from "./guilds";
import { initArchives } from "./archives"; import { initArchives } from "./archives";
import { initDocs } from "./docs";
import { connect } from "../data/db"; import { connect } from "../data/db";
import path from "path"; import path from "path";
import { TokenError } from "passport-oauth2"; import { TokenError } from "passport-oauth2";
@ -12,15 +13,13 @@ import { PluginError } from "knub";
require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") }); require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
function errorHandler(err) { function errorHandler(err) {
// tslint:disable:no-console console.error(err.stack || err); // tslint:disable-line:no-console
console.error(err.stack || err);
process.exit(1); process.exit(1);
// tslint:enable:no-console
} }
process.on("unhandledRejection", errorHandler); process.on("unhandledRejection", errorHandler);
console.log("Connecting to database..."); console.log("Connecting to database..."); // tslint:disable-line
connect().then(() => { connect().then(() => {
const app = express(); const app = express();
@ -34,6 +33,7 @@ connect().then(() => {
initAuth(app); initAuth(app);
initGuildsAPI(app); initGuildsAPI(app);
initArchives(app); initArchives(app);
initDocs(app);
// Default route // Default route
app.get("/", (req, res) => { app.get("/", (req, res) => {
@ -45,7 +45,7 @@ connect().then(() => {
if (err instanceof TokenError) { if (err instanceof TokenError) {
clientError(res, "Invalid code"); clientError(res, "Invalid code");
} else { } else {
console.error(err); console.error(err); // tslint:disable-line
error(res, "Server error", err.status || 500); error(res, "Server error", err.status || 500);
} }
}); });
@ -56,5 +56,6 @@ connect().then(() => {
}); });
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
// tslint:disable-next-line
app.listen(port, () => console.log(`API server listening on port ${port}`)); app.listen(port, () => console.log(`API server listening on port ${port}`));
}); });

View file

@ -21,7 +21,7 @@ export class AutoReactionsPlugin extends ZeppelinPlugin<TConfigSchema> {
private onMessageCreateFn; private onMessageCreateFn;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_manage: false, can_manage: false,

View file

@ -358,7 +358,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
return config; return config;
} }
protected static getStaticDefaultOptions() { public static getStaticDefaultOptions() {
return { return {
rules: [], rules: [],
}; };

View file

@ -27,7 +27,7 @@ export class BotControlPlugin extends GlobalZeppelinPlugin<TConfigSchema> {
protected archives: GuildArchives; protected archives: GuildArchives;
protected static getStaticDefaultOptions() { public static getStaticDefaultOptions() {
return { return {
config: { config: {
can_use: false, can_use: false,

View file

@ -51,7 +51,7 @@ export class CasesPlugin extends ZeppelinPlugin<TConfigSchema> {
protected archives: GuildArchives; protected archives: GuildArchives;
protected logs: GuildLogs; protected logs: GuildLogs;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
log_automatic_actions: true, log_automatic_actions: true,

View file

@ -46,7 +46,7 @@ export class CensorPlugin extends ZeppelinPlugin<TConfigSchema> {
private onMessageCreateFn; private onMessageCreateFn;
private onMessageUpdateFn; private onMessageUpdateFn;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
filter_zalgo: false, filter_zalgo: false,

View file

@ -30,7 +30,7 @@ export class CompanionChannelPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "companion_channels"; public static pluginName = "companion_channels";
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
entries: {}, entries: {},

View file

@ -70,12 +70,13 @@ class ActionError extends Error {}
export class CustomEventsPlugin extends ZeppelinPlugin<TConfigSchema> { export class CustomEventsPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "custom_events"; public static pluginName = "custom_events";
public static showInDocs = false;
public static dependencies = ["cases"]; public static dependencies = ["cases"];
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
private clearTriggers: () => void; private clearTriggers: () => void;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
events: {}, events: {},
@ -162,7 +163,7 @@ export class CustomEventsPlugin extends ZeppelinPlugin<TConfigSchema> {
const casesPlugin = this.getPlugin<CasesPlugin>("cases"); const casesPlugin = this.getPlugin<CasesPlugin>("cases");
await casesPlugin.createCase({ await casesPlugin.createCase({
userId: targetId, userId: targetId,
modId: modId, modId,
type: CaseTypes[action.case_type], type: CaseTypes[action.case_type],
reason: `__[${event.name}]__ ${reason}`, reason: `__[${event.name}]__ ${reason}`,
}); });

View file

@ -21,7 +21,7 @@ export class GlobalZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extend
* we need a static version of getDefaultOptions(). This static version is then, * we need a static version of getDefaultOptions(). This static version is then,
* by turn, called from getDefaultOptions() so everything still works as expected. * by turn, called from getDefaultOptions() so everything still works as expected.
*/ */
protected static getStaticDefaultOptions() { public static getStaticDefaultOptions() {
// Implemented by plugin // Implemented by plugin
return {}; return {};
} }

View file

@ -4,6 +4,7 @@ import { MINUTES } from "../utils";
export class GuildInfoSaverPlugin extends ZeppelinPlugin { export class GuildInfoSaverPlugin extends ZeppelinPlugin {
public static pluginName = "guild_info_saver"; public static pluginName = "guild_info_saver";
public static showInDocs = false;
protected allowedGuilds: AllowedGuilds; protected allowedGuilds: AllowedGuilds;
private updateInterval; private updateInterval;

View file

@ -23,7 +23,7 @@ export class LocatePlugin extends ZeppelinPlugin<TConfigSchema> {
private outdatedAlertsTimeout; private outdatedAlertsTimeout;
private usersWithAlerts: string[] = []; private usersWithAlerts: string[] = [];
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_where: false, can_where: false,

View file

@ -70,7 +70,7 @@ export class LogsPlugin extends ZeppelinPlugin<TConfigSchema> {
private excludedUserProps = ["user", "member", "mod"]; private excludedUserProps = ["user", "member", "mod"];
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
channels: {}, channels: {},

View file

@ -12,11 +12,12 @@ type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export class MessageSaverPlugin extends ZeppelinPlugin<TConfigSchema> { export class MessageSaverPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "message_saver"; public static pluginName = "message_saver";
public static showInDocs = false;
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
protected savedMessages: GuildSavedMessages; protected savedMessages: GuildSavedMessages;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_manage: false, can_manage: false,

View file

@ -16,6 +16,8 @@ import {
stripObjectToScalars, stripObjectToScalars,
successMessage, successMessage,
tNullable, tNullable,
trimEmptyStartEndLines,
trimIndents,
trimLines, trimLines,
ucfirst, ucfirst,
UnknownUser, UnknownUser,
@ -68,11 +70,58 @@ interface IIgnoredEvent {
userId: string; userId: string;
} }
export type WarnResult =
| {
status: "failed";
error: string;
}
| {
status: "success";
case: Case;
notifyResult: INotifyUserResult;
};
export type KickResult =
| {
status: "failed";
error: string;
}
| {
status: "success";
case: Case;
notifyResult: INotifyUserResult;
};
export type BanResult =
| {
status: "failed";
error: string;
}
| {
status: "success";
case: Case;
notifyResult: INotifyUserResult;
};
type WarnMemberNotifyRetryCallback = () => boolean | Promise<boolean>;
export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> { export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "mod_actions"; public static pluginName = "mod_actions";
public static dependencies = ["cases", "mutes"]; public static dependencies = ["cases", "mutes"];
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
public static pluginInfo = {
prettyName: "Mod actions",
description: trimIndents(
trimEmptyStartEndLines(`
Testing **things**
Multiline haHAA
`),
6,
),
};
protected mutes: GuildMutes; protected mutes: GuildMutes;
protected cases: GuildCases; protected cases: GuildCases;
protected serverLogs: GuildLogs; protected serverLogs: GuildLogs;
@ -87,7 +136,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
this.ignoredEvents = []; this.ignoredEvents = [];
} }
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
dm_on_warn: true, dm_on_warn: true,
@ -439,6 +488,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("update", "<caseNumber:number> [note:string$]", { @d.command("update", "<caseNumber:number> [note:string$]", {
overloads: ["[note:string$]"], overloads: ["[note:string$]"],
info: {
description:
"Update the specified case (or, if case number is omitted, your latest case) by adding more notes/details to it",
},
}) })
@d.permission("can_note") @d.permission("can_note")
async updateCmd(msg: Message, args: { caseNumber?: number; note?: string }) { async updateCmd(msg: Message, args: { caseNumber?: number; note?: string }) {
@ -478,7 +531,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`)); msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`));
} }
@d.command("note", "<user:string> <note:string$>") @d.command("note", "<user:string> <note:string$>", {
info: {
description: "Add a note to the specified user",
},
})
@d.permission("can_note") @d.permission("can_note")
async noteCmd(msg: Message, args: { user: string; note: string }) { async noteCmd(msg: Message, args: { user: string; note: string }) {
const user = await this.resolveUser(args.user); const user = await this.resolveUser(args.user);
@ -500,6 +557,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("warn", "<user:string> <reason:string$>", { @d.command("warn", "<user:string> <reason:string$>", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Send a warning to the specified user",
},
}) })
@d.permission("can_warn") @d.permission("can_warn")
async warnCmd(msg: Message, args: { user: string; reason: string; mod?: Member }) { async warnCmd(msg: Message, args: { user: string; reason: string; mod?: Member }) {
@ -702,6 +762,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("mute", "<user:string> <time:delay> <reason:string$>", { @d.command("mute", "<user:string> <time:delay> <reason:string$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"], overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Mute the specified member",
},
}) })
@d.permission("can_mute") @d.permission("can_mute")
async muteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) { async muteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) {
@ -740,6 +803,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("forcemute", "<user:string> <time:delay> <reason:string$>", { @d.command("forcemute", "<user:string> <time:delay> <reason:string$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"], overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Force-mute the specified user, even if they're not on the server",
},
}) })
@d.permission("can_mute") @d.permission("can_mute")
async forcemuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) { async forcemuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod: Member }) {
@ -815,6 +881,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("unmute", "<user:string> <time:delay> <reason:string$>", { @d.command("unmute", "<user:string> <time:delay> <reason:string$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"], overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Unmute the specified member",
},
}) })
@d.permission("can_mute") @d.permission("can_mute")
async unmuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod?: Member }) { async unmuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod?: Member }) {
@ -857,6 +926,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("forceunmute", "<user:string> <time:delay> <reason:string$>", { @d.command("forceunmute", "<user:string> <time:delay> <reason:string$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"], overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Force-unmute the specified user, even if they're not on the server",
},
}) })
@d.permission("can_mute") @d.permission("can_mute")
async forceunmuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod?: Member }) { async forceunmuteCmd(msg: Message, args: { user: string; time?: number; reason?: string; mod?: Member }) {
@ -883,6 +955,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("kick", "<user:string> [reason:string$]", { @d.command("kick", "<user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Kick the specified member",
},
}) })
@d.permission("can_kick") @d.permission("can_kick")
async kickCmd(msg, args: { user: string; reason: string; mod: Member }) { async kickCmd(msg, args: { user: string; reason: string; mod: Member }) {
@ -941,6 +1016,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("ban", "<user:string> [reason:string$]", { @d.command("ban", "<user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Ban the specified member",
},
}) })
@d.permission("can_ban") @d.permission("can_ban")
async banCmd(msg, args: { user: string; reason?: string; mod?: Member }) { async banCmd(msg, args: { user: string; reason?: string; mod?: Member }) {
@ -977,62 +1055,32 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
mod = args.mod; mod = args.mod;
} }
const config = this.getConfig();
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments); const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
const banResult = await this.banUserId(memberToBan.id, reason, {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
});
// Attempt to message the user *before* banning them, as doing it after may not be possible if (banResult.status === "failed") {
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored }; msg.channel.createMessage(errorMessage(`Failed to ban member`));
if (reason) {
const banMessage = await renderTemplate(config.ban_message, {
guildName: this.guild.name,
reason,
});
userMessageResult = await notifyUser(this.bot, this.guild, memberToBan.user, banMessage, {
useDM: config.dm_on_ban,
useChannel: config.message_on_ban,
channelId: config.message_channel,
});
}
// (Try to) ban the user
this.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToBan.id);
this.ignoreEvent(IgnoredEventType.Ban, memberToBan.id);
try {
await memberToBan.ban(1);
} catch (e) {
msg.channel.create(errorMessage("Failed to ban the user"));
return; return;
} }
// Create a case for this action
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
const createdCase = await casesPlugin.createCase({
userId: memberToBan.id,
modId: mod.id,
type: CaseTypes.Ban,
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
noteDetails: userMessageResult.status !== NotifyUserStatus.Ignored ? [ucfirst(userMessageResult.text)] : [],
});
// Confirm the action to the moderator // Confirm the action to the moderator
let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${ let response = `Banned **${memberToBan.user.username}#${memberToBan.user.discriminator}** (Case #${
createdCase.case_number banResult.case.case_number
})`; })`;
if (userMessageResult.text) response += ` (${userMessageResult.text})`; if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
msg.channel.createMessage(successMessage(response)); msg.channel.createMessage(successMessage(response));
// Log the action
this.serverLogs.log(LogType.MEMBER_BAN, {
mod: stripObjectToScalars(mod.user),
user: stripObjectToScalars(memberToBan.user),
});
} }
@d.command("softban", "<user:string> [reason:string$]", { @d.command("softban", "<user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description:
'"Softban" the specified user by banning and immediately unbanning them. Effectively a kick with message deletions.',
},
}) })
@d.permission("can_ban") @d.permission("can_ban")
async softbanCmd(msg, args: { user: string; reason: string; mod?: Member }) { async softbanCmd(msg, args: { user: string; reason: string; mod?: Member }) {
@ -1119,6 +1167,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("unban", "<user:string> [reason:string$]", { @d.command("unban", "<user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Unban the specified member",
},
}) })
@d.permission("can_ban") @d.permission("can_ban")
async unbanCmd(msg: Message, args: { user: string; reason: string; mod: Member }) { async unbanCmd(msg: Message, args: { user: string; reason: string; mod: Member }) {
@ -1170,6 +1221,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("forceban", "<user:string> [reason:string$]", { @d.command("forceban", "<user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Force-ban the specified user, even if they aren't on the server",
},
}) })
@d.permission("can_ban") @d.permission("can_ban")
async forcebanCmd(msg: Message, args: { user: string; reason?: string; mod?: Member }) { async forcebanCmd(msg: Message, args: { user: string; reason?: string; mod?: Member }) {
@ -1233,7 +1287,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
}); });
} }
@d.command("massban", "<userIds:string...>") @d.command("massban", "<userIds:string...>", {
info: {
description: "Mass-ban a list of user IDs",
},
})
@d.permission("can_massban") @d.permission("can_massban")
async massbanCmd(msg: Message, args: { userIds: string[] }) { async massbanCmd(msg: Message, args: { userIds: string[] }) {
// Limit to 100 users at once (arbitrary?) // Limit to 100 users at once (arbitrary?)
@ -1317,6 +1375,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("addcase", "<type:string> <user:string> [reason:string$]", { @d.command("addcase", "<type:string> <user:string> [reason:string$]", {
options: [{ name: "mod", type: "member" }], options: [{ name: "mod", type: "member" }],
info: {
description: "Add an arbitrary case to the specified user without taking any action",
},
}) })
@d.permission("can_addcase") @d.permission("can_addcase")
async addcaseCmd(msg: Message, args: { type: string; user: string; reason?: string; mod?: Member }) { async addcaseCmd(msg: Message, args: { type: string; user: string; reason?: string; mod?: Member }) {
@ -1377,15 +1438,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
}); });
} }
/** @d.command("case", "<caseNumber:number>", {
* Display a case or list of cases info: {
* If the argument passed is a case id, display that case description: "Show information about a specific case",
* If the argument passed is a user id, show all cases on that user },
*/ })
@d.command("case", "<caseNumber:number>")
@d.permission("can_view") @d.permission("can_view")
async showCaseCmd(msg: Message, args: { caseNumber: number }) { async showCaseCmd(msg: Message, args: { caseNumber: number }) {
// Assume case id
const theCase = await this.cases.findByCaseNumber(args.caseNumber); const theCase = await this.cases.findByCaseNumber(args.caseNumber);
if (!theCase) { if (!theCase) {
@ -1411,6 +1470,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
shortcut: "h", shortcut: "h",
}, },
], ],
info: {
description: "Show a list of cases the specified user has",
},
}) })
@d.permission("can_view") @d.permission("can_view")
async userCasesCmd(msg: Message, args: { user: string; expand?: boolean; hidden?: boolean }) { async userCasesCmd(msg: Message, args: { user: string; expand?: boolean; hidden?: boolean }) {
@ -1475,6 +1537,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("cases", null, { @d.command("cases", null, {
options: [{ name: "mod", type: "Member" }], options: [{ name: "mod", type: "Member" }],
info: {
description: "Show the most recent 5 cases by the specified --mod",
},
}) })
@d.permission("can_view") @d.permission("can_view")
async recentCasesCmd(msg: Message, args: { mod?: Member }) { async recentCasesCmd(msg: Message, args: { mod?: Member }) {
@ -1500,7 +1565,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
} }
} }
@d.command("hidecase", "<caseNum:number>") @d.command("hidecase", "<caseNum:number>", {
info: {
description: "Hide the specified case so it doesn't appear in !cases or !info",
},
})
@d.permission("can_hidecase") @d.permission("can_hidecase")
async hideCaseCmd(msg: Message, args: { caseNum: number }) { async hideCaseCmd(msg: Message, args: { caseNum: number }) {
const theCase = await this.cases.findByCaseNumber(args.caseNum); const theCase = await this.cases.findByCaseNumber(args.caseNum);
@ -1515,7 +1584,11 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
); );
} }
@d.command("unhidecase", "<caseNum:number>") @d.command("unhidecase", "<caseNum:number>", {
info: {
description: "Un-hide the specified case, making it appear in !cases and !info again",
},
})
@d.permission("can_hidecase") @d.permission("can_hidecase")
async unhideCaseCmd(msg: Message, args: { caseNum: number }) { async unhideCaseCmd(msg: Message, args: { caseNum: number }) {
const theCase = await this.cases.findByCaseNumber(args.caseNum); const theCase = await this.cases.findByCaseNumber(args.caseNum);

View file

@ -71,7 +71,7 @@ export class MutesPlugin extends ZeppelinPlugin<TConfigSchema> {
protected serverLogs: GuildLogs; protected serverLogs: GuildLogs;
private muteClearIntervalId: NodeJS.Timer; private muteClearIntervalId: NodeJS.Timer;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
mute_role: null, mute_role: null,

View file

@ -13,12 +13,13 @@ type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export class NameHistoryPlugin extends ZeppelinPlugin<TConfigSchema> { export class NameHistoryPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "name_history"; public static pluginName = "name_history";
public static showInDocs = false;
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
protected nicknameHistory: GuildNicknameHistory; protected nicknameHistory: GuildNicknameHistory;
protected usernameHistory: UsernameHistory; protected usernameHistory: UsernameHistory;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_view: false, can_view: false,

View file

@ -22,7 +22,7 @@ export class PersistPlugin extends ZeppelinPlugin<TConfigSchema> {
protected persistedData: GuildPersistedData; protected persistedData: GuildPersistedData;
protected logs: GuildLogs; protected logs: GuildLogs;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
persisted_roles: [], persisted_roles: [],

View file

@ -21,7 +21,7 @@ export class PingableRolesPlugin extends ZeppelinPlugin<TConfigSchema> {
protected cache: Map<string, PingableRole[]>; protected cache: Map<string, PingableRole[]>;
protected timeouts: Map<string, any>; protected timeouts: Map<string, any>;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_manage: false, can_manage: false,

View file

@ -58,7 +58,7 @@ export class PostPlugin extends ZeppelinPlugin<TConfigSchema> {
clearTimeout(this.scheduledPostLoopTimeout); clearTimeout(this.scheduledPostLoopTimeout);
} }
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_post: false, can_post: false,

View file

@ -55,7 +55,7 @@ export class ReactionRolesPlugin extends ZeppelinPlugin<TConfigSchema> {
private autoRefreshTimeout; private autoRefreshTimeout;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
auto_refresh_interval: MIN_AUTO_REFRESH, auto_refresh_interval: MIN_AUTO_REFRESH,

View file

@ -32,7 +32,7 @@ export class RemindersPlugin extends ZeppelinPlugin<TConfigSchema> {
private postRemindersTimeout; private postRemindersTimeout;
private unloaded = false; private unloaded = false;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_use: false, can_use: false,

View file

@ -14,11 +14,12 @@ type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export class SelfGrantableRolesPlugin extends ZeppelinPlugin<TConfigSchema> { export class SelfGrantableRolesPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "self_grantable_roles"; public static pluginName = "self_grantable_roles";
public static showInDocs = false;
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
protected selfGrantableRoles: GuildSelfGrantableRoles; protected selfGrantableRoles: GuildSelfGrantableRoles;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_manage: false, can_manage: false,

View file

@ -42,7 +42,7 @@ export class SlowmodePlugin extends ZeppelinPlugin<TConfigSchema> {
private onMessageCreateFn; private onMessageCreateFn;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
use_native_slowmode: true, use_native_slowmode: true,

View file

@ -96,7 +96,7 @@ export class SpamPlugin extends ZeppelinPlugin<TConfigSchema> {
private expiryInterval; private expiryInterval;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
max_censor: null, max_censor: null,

View file

@ -25,6 +25,7 @@ type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
export class StarboardPlugin extends ZeppelinPlugin<TConfigSchema> { export class StarboardPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "starboard"; public static pluginName = "starboard";
public static showInDocs = false;
protected static configSchema = ConfigSchema; protected static configSchema = ConfigSchema;
protected starboards: GuildStarboards; protected starboards: GuildStarboards;
@ -32,7 +33,7 @@ export class StarboardPlugin extends ZeppelinPlugin<TConfigSchema> {
private onMessageDeleteFn; private onMessageDeleteFn;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_manage: false, can_manage: false,

View file

@ -34,7 +34,7 @@ export class TagsPlugin extends ZeppelinPlugin<TConfigSchema> {
protected tagFunctions; protected tagFunctions;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
prefix: "!!", prefix: "!!",
@ -117,7 +117,7 @@ export class TagsPlugin extends ZeppelinPlugin<TConfigSchema> {
} }
const prefix = this.getConfigForMsg(msg).prefix; const prefix = this.getConfigForMsg(msg).prefix;
const tagNames = tags.map(t => t.tag).sort(); const tagNames = tags.map(tag => tag.tag).sort();
msg.channel.createMessage(` msg.channel.createMessage(`
Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\` Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\`
`); `);

View file

@ -92,7 +92,7 @@ export class UtilityPlugin extends ZeppelinPlugin<TConfigSchema> {
protected lastFullMemberRefresh = 0; protected lastFullMemberRefresh = 0;
protected lastReload; protected lastReload;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> { public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return { return {
config: { config: {
can_roles: false, can_roles: false,

View file

@ -22,12 +22,21 @@ import { mergeConfig } from "knub/dist/configUtils";
const SLOW_RESOLVE_THRESHOLD = 1500; const SLOW_RESOLVE_THRESHOLD = 1500;
export interface PluginInfo { export interface PluginInfo {
name: string; prettyName: string;
description?: string; description?: string;
} }
export interface CommandInfo {
description?: string;
basicUsage?: string;
parameterDescriptions?: {
[key: string]: string;
};
}
export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plugin<TConfig> { export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plugin<TConfig> {
public static pluginInfo: PluginInfo; public static pluginInfo: PluginInfo;
public static showInDocs: boolean = true;
protected static configSchema: t.TypeC<any>; protected static configSchema: t.TypeC<any>;
public static dependencies = []; public static dependencies = [];
@ -51,7 +60,7 @@ export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plug
* we need a static version of getDefaultOptions(). This static version is then, * we need a static version of getDefaultOptions(). This static version is then,
* by turn, called from getDefaultOptions() so everything still works as expected. * by turn, called from getDefaultOptions() so everything still works as expected.
*/ */
protected static getStaticDefaultOptions() { public static getStaticDefaultOptions() {
// Implemented by plugin // Implemented by plugin
return {}; return {};
} }

View file

@ -244,6 +244,37 @@ export function asSingleLine(str: string) {
return trimLines(str).replace(/\n/g, " "); return trimLines(str).replace(/\n/g, " ");
} }
export function trimEmptyStartEndLines(str: string) {
const lines = str.split("\n");
let emptyLinesAtStart = 0;
let emptyLinesAtEnd = 0;
for (const line of lines) {
if (line.match(/^\s*$/)) {
emptyLinesAtStart++;
} else {
break;
}
}
for (let i = lines.length - 1; i > 0; i--) {
if (lines[i].match(/^\s*$/)) {
emptyLinesAtEnd++;
} else {
break;
}
}
return lines.slice(emptyLinesAtStart, emptyLinesAtEnd ? -1 * emptyLinesAtEnd : null).join("\n");
}
export function trimIndents(str: string, indentLength: number) {
return str
.split("\n")
.map(line => line.slice(indentLength))
.join("\n");
}
export const emptyEmbedValue = "\u200b"; export const emptyEmbedValue = "\u200b";
export const embedPadding = "\n" + emptyEmbedValue; export const embedPadding = "\n" + emptyEmbedValue;