Example: OpenAI moderation
Build a plugin that scans incoming group messages, labels flagged content, and optionally deletes or reports it.

Example: OpenAI moderation
This example builds a plugin that scans incoming group messages using OpenAI's Moderation API and takes follow-up action on flagged content. Incoming WhatsApp messages have already arrived, so the plugin can detect, label, delete when allowed, warn, report, or stop your own downstream handling.
What the plugin does
When a message arrives in a moderated group:
- The plugin extracts the text content
- Sends it to OpenAI's Moderation API
- If flagged, the plugin can delete the message, warn the sender, and log the action
- If clean, your other handlers can continue processing it normally
Scan incoming messages
function getModeratableMessage(message: unknown) {
if (!message || typeof message !== 'object') return null;
const candidate = message as {
body?: unknown;
from?: unknown;
chatId?: unknown;
id?: unknown;
isGroupMsg?: unknown;
sender?: { id?: string };
};
if (candidate.isGroupMsg !== true) return null;
if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') return null;
if (typeof candidate.chatId !== 'string' || typeof candidate.id !== 'string') return null;
return {
body: candidate.body,
from: candidate.from,
chatId: candidate.chatId,
id: candidate.id,
sender: candidate.sender,
};
}
events.on('message.received', async ({ message }) => {
const msg = getModeratableMessage(message);
if (!msg) return;
const result = await checkModeration(msg.body, config.apiKey);
if (result.flagged) {
// Take action
}
});Call OpenAI moderation
type ModerationResult = {
flagged: boolean;
categories: Record<string, boolean>;
};
async function checkModeration(text: string, apiKey: string) {
const response = await fetch('https://api.openai.com/v1/moderations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input: text }),
});
if (!response.ok) {
throw new Error(`Moderation API error: ${response.status}`);
}
const data = await response.json() as { results?: ModerationResult[] };
const [result] = data.results ?? [];
if (!result) {
throw new Error('Moderation API response did not include a result');
}
return result;
}Prevent flagged outgoing messages
Use the message.send.before interceptor to prevent flagged messages from being sent:
'message.send.before': async ({ message }) => {
// This interceptor runs before a message is sent
// Return modified output or cancel the send
}Handle flagged incoming messages
For incoming messages, you cannot prevent delivery after receipt. You can delete the message when the connected account has permission, warn the sender, report to admins, and skip downstream business logic:
if (result.flagged) {
// Delete the message if the connected account is a group admin
await client.deleteMessage(msg.chatId, msg.id);
// Warn the sender
await client.reply(
msg.from,
'Your message was flagged for inappropriate content.',
msg.id
);
// Log the action
logger.warn('Message flagged and deleted', {
from: msg.from,
categories: result.categories,
});
}Group-only moderation
Detect group messages and skip direct messages:
events.on('message.received', async ({ message }) => {
const msg = getModeratableMessage(message);
if (!msg) return;
// Check if this group is in the moderation list
if (config.groups.length > 0 && !config.groups.includes(msg.from)) {
return;
}
// Moderate the message
});Exempt admins
const exemptAdmins = new Set(config.adminIds || []);
events.on('message.received', async ({ message }) => {
const msg = getModeratableMessage(message);
if (!msg) return;
if (exemptAdmins.has(msg.sender?.id)) {
return; // Admin messages are exempt
}
});Delete flagged messages
// Delete the message
if (msg.chatId && msg.id) {
await client.deleteMessage(msg.chatId, msg.id);
}Notes:
- The bot must be a group admin to delete messages
- There may be a time limit on how old a message can be deleted
- Incoming messages may have different deletion behavior than sent messages
Log actions
logger.warn('Moderation action taken', {
from: msg.from,
groupId: msg.chatId,
flagged: result.flagged,
categories: result.categories,
action: 'deleted',
});Configuration schema
const configSchema = z.object({
apiKey: z.string().min(1, 'OpenAI API key is required'),
groups: z.array(z.string()).default([]), // Empty = all groups
adminIds: z.array(z.string()).default([]), // Exempt admins
actions: z.object({
delete: z.boolean().default(true),
warn: z.boolean().default(true),
notify: z.boolean().default(false), // Notify admins when action taken
log: z.boolean().default(true),
}).default({}),
});Full code example
// moderation.ts
import { createPlugin } from '@open-wa/plugin-sdk';
import { z } from 'zod';
type ModerationResult = {
flagged: boolean;
categories: Record<string, boolean>;
};
type ModeratableMessage = {
body: string;
from: string;
chatId: string;
id: string;
sender?: { id?: string };
};
function getModeratableMessage(message: unknown): ModeratableMessage | null {
if (!message || typeof message !== 'object') return null;
const candidate = message as {
body?: unknown;
from?: unknown;
chatId?: unknown;
isGroupMsg?: unknown;
id?: unknown;
sender?: { id?: string };
};
if (candidate.isGroupMsg !== true) return null;
if (typeof candidate.body !== 'string' || candidate.body.length === 0) return null;
if (typeof candidate.from !== 'string') return null;
if (typeof candidate.chatId !== 'string' || typeof candidate.id !== 'string') return null;
return {
body: candidate.body,
from: candidate.from,
chatId: candidate.chatId,
id: candidate.id,
sender: candidate.sender,
};
}
function errorMessage(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
const configSchema = z.object({
apiKey: z.string().min(1, 'OpenAI API key is required'),
groups: z.array(z.string()).default([]),
adminIds: z.array(z.string()).default([]),
actions: z.object({
delete: z.boolean().default(true),
warn: z.boolean().default(true),
notify: z.boolean().default(false),
log: z.boolean().default(true),
}).default({}),
});
async function checkModeration(text: string, apiKey: string): Promise<ModerationResult> {
const response = await fetch('https://api.openai.com/v1/moderations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input: text }),
});
if (!response.ok) {
throw new Error(`Moderation API error: ${response.status}`);
}
const data = await response.json() as { results?: ModerationResult[] };
const [result] = data.results ?? [];
if (!result) {
throw new Error('Moderation API response did not include a result');
}
return result;
}
export default createPlugin({
meta: { name: 'openai-moderation' },
configSchema,
init: async ({ events, logger, config, client }) => {
logger.info('OpenAI Moderation loaded', {
groups: config.groups.length > 0 ? config.groups : 'all',
});
const exemptAdmins = new Set(config.adminIds);
events.on('message.received', async ({ message }) => {
const msg = getModeratableMessage(message);
if (!msg) return;
// Skip exempt admins
if (exemptAdmins.has(msg.sender?.id)) return;
// Check group filter
if (config.groups.length > 0 && !config.groups.includes(msg.from)) return;
try {
const result = await checkModeration(msg.body, config.apiKey);
if (result.flagged) {
if (config.actions.log) {
logger.warn('Message flagged', {
from: msg.from,
categories: result.categories,
});
}
if (config.actions.delete) {
try {
await client.deleteMessage(msg.chatId, msg.id);
logger.info('Message deleted');
} catch (error) {
logger.error('Failed to delete message', { error: errorMessage(error) });
}
}
if (config.actions.warn) {
await client.reply(
msg.from,
'Your message was flagged for inappropriate content.',
msg.id
);
}
}
} catch (error) {
logger.error('Moderation check failed', { error: errorMessage(error) });
// Continue processing. Do not treat API errors as moderation decisions.
}
});
},
});Load and test it
// wa.config.js
export default {
plugins: [
'./plugins/moderation',
],
pluginConfig: {
'openai-moderation': {
apiKey: process.env.OPENAI_API_KEY,
groups: ['123456789@g.us'], // Specific group IDs
adminIds: ['1234567890@c.us'], // Exempt admin IDs
actions: {
delete: true,
warn: true,
notify: false,
log: true,
},
},
},
};Performance considerations
Concurrent calls
The moderation API is called for every message. If your group has high volume:
import pQueue from 'p-queue';
const queue = new pQueue({ concurrency: 5 });
events.on('message.received', async ({ message }) => {
// ... validation ...
queue.add(async () => {
const result = await checkModeration(msg.body, config.apiKey);
// ... handle result ...
});
});Caching
Cache recent moderation results for identical messages:
const cache = new Map<string, { flagged: boolean; timestamp: number }>();
const CACHE_TTL = 60 * 1000; // 1 minute
function getCached(text: string) {
const entry = cache.get(text);
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
return entry;
}
return null;
}Related
- Plugin getting started, build your first plugin
- Plugin security model, security boundaries
- External API patterns, calling external services

Was this helpful?
Wally and his cute companion coffee mug are coding day and night to keep this up-to-date!
