Skip to main content

Plugin configuration

Each plugin can be configured individually for each guild (server). Plugin configuration is included in each server's config (see also: getConfig Knub option).

Server configuration

A server configuration has the following basic format. Note the plugin configuration section, where each plugin gets their configuration from.

{
// Optional. Message command prefix.
"prefix": "!",

// Role- and user-specific permission levels for plugin configuration
"levels": {
"role id": 100,
"user id": 50
},

// Plugin configuration
// The key in the object is the plugin's internal name
"plugins": {
// Configuration for plugin "my-plugin"
"my-plugin": {
// Base configuration, before overrides
"config": {
"coolness": 0
},
// Overrides
// For more details, see "Overrides" section further below
"overrides": [
// When level (see "levels" above) is 50 or greater, set "coolness" to 10
{
"level": ">=50",
"config": {
"coolness": 10
}
},
// When level (see "levels" above) is 100 or greater, set "coolness" to 10000
{
"level": ">=100",
"config": {
"coolness": 10000
}
}
]
}
}
}

Overrides

Overrides can be used to change a plugin's configuration for a specific role, user, channel, etc. These overrides are applied automatically when checking message command permissions, and also when getting config within a command or event listener with e.g. pluginData.config.getForMember(member).

For example, with the configuration above:

// Assuming this command is within the plugin "my-plugin"...
guildPluginSlashCommand({
// ...
async run({ pluginData, interaction }) {
const userConfig = await pluginData.config.getForUser(interaction.user);
// If the user has the role id for level 100: "Your coolness: 10000"
// If the user has the user id for level 50: "Your coolness: 10"
// Otherwise: "Your coolness: 0"
interaction.reply(`Your coolness: ${userConfig.coolness}`);
}
})

See the bottom of this page for examples of different kinds of overrides.

Types and validation for plugin config

You may have noticed a configParser property in the plugin examples in the docs. This is a function that takes untrusted input (the plugin's configuration from the server configuration) and returns a valid config.
Many of the examples use configParser: () => ({}), which is fine when you don't have configurable options at all, but what about when you do?

Using the guildPlugin() helper, we can specify a PluginType:

interface MyPluginType extends BasePluginType {
config: {
coolness: number;
};
}

// Note the type annotation and double () here
const myPlugin = guildPlugin<MyPluginType>()({
name: "my-plugin",

// Error! Since we specified a type, this function's return type must match it.
configParser: () => ({}),

// Better. Now the input is validated properly.
configParser: (input) => {
if (! ("coolness" in input)) {
throw new Error("You must specify coolness");
}

if (typeof input.coolness !== "number") {
throw new Error("Coolness must be a number");
}

return {
coolness: input.coolness,
};
},
});

However, doing the validation this way can get very cumbersome very fast. Ideally, we'd be able to specify a type for the config and automatically parse it.

This is where libraries like zod come in. Here's an example using zod:

import { z } from "zod";

const configSchema = z.object({
coolness: z.number(),
});
// The TypeScript type and zod schema are automatically in sync!
type TConfigSchema = z.TypeOf<typeof configSchema>;

interface MyPluginType extends BasePluginType {
config: TConfigSchema;
}

// Note the type annotation and double () here
const myPlugin = guildPlugin<MyPluginType>()({
name: "my-plugin",

// All we have to do here is use zod's parse() method
configParser: (input) => configSchema.parse(input),
});

This may seem like boilerplate at first glance, but specifying the config's type allows it to be inferred elsewhere, such as in commands:

// Note the type annotation and double () here again
const myCommand = guildPluginSlashCommand<MyPluginType>()({
// ...
async run({ interaction, pluginData }) {
// TypeScript knows that "userCoolness" is a number now!
// And, thanks to configParser, we can trust that the number is there.
const userCoolness = await pluginData.config.getForUser(interaction.user);
interaction.reply(`Your coolness: ${userCoolness}`);
}
});

Ok, but what about default options? If you have a lot of options, you probably don't want to require specifying them all in the config, especially if you can give them reasonable defaults.

Here's where defaultOptions come in:

const myPlugin = guildPlugin<MyPluginType>()({
name: "my-plugin",

// All we have to do here is use zod's parse() method
configParser: (input) => configSchema.parse(input),

// These too are type checked
defaultOptions: {
config: {
coolness: 0,
},
},
});

The untrusted input is merged with the config from defaultOptions before being passed to configParser. This effectively makes the coolness property optional in the user-provided configuration.

Override examples

Note: can_kick in these examples is entirely arbitrary and does not inherently grant any permissions.

{
"plugins": {
"example_plugin": {
// Base config
"config": {
"can_kick": false,
"kick_message": "You have been kicked",
"nested": {
"value": "Hello",
"other_value": "Foo"
}
},

// Overrides
"overrides": [
// Simple permission level based override to allow kicking only for levels 50 and up
{
"level": ">=50",
"config": {
"can_kick": true,
"nested": {
"other_value": "Bar"
}
}
},
// Channel override - don't allow kicking on the specified channel
{
"channel": "109672661671505920",
"config": {
"can_kick": false
}
},
// Don't allow kicking from any thread
{
"is_thread": true,
"config": {
"can_kick": false
}
},
// Don't allow kicking from a specific thread
{
"thread_id": "109672661671505920",
"config": {
"can_kick": false
}
},
// Don't allow kicking within a specific category
{
"category": "360735466737369109",
"config": {
"can_kick": false
}
},
// Multiple channels. If any of them match, this override applies.
{
"channel": ["109672661671505920", "570714864285253677"],
"config": {
"can_kick": false
}
},
// Give a specific role permission to kick
{
"role": "172950000412655616",
"config": {
"can_kick": true
}
},
// Match based on multiple roles. The user must have ALL roles mentioned here for this override to apply.
{
"role": ["172950000412655616", "172949857164722176"],
"config": {
"can_kick": true
}
},
// Match on user id
{
"role": "106391128718245888",
"config": {
"kick_message": "You have been kicked by Dragory"
}
},
// Match on multiple conditions
// All of them must apply
{
"channel": "109672661671505920",
"role": "172950000412655616",
"config": {
"can_kick": false
}
},
// Match on ANY of multiple conditions
{
"any": [
{ "channel": "109672661671505920" },
{ "role": "172950000412655616" }
],
"config": {
"can_kick": false
}
},
// Match on either of two complex conditions
{
"any": [
{
"all": [
{ "channel": "109672661671505920" },
{ "role": "172950000412655616" },
{
"not": {
"role": "473085927053590538"
}
}
]
},
{
"channel": "534727637017559040",
"role": "473086848831455234",
}
],
"config": {
"can_kick": false
}
}
]
}
}
}