Hooks
Change how Medusa behaves from a module without touching the framework.
The framework never checks for a specific module. When a module needs to change core behavior, it registers a hook on this.medusa.hooks. Remove the module and its hooks go with it.
async onStartup() {
this.medusa.hooks.register("restrict", this.restrict);
}
async onShutdown() {
this.medusa.hooks.unregister("restrict", this.restrict);
}Register in onStartup() and unregister in onShutdown() with the same function, so turning the module off removes it.
Registry
| Method | What it does |
|---|---|
register(name, handle) | Adds a handle under name. |
unregister(name, handle) | Removes it. |
has(name) | Whether any handle is registered. |
get(name, ...args) | Calls each handle in order and returns the first result that is not undefined. |
value(name, ...args) | The same as get, without waiting. Handles must return plain values. |
run(name, ...args) | Calls every handle and ignores the results. |
restricted(action, context) | Asks the restrict hooks about action. Returns the reason as a string, or "" when allowed. |
assert(action, context) | Throws the reason when action is restricted. |
Hooks Medusa Calls
| Hook | Call | Arguments | Return |
|---|---|---|---|
session | get | (request) | A user for visitors with no session. Medusa marks it guest: true. |
permissions | get | (discordId) | Permission list for that user. Skips the server role lookup. |
signup | get | (discordId) | Runs when a new dashboard user signs in. |
servers | get | (discordId) | Server list shown in the server picker. |
scan | run | (value, label) | Gets every value saved from the dashboard or the API, such as config, phrases, presence and the bot username. |
restrict | restricted | (action, context) | A reason string to block the action, or undefined to allow it. |
mode | get | ({ session }) | { badge, hint, link, server }. Shows a badge next to the brand. badge and hint are lang keys, link and server are { label, href } or null. |
Restrictions
Each action below is passed to restricted() before it runs.
| Action | Covers |
|---|---|
action:<name> | A dashboard action, such as action:saveSettings. |
page:<module>:<name> | Hides a module page from the sidebar. |
login | The login page. |
logs | The Logs page and its API. |
owner | Owner-only settings. |
modules, tokens, export, settings | Locks those dashboard sections. |
write | Every dashboard write through the API proxy. Context: { session, discordId, guild }. |
automation | Saving automation flows. |
appeals.server | Choosing the appeals server. |
servers.primary | Changing the primary server. |
The shell receives restricted, the subset of logs, owner, modules, tokens, export and settings that is blocked, so the dashboard can hide those controls.
Demo Module
Every product ships its demo as the demo module on the demo/module branch. Customer releases leave it out. The demo build adds it and sets DEMO=true.
The demo module:
- Sets
hidden: truein its manifest so it never shows on the Modules page. - Registers its hooks in
onStartup()only whenprocess.env.DEMO === "true", and unregisters them inonShutdown(). - Lists blocked actions in
actionsand blocked restrictions indenied. Anything not listed is allowed. - Returns its
messages.lockedphrase as the reason, and keeps its badge text inresources/lang/<code>/common.json.
It also adds trial servers: mode.server links to /m/demo/trial, and mode.link points to the public bot invite.
restrict(action) {
if (this.denied.includes(action)) return this.medusa.modules.lang.phrase("demo", "messages", "locked");
if (action.startsWith("action:") && this.actions.includes(action.slice(7))) return this.medusa.modules.lang.phrase("demo", "messages", "locked");
return undefined;
}