Skip to main content

Chat Moderation Bot Module

A bot module that uses the moderate() handler to filter chat messages. Demonstrates a link blocklist, timeout escalation (warn, timeout, ban), and audit trail integration.

Project structure

chat-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": "Chat Moderation",
"category": "bot_module",
"version": "1.0.0",
"platforms": ["twitch", "youtube", "kick"],
"server": true,
"permissions": ["chat:send", "chat:read", "chat:ban", "chat:delete"],
"triggers": {
"commands": [
{
"name": "warnings",
"description": "Check how many warnings a user has",
"cooldown_global": 5,
"cooldown_user": 15,
"min_role": "moderator"
}
],
"keywords": ["follow me at", "buy followers"]
},
"config_schema": [
{
"key": "blockedDomains",
"type": "string",
"label": "Blocked domains (comma-separated)",
"default_value": "banned-site.com,scam-link.net",
"required": true
},
{
"key": "warningsBeforeTimeout",
"type": "number",
"label": "Warnings before timeout",
"default_value": 2,
"required": true
},
{
"key": "timeoutsBeforeBan",
"type": "number",
"label": "Timeouts before ban",
"default_value": 3,
"required": true
},
{
"key": "timeoutDuration",
"type": "number",
"label": "Timeout duration (seconds)",
"default_value": 300,
"required": true
}
]
}

server/schema.ts

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

export default defineSchema({
user_infractions: defineTable({
user_id: column.string(),
platform: column.string(),
warnings: column.number(),
timeouts: column.number(),
last_infraction: column.number(),
banned: column.boolean(),
}),
infraction_log: defineTable({
user_id: column.string(),
user_name: column.string(),
action: column.string(),
reason: column.string(),
timestamp: column.number(),
}),
});

server/functions.ts

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

// Moderation handler — runs on every message that passes built-in module checks
// Sync path: must respond within 500ms
export const linkFilter = moderate(async (ctx, message) => {
const blockedDomainsRaw = (ctx.config.blockedDomains as string) ?? "";
const blockedDomains = blockedDomainsRaw.split(",").map((d) => d.trim().toLowerCase());

const textLower = message.text.toLowerCase();

// Check for blocked domains
const matchedDomain = blockedDomains.find((domain) => textLower.includes(domain));

if (!matchedDomain) {
return { block: false };
}

// Look up user infraction history from cache
const cacheKey = `infractions:${ctx.user.id}`;
let warnings = ((await ctx.cache.get(`${cacheKey}:warnings`)) as number) ?? 0;
let timeouts = ((await ctx.cache.get(`${cacheKey}:timeouts`)) as number) ?? 0;

const warningsThreshold = (ctx.config.warningsBeforeTimeout as number) ?? 2;
const timeoutsThreshold = (ctx.config.timeoutsBeforeBan as number) ?? 3;
const timeoutDuration = (ctx.config.timeoutDuration as number) ?? 300;

// Escalation logic: warn → timeout → ban
if (warnings < warningsThreshold) {
// Warning phase: delete message, send warning
warnings++;
await ctx.cache.set(`${cacheKey}:warnings`, warnings, 86400);

ctx.defer(async () => {
await logInfraction(ctx, "warning", `Blocked domain: ${matchedDomain}`);
});

return {
block: true,
action: "delete",
reason: `Warning ${warnings}/${warningsThreshold}: blocked link (${matchedDomain})`,
};
}

if (timeouts < timeoutsThreshold) {
// Timeout phase
timeouts++;
await ctx.cache.set(`${cacheKey}:timeouts`, timeouts, 86400);

ctx.defer(async () => {
await logInfraction(ctx, "timeout", `Blocked domain: ${matchedDomain}`);
});

return {
block: true,
action: "timeout",
duration: timeoutDuration,
reason: `Timeout ${timeouts}/${timeoutsThreshold}: repeated blocked link (${matchedDomain})`,
};
}

// Ban phase
ctx.defer(async () => {
await logInfraction(ctx, "ban", `Blocked domain: ${matchedDomain}`);
});

return {
block: true,
action: "ban",
reason: `Banned: exceeded timeout limit with repeated blocked links`,
};
});

// Helper: log an infraction to the database
async function logInfraction(
ctx: any,
action: string,
reason: string
) {
// Update user record
const existing = await ctx.db.get("user_infractions", ctx.user.id);
if (existing) {
await ctx.db.patch("user_infractions", ctx.user.id, {
warnings: ((existing.warnings as number) ?? 0) + (action === "warning" ? 1 : 0),
timeouts: ((existing.timeouts as number) ?? 0) + (action === "timeout" ? 1 : 0),
banned: action === "ban",
last_infraction: Date.now(),
});
} else {
await ctx.db.insert("user_infractions", {
user_id: ctx.user.id,
platform: ctx.user.platform,
warnings: action === "warning" ? 1 : 0,
timeouts: action === "timeout" ? 1 : 0,
last_infraction: Date.now(),
banned: action === "ban",
});
}

// Log the infraction
await ctx.db.insert("infraction_log", {
user_id: ctx.user.id,
user_name: ctx.user.displayName,
action,
reason,
timestamp: Date.now(),
});
}

// Keyword handler — detect self-promotion spam
export const spamDetector = keyword("follow me at", async (ctx, message) => {
ctx.defer(async () => {
await logInfraction(ctx, "warning", "Self-promotion spam detected");
});
return null; // Moderation handled by moderate(), keyword just logs
});

// !warnings <username> — Check a user's infraction count (moderator only)
export const warnings = command("warnings", async (ctx, args) => {
const targetName = args[0];
if (!targetName) {
return { reply: "Usage: !warnings <username>" };
}

// Look up by username in the infraction log
const allInfractions = await ctx.db.list("user_infractions");
const infractions = await ctx.db.list("infraction_log");
const userInfractions = infractions.filter(
(i) => (i.user_name as string)?.toLowerCase() === targetName.toLowerCase()
);

if (userInfractions.length === 0) {
return { reply: `${targetName} has no infractions.` };
}

const warningCount = userInfractions.filter((i) => i.action === "warning").length;
const timeoutCount = userInfractions.filter((i) => i.action === "timeout").length;
const banCount = userInfractions.filter((i) => i.action === "ban").length;

return {
reply: `${targetName}: ${warningCount} warning(s), ${timeoutCount} timeout(s), ${banCount} ban(s)`,
};
});

Key concepts demonstrated

  • moderate() handler — sync execution path (500ms deadline), runs on every message after built-in modules pass
  • Escalation logic — warnings lead to timeouts, which lead to bans. Thresholds are configurable.
  • Cache-based infraction trackingctx.cache stores warning/timeout counts with 24-hour TTL for fast lookup during sync moderation
  • Background loggingctx.defer() persists infraction records to the database after the moderation decision is returned
  • Enhanced review permissionschat:ban and chat:delete trigger enhanced manual review during store approval
  • Config-driven thresholds — the streamer controls warning limits, timeout duration, and blocked domains through the dashboard settings panel