Skip to main content

Discord Moderation Bot Module

A Discord-only bot module that provides /warn and /mute slash commands, keyword-based auto-moderation, and a member join welcome message. Demonstrates Discord-specific features including discord_options for slash command parameters.

Project structure

discord-moderation/
├── lumio.config.json
├── server/
│ ├── schema.ts
│ └── functions.ts
├── package.json
└── tsconfig.json

lumio.config.json

{
"$schema": "https://lumio.vision/schemas/lumio.config.schema.json",
"extension_id": "ext_placeholder",
"name": "Discord Moderation Bot",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["discord"],
"server": true,
"permissions": ["chat:send", "chat:read", "chat:ban", "chat:delete"],
"triggers": {
"commands": [
{
"name": "warn",
"description": "Warn a user for rule violation",
"cooldown_global": 0,
"min_role": "moderator",
"discord_options": [
{
"name": "user",
"type": "user",
"description": "User to warn",
"required": true
},
{
"name": "reason",
"type": "string",
"description": "Reason for the warning",
"required": true
}
]
},
{
"name": "mute",
"description": "Temporarily mute a user",
"cooldown_global": 0,
"min_role": "moderator",
"discord_options": [
{
"name": "user",
"type": "user",
"description": "User to mute",
"required": true
},
{
"name": "duration",
"type": "integer",
"description": "Mute duration in minutes",
"required": true
},
{
"name": "reason",
"type": "string",
"description": "Reason for the mute",
"required": false
}
]
},
{
"name": "infractions",
"description": "View a user's infraction history",
"cooldown_global": 5,
"min_role": "moderator",
"discord_options": [
{
"name": "user",
"type": "user",
"description": "User to check",
"required": true
}
]
}
],
"keywords": ["discord.gg/", "discordapp.com/invite/"],
"events": ["discord:member_join"]
},
"config_schema": [
{
"key": "maxWarnings",
"type": "number",
"label": "Warnings before auto-mute",
"default_value": 3,
"required": true
},
{
"key": "autoMuteDuration",
"type": "number",
"label": "Auto-mute duration (minutes)",
"default_value": 30,
"required": true
},
{
"key": "welcomeMessage",
"type": "string",
"label": "Welcome message for new members",
"default_value": "Welcome to the server, {name}! Please read the rules.",
"required": true
},
{
"key": "blockInviteLinks",
"type": "boolean",
"label": "Auto-delete Discord invite links from non-mods",
"default_value": true,
"required": true
}
]
}

server/schema.ts

import { defineSchema, defineTable, column } from "@zaflun/lumio-sdk/server";

export default defineSchema({
infractions: defineTable({
user_id: column.string(),
user_name: column.string(),
type: column.string(),
reason: column.string(),
issued_by: column.string(),
issued_by_name: column.string(),
timestamp: column.number(),
}),
});

server/functions.ts

import { command, keyword, event } from "@zaflun/lumio-sdk/server";

// /warn — Issue a warning to a user (Discord slash command)
// args[0] = user ID, args[1] = reason
export const warn = command("warn", async (ctx, args) => {
const targetUserId = args[0];
const reason = args[1] ?? "No reason provided";

if (!targetUserId) {
return { reply: "Usage: /warn <user> <reason>" };
}

// Get current warning count from cache
const warningKey = `warnings:${targetUserId}`;
const warningCount = await ctx.cache.increment(warningKey, 1);

// Set 30-day TTL on first warning
if (warningCount === 1) {
await ctx.cache.set(warningKey, 1, 2592000); // 30 days
}

// Log infraction in background
ctx.defer(async () => {
await ctx.db.insert("infractions", {
user_id: targetUserId,
user_name: targetUserId, // Discord resolves the display name
type: "warning",
reason,
issued_by: ctx.user.id,
issued_by_name: ctx.user.displayName,
timestamp: Date.now(),
});
});

const maxWarnings = (ctx.config.maxWarnings as number) ?? 3;

if (warningCount >= maxWarnings) {
// Auto-mute after reaching threshold
const muteDuration = (ctx.config.autoMuteDuration as number) ?? 30;

return {
reply: `User <@${targetUserId}> has been warned (${warningCount}/${maxWarnings}). Auto-mute applied for ${muteDuration} minutes. Reason: ${reason}`,
actions: [
{
action: "chat:ban",
params: {
user_id: targetUserId,
type: "timeout",
duration: muteDuration * 60,
reason: `Auto-mute: ${warningCount} warnings reached. Last: ${reason}`,
},
},
],
};
}

return {
reply: `Warning ${warningCount}/${maxWarnings} issued to <@${targetUserId}>. Reason: ${reason}`,
};
});

// /mute — Temporarily mute a user (Discord slash command)
// args[0] = user ID, args[1] = duration (minutes), args[2] = reason
export const mute = command("mute", async (ctx, args) => {
const targetUserId = args[0];
const duration = parseInt(args[1] ?? "10", 10);
const reason = args[2] ?? "No reason provided";

if (!targetUserId) {
return { reply: "Usage: /mute <user> <duration> [reason]" };
}

// Log in background
ctx.defer(async () => {
await ctx.db.insert("infractions", {
user_id: targetUserId,
user_name: targetUserId,
type: "mute",
reason,
issued_by: ctx.user.id,
issued_by_name: ctx.user.displayName,
timestamp: Date.now(),
});
});

return {
reply: `<@${targetUserId}> has been muted for ${duration} minute(s). Reason: ${reason}`,
actions: [
{
action: "chat:ban",
params: {
user_id: targetUserId,
type: "timeout",
duration: duration * 60,
reason,
},
},
],
};
});

// /infractions — View a user's infraction history
// args[0] = user ID
export const infractions = command("infractions", async (ctx, args) => {
const targetUserId = args[0];

if (!targetUserId) {
return { reply: "Usage: /infractions <user>" };
}

const allInfractions = await ctx.db.list("infractions");
const userInfractions = allInfractions
.filter((i) => i.user_id === targetUserId)
.sort((a, b) => (b.timestamp as number) - (a.timestamp as number))
.slice(0, 10);

if (userInfractions.length === 0) {
return { reply: `<@${targetUserId}> has no infractions.` };
}

const summary = userInfractions
.map((i) => {
const date = new Date(i.timestamp as number).toLocaleDateString();
return `${i.type}: ${i.reason} (${date})`;
})
.join(" | ");

return {
reply: `<@${targetUserId}> infractions (${userInfractions.length}): ${summary}`,
};
});

// Keyword handler — detect Discord invite links
export const inviteLinkFilter = keyword("discord.gg/", async (ctx, message) => {
const blockInvites = (ctx.config.blockInviteLinks as boolean) ?? true;

if (!blockInvites) {
return null;
}

// Moderators are exempt
if (ctx.user.isMod) {
return null;
}

// Log the attempted invite
ctx.defer(async () => {
await ctx.db.insert("infractions", {
user_id: ctx.user.id,
user_name: ctx.user.displayName,
type: "auto_delete",
reason: "Discord invite link posted",
issued_by: "system",
issued_by_name: "Auto-mod",
timestamp: Date.now(),
});
});

return {
reply: `${ctx.user.name}, invite links are not allowed in this server.`,
actions: [
{
action: "chat:delete",
params: { message_id: message.platformMessageId },
},
],
};
});

// Event handler — welcome new Discord server members
export const memberJoinWelcome = event("discord:member_join", async (ctx, evt) => {
const userName = evt.user_name as string;
const template = (ctx.config.welcomeMessage as string) ?? "Welcome, {name}!";
const message = template.replace("{name}", userName);

return { reply: message };
});

Key concepts demonstrated

  • Discord slash commandsdiscord_options defines typed parameters for /warn, /mute, and /infractions
  • Discord-only moduleplatforms: ["discord"] restricts this to Discord servers
  • Auto-escalation — warnings automatically escalate to mutes after a configurable threshold
  • Keyword auto-moderation — invite links are detected and auto-deleted for non-moderators
  • Role-based exemptionsctx.user.isMod checks let moderators bypass auto-mod rules
  • Action responses — handlers return actions array to trigger moderation actions (chat:ban, chat:delete)
  • Enhanced review permissionschat:ban and chat:delete require enhanced manual review during store approval