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

View file

@ -23,6 +23,8 @@
"bulma": "^0.7.5",
"bulmaswatch": "^0.7.2",
"js-cookie": "^2.2.0",
"js-yaml": "^3.13.1",
"marked": "^0.7.0",
"vue": "^2.6.10",
"vue-codemirror": "^4.0.6",
"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>
<div>
<h1>Configuration format</h1>
<p>
<h1 class="z-title is-1 mb-1">Configuration format</h1>
<p class="mb-1">
This is the basic format of the bot configuration for a guild. The basic breakdown is:
</p>
<ol>
<ol class="z-list mb-1">
<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>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>
<div>
<h1>Introduction</h1>
<p>
<h1 class="z-title is-1 mb-1">Introduction</h1>
<p class="mb-1">
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
</p>
<h2>Getting the bot</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Getting the bot</h2>
<p class="mb-1">
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.
</p>
<h2>Configuration</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Configuration</h2>
<p class="mb-1">
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.
</p>
<h2>Plugins</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Plugins</h2>
<p class="mb-1">
Zeppelin is divided into plugins: grouped functionality that can be enabled/disabled as needed, and that have their own configurations.
</p>
<h2>Commands</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Commands</h2>
<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.
</p>
</div>

View file

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

View file

@ -1,23 +1,23 @@
<template>
<div>
<h1>Permissions</h1>
<p>
<h1 class="z-title is-1 mb-1">Permissions</h1>
<p class="mb-1">
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)
and can depend on e.g. user id, role id, channel id, category id, or <strong>permission level</strong>.
</p>
<h2>Permission levels</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Permission levels</h2>
<p class="mb-1">
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
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 class="mb-1">
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).
</p>
<p>
<p class="mb-1">
Permission levels are defined in the config in the <strong>levels</strong> section. For example:
</p>
@ -28,14 +28,14 @@
"172950000412655616": 50 # Example mod
</CodeBlock>
<h2>Examples</h2>
<h2 class="z-title is-2 mt-2 mb-1">Examples</h2>
<h3>Basic overrides</h3>
<p>
<h3 class="z-title is-3 mb-1">Basic overrides</h3>
<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>.
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 class="mb-1">
Here's what the configuration for this would look like:
</p>
@ -51,15 +51,15 @@
can_cat: true
</CodeBlock>
<h3>Replacing defaults</h3>
<p>
<h3 class="z-title is-3 mt-2 mb-1">Replacing defaults</h3>
<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.
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.
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.
</p>
<p>
<p class="mb-1">
Here's what the configuration for this would look like:
</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>
<div>
<h1>Plugin configuration</h1>
<p>
<h1 class="z-title is-1 mb-1">Plugin configuration</h1>
<p class="mb-1">
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
the same rules with overrides etc. as other options (see <router-link to="/docs/permissions">Permissions</router-link> for more info).
</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.
</p>
<h2>Overrides</h2>
<p>
<h2 class="z-title is-2 mt-2 mb-1">Overrides</h2>
<p class="mb-1">
Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc.
</p>
<p>
<p class="mb-1">
Here's an example demonstrating different types of overrides:
</p>

View file

@ -36,28 +36,12 @@ export const router = new VueRouter({
component: () => import("./components/docs/PluginConfiguration.vue"),
},
{
path: "descriptions",
component: () => import("./components/docs/descriptions/Layout.vue"),
children: [
{
path: "argument-types",
component: () => import("./components/docs/descriptions/ArgumentTypes.vue"),
},
],
path: "descriptions/argument-types",
component: () => import("./components/docs/ArgumentTypes.vue"),
},
{
path: "plugins",
component: () => import("./components/docs/plugins/Layout.vue"),
children: [
{
path: "mod-actions",
component: () => import("./components/docs/plugins/ModActions.vue"),
},
{
path: "locate-user",
component: () => import("./components/docs/plugins/LocateUser.vue"),
},
],
path: "plugins/:pluginName",
component: () => import("./components/docs/Plugin.vue"),
},
],
},
@ -80,11 +64,15 @@ export const router = new VueRouter({
},
],
scrollBehavior(to) {
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
return {
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 { AuthStore } from "./auth";
import { GuildStore } from "./guilds";
import { DocsStore } from "./docs";
export const RootStore = new Vuex.Store<RootState>({
modules: {
auth: AuthStore,
guilds: GuildStore,
docs: DocsStore,
},
});
// Set up typings so Vue/our components know about the state's types
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
// @ts-ignore
store?: Store<RootState>;
}
}
declare module "vue/types/vue" {
interface Vue {
// @ts-ignore
$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 = {
auth: AuthState;
guilds: GuildState;
docs: DocsState;
};

View file

@ -1,6 +1,12 @@
@import url('https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css');
$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 "~bulma/bulma";
@ -9,3 +15,30 @@ $family-primary: 'Open Sans', sans-serif;
.docs-cloak {
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 { initGuildsAPI } from "./guilds";
import { initArchives } from "./archives";
import { initDocs } from "./docs";
import { connect } from "../data/db";
import path from "path";
import { TokenError } from "passport-oauth2";
@ -12,15 +13,13 @@ import { PluginError } from "knub";
require("dotenv").config({ path: path.resolve(__dirname, "..", "..", "api.env") });
function errorHandler(err) {
// tslint:disable:no-console
console.error(err.stack || err);
console.error(err.stack || err); // tslint:disable-line:no-console
process.exit(1);
// tslint:enable:no-console
}
process.on("unhandledRejection", errorHandler);
console.log("Connecting to database...");
console.log("Connecting to database..."); // tslint:disable-line
connect().then(() => {
const app = express();
@ -34,6 +33,7 @@ connect().then(() => {
initAuth(app);
initGuildsAPI(app);
initArchives(app);
initDocs(app);
// Default route
app.get("/", (req, res) => {
@ -45,7 +45,7 @@ connect().then(() => {
if (err instanceof TokenError) {
clientError(res, "Invalid code");
} else {
console.error(err);
console.error(err); // tslint:disable-line
error(res, "Server error", err.status || 500);
}
});
@ -56,5 +56,6 @@ connect().then(() => {
});
const port = process.env.PORT || 3000;
// tslint:disable-next-line
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;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return {
config: {
can_manage: false,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -70,12 +70,13 @@ class ActionError extends Error {}
export class CustomEventsPlugin extends ZeppelinPlugin<TConfigSchema> {
public static pluginName = "custom_events";
public static showInDocs = false;
public static dependencies = ["cases"];
protected static configSchema = ConfigSchema;
private clearTriggers: () => void;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return {
config: {
events: {},
@ -162,7 +163,7 @@ export class CustomEventsPlugin extends ZeppelinPlugin<TConfigSchema> {
const casesPlugin = this.getPlugin<CasesPlugin>("cases");
await casesPlugin.createCase({
userId: targetId,
modId: modId,
modId,
type: CaseTypes[action.case_type],
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,
* by turn, called from getDefaultOptions() so everything still works as expected.
*/
protected static getStaticDefaultOptions() {
public static getStaticDefaultOptions() {
// Implemented by plugin
return {};
}

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,8 @@ import {
stripObjectToScalars,
successMessage,
tNullable,
trimEmptyStartEndLines,
trimIndents,
trimLines,
ucfirst,
UnknownUser,
@ -68,11 +70,58 @@ interface IIgnoredEvent {
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> {
public static pluginName = "mod_actions";
public static dependencies = ["cases", "mutes"];
protected static configSchema = ConfigSchema;
public static pluginInfo = {
prettyName: "Mod actions",
description: trimIndents(
trimEmptyStartEndLines(`
Testing **things**
Multiline haHAA
`),
6,
),
};
protected mutes: GuildMutes;
protected cases: GuildCases;
protected serverLogs: GuildLogs;
@ -87,7 +136,7 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
this.ignoredEvents = [];
}
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return {
config: {
dm_on_warn: true,
@ -439,6 +488,10 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("update", "<caseNumber:number> [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")
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`));
}
@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")
async noteCmd(msg: Message, args: { user: string; note: string }) {
const user = await this.resolveUser(args.user);
@ -500,6 +557,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
@d.command("warn", "<user:string> <reason:string$>", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Send a warning to the specified user",
},
})
@d.permission("can_warn")
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$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }],
info: {
description: "Mute the specified member",
},
})
@d.permission("can_mute")
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$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }],
info: {
description: "Force-mute the specified user, even if they're not on the server",
},
})
@d.permission("can_mute")
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$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }],
info: {
description: "Unmute the specified member",
},
})
@d.permission("can_mute")
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$>", {
overloads: ["<user:string> <time:delay>", "<user:string> [reason:string$]"],
options: [{ name: "mod", type: "member" }],
info: {
description: "Force-unmute the specified user, even if they're not on the server",
},
})
@d.permission("can_mute")
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$]", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Kick the specified member",
},
})
@d.permission("can_kick")
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$]", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Ban the specified member",
},
})
@d.permission("can_ban")
async banCmd(msg, args: { user: string; reason?: string; mod?: Member }) {
@ -977,62 +1055,32 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
mod = args.mod;
}
const config = this.getConfig();
const reason = this.formatReasonWithAttachments(args.reason, msg.attachments);
// Attempt to message the user *before* banning them, as doing it after may not be possible
let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
if (reason) {
const banMessage = await renderTemplate(config.ban_message, {
guildName: this.guild.name,
reason,
const banResult = await this.banUserId(memberToBan.id, reason, {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : null,
});
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"));
if (banResult.status === "failed") {
msg.channel.createMessage(errorMessage(`Failed to ban member`));
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
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));
// Log the action
this.serverLogs.log(LogType.MEMBER_BAN, {
mod: stripObjectToScalars(mod.user),
user: stripObjectToScalars(memberToBan.user),
});
}
@d.command("softban", "<user:string> [reason:string$]", {
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")
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$]", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Unban the specified member",
},
})
@d.permission("can_ban")
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$]", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Force-ban the specified user, even if they aren't on the server",
},
})
@d.permission("can_ban")
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")
async massbanCmd(msg: Message, args: { userIds: string[] }) {
// 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$]", {
options: [{ name: "mod", type: "member" }],
info: {
description: "Add an arbitrary case to the specified user without taking any action",
},
})
@d.permission("can_addcase")
async addcaseCmd(msg: Message, args: { type: string; user: string; reason?: string; mod?: Member }) {
@ -1377,15 +1438,13 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
});
}
/**
* Display a case or list of cases
* If the argument passed is a case id, display that case
* If the argument passed is a user id, show all cases on that user
*/
@d.command("case", "<caseNumber:number>")
@d.command("case", "<caseNumber:number>", {
info: {
description: "Show information about a specific case",
},
})
@d.permission("can_view")
async showCaseCmd(msg: Message, args: { caseNumber: number }) {
// Assume case id
const theCase = await this.cases.findByCaseNumber(args.caseNumber);
if (!theCase) {
@ -1411,6 +1470,9 @@ export class ModActionsPlugin extends ZeppelinPlugin<TConfigSchema> {
shortcut: "h",
},
],
info: {
description: "Show a list of cases the specified user has",
},
})
@d.permission("can_view")
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, {
options: [{ name: "mod", type: "Member" }],
info: {
description: "Show the most recent 5 cases by the specified --mod",
},
})
@d.permission("can_view")
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")
async hideCaseCmd(msg: Message, args: { caseNum: number }) {
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")
async unhideCaseCmd(msg: Message, args: { caseNum: number }) {
const theCase = await this.cases.findByCaseNumber(args.caseNum);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ export class TagsPlugin extends ZeppelinPlugin<TConfigSchema> {
protected tagFunctions;
protected static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
return {
config: {
prefix: "!!",
@ -117,7 +117,7 @@ export class TagsPlugin extends ZeppelinPlugin<TConfigSchema> {
}
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(`
Available tags (use with ${prefix}tag): \`\`\`${tagNames.join(", ")}\`\`\`
`);

View file

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

View file

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

View file

@ -244,6 +244,37 @@ export function asSingleLine(str: string) {
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 embedPadding = "\n" + emptyEmbedValue;