Moderation

AI Moderation for Community and Forum Content

⏱ 15 min

A paid community is a different moderation problem than a blog’s comment section: higher volume, higher stakes for a wrongly-flagged member, and content that doesn’t always live in WordPress’s own wp_comments table. This lesson extends Course 6’s classify-and-recommend pattern so it works across whatever forum or community plugin a membership site actually runs, without ever auto-deleting a member’s post.

What you'll learn in this lesson
Why comment meta doesn't generalize to every community content type
Forum replies, activity updates, and topics don't all store like WordPress comments.
A content-type-agnostic moderation log table
One dbDelta()-created table that works whether the content came from bbPress, BuddyPress, or your own plugin.
Reusing Course 6's classification prompt for community content
The same fixed-format Classification:/Reason: pattern, tuned for harassment and off-topic spam in a paid space.
Wiring the classifier to real triggers without inventing plugin internals
A working example on WordPress comments, plus how to adapt it to a dedicated forum plugin's own hooks.
Prerequisites

Course 6’s Project: Content Moderation Tool already built and working. A community or forum feature active on your membership site, whichever plugin provides it.

Step 1: Why one moderation log needs to cover several content types

Course 6’s project logs a recommendation to comment meta, which works because the content being classified is always a WordPress comment. A membership site’s community content often isn’t: bbPress stores topics and replies as their own post types, BuddyPress activity updates aren’t posts at all, and a custom-built community feature might store replies in its own table entirely. Rather than write a different logging mechanism per plugin, this lesson uses one generic table keyed by a content type label and an ID, so the same classifier and the same review screen work regardless of where the content actually lives.

// File: community-moderator/class-moderation-log.php
function community_moderator_create_table() {
	global $wpdb;
	$table   = $wpdb->prefix . 'community_moderation_log';
	$charset = $wpdb->get_charset_collate();

	$sql = "CREATE TABLE {$table} (
		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
		content_type VARCHAR(32) NOT NULL,
		content_id BIGINT UNSIGNED NOT NULL,
		classification VARCHAR(32) NOT NULL,
		reason TEXT NOT NULL,
		status VARCHAR(20) NOT NULL DEFAULT 'pending_review',
		created_at DATETIME NOT NULL,
		PRIMARY KEY (id),
		KEY content_lookup (content_type, content_id)
	) {$charset};";

	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	dbDelta( $sql );
}

Step 2: Reuse Course 6’s classifier, tuned for community content

// File: community-moderator/class-moderation-log.php
function community_moderator_classify_content( string $content_type, int $content_id, string $author_name, string $content_text ) {
	$prompt = <<<PROMPT
Classify this community post into exactly one category: "acceptable", "likely_spam", or "policy_violation".
This is a paid membership community, policy violations include harassment, hate speech,
and threats. Spam includes promotional links, repeated gibberish, or content unrelated
to the community's topic. Respond in exactly this format:

Classification: <category>
Reason: <one sentence>

Author: {$author_name}
Content: {$content_text}
PROMPT;

	$response = wp_ai_client_prompt( $prompt )
		->using_model_preference( 'anthropic/claude', 'openai/gpt' )
		->generate_text();

	if ( empty( $response ) ) {
		return new WP_Error( 'empty_response', 'No classification returned, defaulting to manual review.' );
	}

	$classification = 'acceptable';
	$reason         = '';

	if ( preg_match( '/Classification:\s*(\w+)/i', $response, $m ) ) {
		$classification = strtolower( trim( $m[1] ) );
	}
	if ( preg_match( '/Reason:\s*(.+)/i', $response, $m ) ) {
		$reason = trim( $m[1] );
	}

	if ( ! in_array( $classification, array( 'acceptable', 'likely_spam', 'policy_violation' ), true ) ) {
		$classification = 'acceptable'; // Safest default, same discipline as Course 6.
	}

	global $wpdb;
	$wpdb->insert( $wpdb->prefix . 'community_moderation_log', array(
		'content_type'    => $content_type,
		'content_id'      => $content_id,
		'classification'  => $classification,
		'reason'          => $reason,
		'status'          => 'pending_review',
		'created_at'      => current_time( 'mysql' ),
	) );

	return array( 'classification' => $classification, 'reason' => $reason );
}

This is Course 6’s prompt structure with two changes: the framing explicitly names a “paid membership community” so the model weighs context accordingly, and the result writes to the content-type-agnostic log table from Step 1 instead of comment meta.

Step 3: Wire it to a real trigger

WordPress’s own comment_post action is the trigger Course 6 already uses, and it applies directly if your community feature stores its replies as ordinary comments on a custom post type, a common, real pattern for lightweight membership communities:

// File: community-moderator/class-moderation-log.php
add_action( 'comment_post', function ( $comment_id, $approved ) {
	$comment = get_comment( $comment_id );
	community_moderator_classify_content(
		'comment',
		$comment_id,
		$comment->comment_author,
		$comment->comment_content
	);
}, 10, 2 );
Dedicated forum plugins expose their own hooks, confirm them directly

If you’re running bbPress, BuddyPress, or another dedicated community plugin, that plugin fires its own action when a topic, reply, or activity update is created. This lesson doesn’t guess at those hook names or argument orders here, since they belong to that plugin, not WordPress core, and they vary by plugin and version. Check that plugin’s own hook reference, then call community_moderator_classify_content() from inside it with the appropriate content_type label (‘forum_reply’, ‘activity_update’, and so on) and the real content text that hook provides. The classifier and the log table in Steps 1 and 2 don’t change, only the trigger does.

Step 4: A moderator review screen queries one table, regardless of source

// File: community-moderator/class-moderation-log.php
function community_moderator_get_pending(): array {
	global $wpdb;
	return $wpdb->get_results(
		"SELECT * FROM {$wpdb->prefix}community_moderation_log WHERE status = 'pending_review' ORDER BY created_at DESC",
		ARRAY_A
	);
}

A moderator sees every flagged item across every content type in one list, and the actual moderation action, whatever “remove,” “warn,” or “dismiss” means for a given content type, is a separate, explicitly human-triggered function this lesson doesn’t call automatically, exactly Course 6’s discipline carried forward.

Test it

wp-env run cli wp comment create --comment_post_ID=1 --comment_content="Check out my crypto opportunity at totally-legit-crypto.example" --comment_author="Spammer"
wp-env run cli wp eval '
print_r( community_moderator_get_pending() );
'

Confirm the flagged comment appears with classification of likely_spam, status of pending_review, and that no comment status changed as a side effect of running the classifier.

Higher volume communities make silent auto-action more tempting, and more dangerous

A busy membership community generates far more flagged content per day than a typical blog’s comment section, which makes “just auto-remove the obvious ones” a tempting shortcut once a moderator’s queue starts backing up. Don’t. A wrongly-removed post in a paid community is a paying member’s contribution silently deleted, with no record they can point to and no easy way to know it happened. Scale the moderation queue and the people reviewing it, not the automation acting on it.

Recap

Community and forum content on a membership site rarely all lives in one storage shape, so this lesson logs moderation recommendations to a generic, content-type-agnostic table instead of comment meta alone. The classifier itself is Course 6’s pattern, framed for a paid community’s specific risks, and it’s wired to real WordPress comment hooks directly, with an explicit note to confirm any dedicated forum plugin’s own hook names rather than guessing at them. Every flagged item stays pending_review until a human acts on it, no matter which plugin the content came from. The final lesson recaps this whole course.

Resources & further reading

← Integrating Deeply With LearnDash's Content Structure Course Recap: An AI Layer for Learning and Membership Products →