open-wa v5 is alpha. Use v4.76.0 for mature production systems unless you are validating v5.
The Client APIAPI ExplorerLicensing

Example: OpenAI moderation

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

Plugin Moderation Shield Wally

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:

  1. The plugin extracts the text content
  2. Sends it to OpenAI's Moderation API
  3. If flagged, the plugin can delete the message, warn the sender, and log the action
  4. 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;
}
Wally the Walrus typing

Was this helpful?

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

On this page