Cumcord plugins are in their most basic form, a JavaScript file that tells Cumcord what to do when it is loaded and unloaded.
This guide will explain technical details of what is involved in plugin creation.
To get started, you'll need sperm
, Cumcord's plugin build tool.
sperm
can be installed from npm.
Creating a new plugin is as simple as making a new directory and running sperm init
to initialize your plugin.
sperm
should've asked you for a main file path, so if you haven't already, create a JS (or JSX) file at the path you put in.
Here's an empty example of possible plugin exports:
// functional style
export default (data) => ({
onLoad() {},
onUnload() {},
})
// object style
export default {
onLoad() {},
onUnload() {}
}
// export style
export function onLoad() {}
export function onUnload() {}
Plugins consist of an onLoad
and an onUnload
function.
The onLoad
function is optional, as you can just load your plugin at the top level.
You may either export an object with these on, export them separately, or export a function returning them.
If a function, it will be passed cumcord.pluginData
.
You can import plugin-specific APIs from cumcord.pluginData
/ @cumcord/pluginData
.
This includes:
persist
: a nest to be used for storing dataid
: The current id of the plugin. Deprecated and kept for back-compat.manifest
: Your plugin manifest.showSettings()
: opens your own settings modal (see Plugin settings below)
Cumcord uses Nests to provide your plugin with a persist
Nest that persists data between sessions.
The simplest way of using persistent data is to use the persist.store
properties of the data
argument.
import data from "@cumcord/pluginData";
export default {
onLoad() {
const store = data.persist.store;
// Refer to data.persist.store to store data persistently
store.my.data.goes.here = "This data will persist between reloads!";
},
onUnload() {}
}
data.persist
is full Nest object, so I suggest you read the Nests documentation to learn more about how to use it.
The maximum size of your persistent data is the size of the user's disk.
Plugins need to be built into single-file bundles before they can be used.
sperm build
will automatically do this for you and create a dist/
directory with the built plugin.
Do not edit the built plugin, as it will be overwritten.
Instead, make changes to the plugin's source files and run sperm build
again.
Testing a plugin locally can be done with sperm dev
on Discord desktop.
This will connect to Cumcord's development websocket and reload your plugin when you make changes.
Please note that you must enable developer mode with cumcord.dev.toggleDevMode()
before you can use sperm dev
.
If you want to access your plugin's settings modal while developing, you can use cumcord.dev.showSettings()
.
You can access your plugin's persist nest from cumcord.dev.storage
.
sperm dev
does not automatically rebuild your plugin dist/
on disk,
so you'll need to run sperm build
again to publish once you're done developing.
Distributing plugins is as simple as statically hosting your plugin's dist/
directory and publishing a link to it.
Updating plugins is as simple as building a new version and hosting the new dist/
directory at the same URL.
Static hosting platforms I recommend:
- GitHub Pages
- Netlify
- Vercel (The site this guide is hosted on!)
If you'd like to get your plugin on the Cumdump you can join the Cumcord Discord, and ask a Cumdump manager to put it there.
Cumcord plugins can export a React component to be used as a settings panel.
export default {
onLoad(){},
onUnload(){},
settings: () => (
<div>
<Switch />
<Input />
<Button />
</div>
),
};
Cumcord plugins can import Cumcord APIs through the import
keyword with the alias @cumcord
.
For example, the following plugin imports Cumcord's logger and prints a message to the console:
import { log } from '@cumcord/utils/logger';
export default {
onLoad() {
log("I've been loaded!");
},
onUnload() {
log("I've been unloaded!");
}
};
These are always equal to the same apis on the cumcord global
import { findByProps } from "@cumcord/modules/webpack";
import patcher from "@cumcord/patcher";
findByProps === cumcord.modules.webpack.findByProps;
patcher === cumcord.patcher;
The only exception to this is plugin-specific APIs (see Creating a plugin):
import pluginData from "@cumcord/pluginData";
cumcord.pluginData === pluginData;
window.cumcord.pluginData === undefined;
Imports like this are heavily preferred over accessing the cumcord global.
Cumcord plugins can import files statically for use by appending :static
to a file's path.
import fileContent from "./file.txt:static";
Cumcord provides an API for patching things at cumcord.patcher
.
All of the patcher functions are powerful and will likely be used frequently in your plugins.
cumcord.patcher.injectCSS
injects CSS styles to the DOM and returns a function for modifying them.
Calling this function with a string will replace the current CSS styles with the new styles,
and calling it with nothing will remove the current styles.
// Add styles
const modifyCSS = cumcord.patcher.injectCSS(`
.vizality {
display: none;
}
`);
// modify styles to something different
// replaces old styles
modifyCSS(`
.vizality > * {
color: red;
background-color: red;
}
`);
// Remove styles
modifyCSS();
Importing a CSS, SASS, or SCSS file will return a function that injects the css in that file.
import cssInject from "./styles.css";
// Inject styles
const uninjectCss = cssInject();
// Remove styles
uninjectCss();
cumcord.patcher.before
injects into a function before it runs, and allows modifying the args.
function exampleFunction(arg1, arg2) { console.log(arg2) };
// Currently, this function will log "there" to the console. Let's patch it so it also logs "hi"!
exampleFunction("hi", "there");
const unpatch = cumcord.patcher.before(
// name of function to patch
"exampleFunction",
// object containing patch. `function` decls apply to window.
window,
// patch function. if returns Array, that becomes the new args to the function.
// if returns anything else, the original args are used.
(args) => [args[0], `${args[0]} ${args[1]}`]
);
/*
This function will now log the following:
hi
there
*/
exampleFunction("hi", "there");
// This removes the patch.
unpatch();
// This now logs "there" again.
exampleFunction("hi", "there");
cumcord.patcher.after
injects into a function after it has returned and allows modifying the return value.
If you are patching an async function, be careful to await the returned promise and use an async patch func.
function exampleFunction() { return "hi" };
// Currently, this logs "hi" to the console. Let's patch it so it logs "hi there :)" instead.
console.log(exampleFunction());
const unpatch = cumcord.patcher.after(
// name of func to patch
"exampleFunction",
// object containing func
window,
// patch function. If returns undefined, return value of patched is unmodified, else uses returned value.
(args, returnValue) => `${returnValue} there :)`
);
// This now logs "hi there :)"
console.log(exampleFunction());
// This removes the patch.
unpatch();
// This now logs "hi" again.
console.log(exampleFunction());
Returning anything in your callback function will override the return value of the patched function.
cumcord.patcher.instead
replaces a function with a new function, passing through args and overriding the return value.
function exampleFunction() { console.log("hello"); };
// Currently, this function logs "hello" to the console. Let's patch it so it logs "goodbye" instead.
exampleFunction();
const unpatch = cumcord.patcher.instead(
// name of func to patch
"exampleFunction",
// object containing func
window,
// patch func. Entirely replaces original function, but has access to call it etc.
(args, originalFunc) => console.log("goodbye")
);
// This now logs "goodbye".
exampleFunction();
// This removes the patch.
unpatch();
// This now logs "hello" again.
exampleFunction();
cumcord.patcher.findAndPatch
solves the problem of lazy-loaded components.
This provides an easy method to patch a component as soon as it is available:
// traditional way
const MessageContextMenu = cumcord.webpack.findByDisplayName("MessageContextMenu", false);
const unpatch1 = cumcord.patcher.after("default", MessageContextMenu, () => {});
// ERROR!!!! - MessageContextMenu may be `undefined`
// new way
const unpatch2 = cumcord.patcher.findAndPatch(
() => cumcord.webpack.findByDisplayName("MessageContextMenu", false),
(MessageContextMenu) => cumcord.patcher.after("default", MessageContextMenu, () => {})
); // works!
Cumcord provides multiple APIs for managing UI elements, but under the hood they all use React.
Cumcord plugins can use .jsx
files to create React components.
JSX files get the React
object in them automatically for free,
and sperm will replace imports from "react"
to make npm packages work,
but it is preferred to explicitly import from cumcord if you need react apis, as follows:
import { React } from "@cumcord/modules/common";
cumcord.ui.toasts.showToast
shows a toast message, removes it after a set period of time, and returns a function that removes it immediately.
The function takes an object with the following properties:
content
: The content of the toast. This can be a React component.title
: The title of the toast. This can also be a React component.duration
: The duration to show the toast in milliseconds.class
: An html class to use for the toast.onClick
: A function to call when the toast is clicked.
// The duration is in milliseconds to allow fine control of your toasts.
cumcord.ui.toasts.showToast({
title: "Hello!",
content: "Hello there!",
duration: 3000
});
cumcord.ui.modals.showConfirmationModal
shows a confirmation modal, and returns a promise that resolves to a boolean.
This function takes an object and a callback function. The object contains the following properties:
header
: A header for the modal.content
: A description for the modal.confirmText
: The text for the confirm button.cancelText
: The text for the cancel button.type
: The type for the confirm button. Can bedanger
,confirm
, orneutral
.
let confirmed = await cumcord.ui.modals.showConfirmationModal({
header: "Are you sure?",
content: "This will delete your account.",
confirmText: "Delete",
type: "danger"
});
Sometimes, you'll want to add a command to Discord. Cumcord provides a command API for this.
cumcord.commands.addCommand
adds a client-side commmand to Discord and returns a function that removes the command.
const removeCommand = cumcord.commands.addCommand({
name: "example",
description: "An example command.",
args: [
{
name: "myEpicArg",
description: "An example argument.",
// This can be string, bool, user, channel, or role. If you don't specify, it defaults to string.
type: "string",
// You can specify whether an arg is required or not. If you don't specify, it defaults to true.
required: false
}
],
// This is the function that gets called when the command is run. It can be async.
handler: (ctx, send) => {
// ctx is an object that contains 3 properties, args, guild, and channel.
// The value of the first argument. If the arg is not provided, it will be undefined.
ctx.args.myEpicArg;
// The guild the command was run in. This is a standard Discord guild object, and you can find out more about it by console.logging it.
ctx.guild;
// The channel the command was run in. This is a standard Discord channel object, and you can find out more about it by console.logging it.
ctx.channel;
// This sends a client-side (fake) message to the channel the command was run in.
send("Hello world!")
// Returning a string will send it as a (real) message to the channel the command was run in.
// Returning nothing will not send anything.
return "Hello world!"
}
})
removeCommand() // This removes the command.
Usually, finding internal Discord functions is fairly difficult, given that Discord's internal modules are not documented.
Cumcord provides an API to help get Discord's internal modules.
cumcord.modules.webpack
provides functions for searching through Discord's internal modules.cumcord.modules.common
provides a list of common Discord modules such as React, ReactDOM, etc.cumcord.modules.internal
provides Cumcord's npm dependencies such asidbKeyval
andnests
.
Typically, you'll use cumcord.modules.webpack.findByProps
to find basic Discord internal functions such as getUser()
, and cumcord.modules.webpack.findByDisplayName
to find React components such as Markdown
.
Internal functions typically use camelCase (likeThis), and React components typically use PascalCase (LikeThis).
cumcord.modules.webpack.findByProps
finds a module based on the props exported on it.
For example, to find the component with the getGuild
function:
const { getGuild } = cumcord.modules.webpack.findByProps("getGuild")
getGuild("824921608560181258") // the Cumcord guild!
You can also pass multiple props that must exist: findByProps("getGuilds", "getGuildCount")
.
You can get all modules that match instead of the first one with findByPropsAll
.
cumcord.modules.webpack.findByDisplayName
finds a module based on the display name of its default export.
This is most useful for finding react components:
const FormText = cumcord.modules.webpack.findByDisplayName("FormText");
<FormText />
You can also pass false
as the second argument to get the parent for patching with
const FormText = cumcord.modules.webpack.findByDisplayName("FormText", false);
const unpatch = cumcord.patcher.after("default", FormText, () => {});
You can find all modules with that display name instead of only the first with findByDisplayNameAll
,
though that does not support finding the parent of the module for patching.
cumcord.modules.webpack.findByKeywordAll
can be helpful in finding modules that you don't know the exact props of.
It searches for any props in the module that case insensitively match a set of keywords. For example, for all modules mentioning the user, you can:
const matches = cumcord.modules.webpack.findByKeywordAll("user");
matches.length === 219 // as of the time of writing :)
findByKeywordAll is not efficient, do NOT use this in a finished plugin, its for development use
cumcord.modules.webpack.findByStrings
finds a module based on strings it may contain.
This function is VERY inefficient but almost guaranteed to find a match of your string.
It should never be needed except for development, and where it may be needed (notable example: zustand), you should instead write a much more performant custom find filter
Custom find filters will be explained more in the webpack page.
THIS IS ACTUALLY REALLY SLOW, NEVER EVER EVER USE THIS IN A PLUGIN