-
Permissions
-
+
Permissions
+
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 Plugin configuration for more info)
and can depend on e.g. user id, role id, channel id, category id, or permission level.
-
Permission levels
-
+
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
be used in permission overrides. By default, several commands are "moderator only" (level 50 and up) or "admin only" (level 100 and up).
-
+
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).
-
+
Permission levels are defined in the config in the levels section. For example:
@@ -28,14 +28,14 @@
"172950000412655616": 50 # Example mod
-
Examples
+
Examples
-
Basic overrides
-
+
Basic overrides
+
For this example, let's assume we have a plugin called cats
which has a command !cat
locked behind the permission can_cat
.
Let's say that by default, the plugin allows anyone to use !cat
, but we want to restrict it to moderators only.
-
+
Here's what the configuration for this would look like:
@@ -51,15 +51,15 @@
can_cat: true
-
Replacing defaults
-
+
Replacing defaults
+
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...
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 replace the default overrides that enable moderator commands at level 50.
-
+
Here's what the configuration for this would look like:
diff --git a/dashboard/src/components/docs/Plugin.vue b/dashboard/src/components/docs/Plugin.vue
new file mode 100644
index 00000000..b8d58297
--- /dev/null
+++ b/dashboard/src/components/docs/Plugin.vue
@@ -0,0 +1,77 @@
+
+
+ Loading...
+
+
+
{{ data.info.prettyName || data.name }}
+
+ Name in config: {{ data.name }}
+
+
+
+
+
Default configuration
+
{{ renderConfiguration(data.options) }}
+
+
+
Commands
+
+
!{{ command.trigger }}
+
+ Permission: {{ command.config.requiredPermission }}
+
+
+ Basic usage: {{ command.config.info.basicUsage }}
+
+
+ Shortcut:
+ !{{ alias }}
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/docs/PluginConfiguration.vue b/dashboard/src/components/docs/PluginConfiguration.vue
index 2b2555a8..2caa40e5 100644
--- a/dashboard/src/components/docs/PluginConfiguration.vue
+++ b/dashboard/src/components/docs/PluginConfiguration.vue
@@ -1,20 +1,20 @@
-
Plugin configuration
-
+
Plugin configuration
+
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 Permissions for more info).
-
+
Information about each plugin's options can be found on the plugin's page on the sidebar. See Configuration format for an example of a full config.
-
Overrides
-
+
Overrides
+
Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc.
-
+
Here's an example demonstrating different types of overrides:
diff --git a/dashboard/src/routes.ts b/dashboard/src/routes.ts
index 4463c075..f90b6171 100644
--- a/dashboard/src/routes.ts
+++ b/dashboard/src/routes.ts
@@ -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 };
}
},
});
diff --git a/dashboard/src/store/docs.ts b/dashboard/src/store/docs.ts
new file mode 100644
index 00000000..44203c8e
--- /dev/null
+++ b/dashboard/src/store/docs.ts
@@ -0,0 +1,54 @@
+import { get } from "../api";
+import { Module } from "vuex";
+import { DocsState, RootState } from "./types";
+
+export const DocsStore: Module
= {
+ 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;
+ },
+ },
+};
diff --git a/dashboard/src/store/index.ts b/dashboard/src/store/index.ts
index 4d53ae51..c4aadd83 100644
--- a/dashboard/src/store/index.ts
+++ b/dashboard/src/store/index.ts
@@ -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({
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 {
+ // @ts-ignore
store?: Store;
}
}
declare module "vue/types/vue" {
interface Vue {
+ // @ts-ignore
$store: Store;
}
}
diff --git a/dashboard/src/store/types.ts b/dashboard/src/store/types.ts
index b4ee3af4..82fae8b0 100644
--- a/dashboard/src/store/types.ts
+++ b/dashboard/src/store/types.ts
@@ -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;
};
diff --git a/dashboard/src/style/docs.scss b/dashboard/src/style/docs.scss
index c4d48a3f..33f7c9a3 100644
--- a/dashboard/src/style/docs.scss
+++ b/dashboard/src/style/docs.scss
@@ -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; }
diff --git a/src/api/docs.ts b/src/api/docs.ts
new file mode 100644
index 00000000..d35e8ab6
--- /dev/null
+++ b/src/api/docs.ts
@@ -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,
+ });
+ });
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index 8daeda13..4dc9c942 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -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}`));
});
diff --git a/src/plugins/AutoReactionsPlugin.ts b/src/plugins/AutoReactionsPlugin.ts
index c8cfe67a..bb070613 100644
--- a/src/plugins/AutoReactionsPlugin.ts
+++ b/src/plugins/AutoReactionsPlugin.ts
@@ -21,7 +21,7 @@ export class AutoReactionsPlugin extends ZeppelinPlugin {
private onMessageCreateFn;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_manage: false,
diff --git a/src/plugins/Automod.ts b/src/plugins/Automod.ts
index 8fed54f2..00a152b8 100644
--- a/src/plugins/Automod.ts
+++ b/src/plugins/Automod.ts
@@ -358,7 +358,7 @@ export class AutomodPlugin extends ZeppelinPlugin {
return config;
}
- protected static getStaticDefaultOptions() {
+ public static getStaticDefaultOptions() {
return {
rules: [],
};
diff --git a/src/plugins/BotControl.ts b/src/plugins/BotControl.ts
index 2345438e..25eedc64 100644
--- a/src/plugins/BotControl.ts
+++ b/src/plugins/BotControl.ts
@@ -27,7 +27,7 @@ export class BotControlPlugin extends GlobalZeppelinPlugin {
protected archives: GuildArchives;
- protected static getStaticDefaultOptions() {
+ public static getStaticDefaultOptions() {
return {
config: {
can_use: false,
diff --git a/src/plugins/Cases.ts b/src/plugins/Cases.ts
index 5d333435..ff265591 100644
--- a/src/plugins/Cases.ts
+++ b/src/plugins/Cases.ts
@@ -51,7 +51,7 @@ export class CasesPlugin extends ZeppelinPlugin {
protected archives: GuildArchives;
protected logs: GuildLogs;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
log_automatic_actions: true,
diff --git a/src/plugins/Censor.ts b/src/plugins/Censor.ts
index 69a9ad86..791403ed 100644
--- a/src/plugins/Censor.ts
+++ b/src/plugins/Censor.ts
@@ -46,7 +46,7 @@ export class CensorPlugin extends ZeppelinPlugin {
private onMessageCreateFn;
private onMessageUpdateFn;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
filter_zalgo: false,
diff --git a/src/plugins/CompanionChannels.ts b/src/plugins/CompanionChannels.ts
index a8771601..dc3f16c9 100644
--- a/src/plugins/CompanionChannels.ts
+++ b/src/plugins/CompanionChannels.ts
@@ -30,7 +30,7 @@ export class CompanionChannelPlugin extends ZeppelinPlugin {
public static pluginName = "companion_channels";
protected static configSchema = ConfigSchema;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
entries: {},
diff --git a/src/plugins/CustomEvents.ts b/src/plugins/CustomEvents.ts
index aaeb9926..a2c89ab9 100644
--- a/src/plugins/CustomEvents.ts
+++ b/src/plugins/CustomEvents.ts
@@ -70,12 +70,13 @@ class ActionError extends Error {}
export class CustomEventsPlugin extends ZeppelinPlugin {
public static pluginName = "custom_events";
+ public static showInDocs = false;
public static dependencies = ["cases"];
protected static configSchema = ConfigSchema;
private clearTriggers: () => void;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
events: {},
@@ -162,7 +163,7 @@ export class CustomEventsPlugin extends ZeppelinPlugin {
const casesPlugin = this.getPlugin("cases");
await casesPlugin.createCase({
userId: targetId,
- modId: modId,
+ modId,
type: CaseTypes[action.case_type],
reason: `__[${event.name}]__ ${reason}`,
});
diff --git a/src/plugins/GlobalZeppelinPlugin.ts b/src/plugins/GlobalZeppelinPlugin.ts
index c49da5aa..50d40689 100644
--- a/src/plugins/GlobalZeppelinPlugin.ts
+++ b/src/plugins/GlobalZeppelinPlugin.ts
@@ -21,7 +21,7 @@ export class GlobalZeppelinPlugin 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 {};
}
diff --git a/src/plugins/GuildInfoSaver.ts b/src/plugins/GuildInfoSaver.ts
index 9c1108c4..2dfd591b 100644
--- a/src/plugins/GuildInfoSaver.ts
+++ b/src/plugins/GuildInfoSaver.ts
@@ -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;
diff --git a/src/plugins/LocateUser.ts b/src/plugins/LocateUser.ts
index ada922aa..e9cf853e 100644
--- a/src/plugins/LocateUser.ts
+++ b/src/plugins/LocateUser.ts
@@ -23,7 +23,7 @@ export class LocatePlugin extends ZeppelinPlugin {
private outdatedAlertsTimeout;
private usersWithAlerts: string[] = [];
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_where: false,
diff --git a/src/plugins/Logs.ts b/src/plugins/Logs.ts
index 5e23d308..349f8eb3 100644
--- a/src/plugins/Logs.ts
+++ b/src/plugins/Logs.ts
@@ -70,7 +70,7 @@ export class LogsPlugin extends ZeppelinPlugin {
private excludedUserProps = ["user", "member", "mod"];
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
channels: {},
diff --git a/src/plugins/MessageSaver.ts b/src/plugins/MessageSaver.ts
index ca4d138d..a8c3bb89 100644
--- a/src/plugins/MessageSaver.ts
+++ b/src/plugins/MessageSaver.ts
@@ -12,11 +12,12 @@ type TConfigSchema = t.TypeOf;
export class MessageSaverPlugin extends ZeppelinPlugin {
public static pluginName = "message_saver";
+ public static showInDocs = false;
protected static configSchema = ConfigSchema;
protected savedMessages: GuildSavedMessages;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_manage: false,
diff --git a/src/plugins/ModActions.ts b/src/plugins/ModActions.ts
index 6545f62c..2b6edd42 100644
--- a/src/plugins/ModActions.ts
+++ b/src/plugins/ModActions.ts
@@ -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;
+
export class ModActionsPlugin extends ZeppelinPlugin {
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 {
this.ignoredEvents = [];
}
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
dm_on_warn: true,
@@ -439,6 +488,10 @@ export class ModActionsPlugin extends ZeppelinPlugin {
@d.command("update", " [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 {
msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`));
}
- @d.command("note", " ")
+ @d.command("note", " ", {
+ 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 {
@d.command("warn", " ", {
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 {
@d.command("mute", " ", {
overloads: [" ", " [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 {
@d.command("forcemute", " ", {
overloads: [" ", " [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 {
@d.command("unmute", " ", {
overloads: [" ", " [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 {
@d.command("forceunmute", " ", {
overloads: [" ", " [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 {
@d.command("kick", " [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 {
@d.command("ban", " [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 {
mod = args.mod;
}
- const config = this.getConfig();
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
- let userMessageResult: INotifyUserResult = { status: NotifyUserStatus.Ignored };
- 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"));
+ if (banResult.status === "failed") {
+ msg.channel.createMessage(errorMessage(`Failed to ban member`));
return;
}
- // Create a case for this action
- const casesPlugin = this.getPlugin("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", " [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 {
@d.command("unban", " [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 {
@d.command("forceban", " [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 {
});
}
- @d.command("massban", "")
+ @d.command("massban", "", {
+ 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 {
@d.command("addcase", " [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 {
});
}
- /**
- * 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", "")
+ @d.command("case", "", {
+ 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 {
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 {
@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 {
}
}
- @d.command("hidecase", "")
+ @d.command("hidecase", "", {
+ 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 {
);
}
- @d.command("unhidecase", "")
+ @d.command("unhidecase", "", {
+ 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);
diff --git a/src/plugins/Mutes.ts b/src/plugins/Mutes.ts
index de9c6e38..e86e93d4 100644
--- a/src/plugins/Mutes.ts
+++ b/src/plugins/Mutes.ts
@@ -71,7 +71,7 @@ export class MutesPlugin extends ZeppelinPlugin {
protected serverLogs: GuildLogs;
private muteClearIntervalId: NodeJS.Timer;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
mute_role: null,
diff --git a/src/plugins/NameHistory.ts b/src/plugins/NameHistory.ts
index 673372d5..d8756c1e 100644
--- a/src/plugins/NameHistory.ts
+++ b/src/plugins/NameHistory.ts
@@ -13,12 +13,13 @@ type TConfigSchema = t.TypeOf;
export class NameHistoryPlugin extends ZeppelinPlugin {
public static pluginName = "name_history";
+ public static showInDocs = false;
protected static configSchema = ConfigSchema;
protected nicknameHistory: GuildNicknameHistory;
protected usernameHistory: UsernameHistory;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_view: false,
diff --git a/src/plugins/Persist.ts b/src/plugins/Persist.ts
index 1e3b8d74..801c7b08 100644
--- a/src/plugins/Persist.ts
+++ b/src/plugins/Persist.ts
@@ -22,7 +22,7 @@ export class PersistPlugin extends ZeppelinPlugin {
protected persistedData: GuildPersistedData;
protected logs: GuildLogs;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
persisted_roles: [],
diff --git a/src/plugins/PingableRolesPlugin.ts b/src/plugins/PingableRolesPlugin.ts
index 7c3fff54..95998436 100644
--- a/src/plugins/PingableRolesPlugin.ts
+++ b/src/plugins/PingableRolesPlugin.ts
@@ -21,7 +21,7 @@ export class PingableRolesPlugin extends ZeppelinPlugin {
protected cache: Map;
protected timeouts: Map;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_manage: false,
diff --git a/src/plugins/Post.ts b/src/plugins/Post.ts
index 4d8572bd..46e74dc4 100644
--- a/src/plugins/Post.ts
+++ b/src/plugins/Post.ts
@@ -58,7 +58,7 @@ export class PostPlugin extends ZeppelinPlugin {
clearTimeout(this.scheduledPostLoopTimeout);
}
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_post: false,
diff --git a/src/plugins/ReactionRoles.ts b/src/plugins/ReactionRoles.ts
index 1520a9ac..0d6dc570 100644
--- a/src/plugins/ReactionRoles.ts
+++ b/src/plugins/ReactionRoles.ts
@@ -55,7 +55,7 @@ export class ReactionRolesPlugin extends ZeppelinPlugin {
private autoRefreshTimeout;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
auto_refresh_interval: MIN_AUTO_REFRESH,
diff --git a/src/plugins/Reminders.ts b/src/plugins/Reminders.ts
index 6f2c214d..3be96382 100644
--- a/src/plugins/Reminders.ts
+++ b/src/plugins/Reminders.ts
@@ -32,7 +32,7 @@ export class RemindersPlugin extends ZeppelinPlugin {
private postRemindersTimeout;
private unloaded = false;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_use: false,
diff --git a/src/plugins/SelfGrantableRolesPlugin.ts b/src/plugins/SelfGrantableRolesPlugin.ts
index bf9cb936..370a81dc 100644
--- a/src/plugins/SelfGrantableRolesPlugin.ts
+++ b/src/plugins/SelfGrantableRolesPlugin.ts
@@ -14,11 +14,12 @@ type TConfigSchema = t.TypeOf;
export class SelfGrantableRolesPlugin extends ZeppelinPlugin {
public static pluginName = "self_grantable_roles";
+ public static showInDocs = false;
protected static configSchema = ConfigSchema;
protected selfGrantableRoles: GuildSelfGrantableRoles;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_manage: false,
diff --git a/src/plugins/Slowmode.ts b/src/plugins/Slowmode.ts
index df4801d0..bd402397 100644
--- a/src/plugins/Slowmode.ts
+++ b/src/plugins/Slowmode.ts
@@ -42,7 +42,7 @@ export class SlowmodePlugin extends ZeppelinPlugin {
private onMessageCreateFn;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
use_native_slowmode: true,
diff --git a/src/plugins/Spam.ts b/src/plugins/Spam.ts
index d398fb3f..6c7687db 100644
--- a/src/plugins/Spam.ts
+++ b/src/plugins/Spam.ts
@@ -96,7 +96,7 @@ export class SpamPlugin extends ZeppelinPlugin {
private expiryInterval;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
max_censor: null,
diff --git a/src/plugins/Starboard.ts b/src/plugins/Starboard.ts
index dcf637a6..89ac79a8 100644
--- a/src/plugins/Starboard.ts
+++ b/src/plugins/Starboard.ts
@@ -25,6 +25,7 @@ type TConfigSchema = t.TypeOf;
export class StarboardPlugin extends ZeppelinPlugin {
public static pluginName = "starboard";
+ public static showInDocs = false;
protected static configSchema = ConfigSchema;
protected starboards: GuildStarboards;
@@ -32,7 +33,7 @@ export class StarboardPlugin extends ZeppelinPlugin {
private onMessageDeleteFn;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_manage: false,
diff --git a/src/plugins/Tags.ts b/src/plugins/Tags.ts
index 2937f026..cd17c4a8 100644
--- a/src/plugins/Tags.ts
+++ b/src/plugins/Tags.ts
@@ -34,7 +34,7 @@ export class TagsPlugin extends ZeppelinPlugin {
protected tagFunctions;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
prefix: "!!",
@@ -117,7 +117,7 @@ export class TagsPlugin extends ZeppelinPlugin {
}
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(", ")}\`\`\`
`);
diff --git a/src/plugins/Utility.ts b/src/plugins/Utility.ts
index 1e66c80f..4ffc733a 100644
--- a/src/plugins/Utility.ts
+++ b/src/plugins/Utility.ts
@@ -92,7 +92,7 @@ export class UtilityPlugin extends ZeppelinPlugin {
protected lastFullMemberRefresh = 0;
protected lastReload;
- protected static getStaticDefaultOptions(): IPluginOptions {
+ public static getStaticDefaultOptions(): IPluginOptions {
return {
config: {
can_roles: false,
diff --git a/src/plugins/ZeppelinPlugin.ts b/src/plugins/ZeppelinPlugin.ts
index 0b02bcde..e76a36b1 100644
--- a/src/plugins/ZeppelinPlugin.ts
+++ b/src/plugins/ZeppelinPlugin.ts
@@ -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 extends Plugin {
public static pluginInfo: PluginInfo;
+ public static showInDocs: boolean = true;
protected static configSchema: t.TypeC;
public static dependencies = [];
@@ -51,7 +60,7 @@ export class ZeppelinPlugin 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 {};
}
diff --git a/src/utils.ts b/src/utils.ts
index eb85f2e4..669d3dbf 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -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;