=== AI Comment Moderation ===
Contributors: spwhite
Tags: comments, comment moderation, spam, artificial intelligence, moderation queue
Requires at least: 7.0
Tested up to: 7.1
Requires PHP: 8.0
Stable tag: 2.11.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Classify and moderate comments with your choice of AI provider, with reason codes, a manual review queue, and batch processing.

== Description ==

AI Comment Moderation sends new comments to the AI provider you choose for a spam, toxicity and quality classification, then approves, spams or trashes them according to your prompt's configured actions. A verdict the AI isn't confident about, an explicit HOLD or a confidence below your floors, goes to a manual Review Queue instead of being acted on.

The provider is set once on the shared **AI Connection** screen (404ops > AI Connection), which every 404ops plugin on the site uses: Ollama (a model server you run yourself), OpenAI, Claude (Anthropic), Google Gemini, OpenRouter, or any OpenAI-compatible URL you supply. This plugin makes no external AI request until you have connected a provider and enabled the plugin there. See **External services** for exactly what is sent, and where.

Features:

* **Reason codes and a confidence score** on every verdict, custom prompts, and a per-outcome action (approve / spam / trash).
* **Local-first**: WordPress's own Disallowed and Moderation Comment Keys are applied before any AI call, and a local filter sends obvious bot spam straight to Spam without one.
* **Trusted Commenter Threshold**: commenters with a good record skip the AI call, except on comments containing a link and a small random sample (set it to 0 to check every comment).
* **Testing Mode**: record every verdict without changing any comment, to watch real decisions before you trust them.
* **Batch processing** of existing comments, and optional auto-processing of new ones.
* **Protected surfaces**, each with its own toggle: the registration form, repeated login failures, and form submissions.
* **Self-tuning** from the corrections your moderators make: calibration examples you can switch off, and confidence-floor recommendations you apply with one click.
* An **AI Verdict** column and a Suggest Reply action on WordPress's Comments screen.
* Analytics with CSV export, reputation tracking, log retention windows, and optional webhook notifications.

**The Pro add-on is a separate, paid plugin. It is not included in this one, and this plugin is complete without it.** It adds remote site sync, reputation analytics, a weekly digest email, model routing and ensemble voting; each is described under the FAQ.

== External services ==

This plugin contacts an outside service only for features you turn on and configure yourself. Nothing is sent anywhere on activation.

**AI moderation provider.** Only if you deliberately connect OpenAI, Claude (Anthropic), Google Gemini, OpenRouter, or OpenAI-Compatible (custom URL) on the AI Connection screen (404ops > AI Connection) *and* tick AI Comment Moderation there &#8211; one provider serves the whole site, and nothing is sent anywhere until both steps are done. When one of those is connected, each comment sent for moderation transmits the comment content, the commenter's display name and email address, and the related post title to that provider so it can return a classification. The same provider, model and prompt also classify the protected surfaces (Settings > Protected Surfaces), and each surface sends the same kinds of fields a comment would: a registration attempt transmits the chosen username and email address; a repeated-failure login evaluation transmits the attempted username only (never a password); a form submission transmits the submitter's name, email address, and message. When prompt calibration is active (Settings > Self-tuning, once 5 corrections exist), each comment verdict prompt additionally carries up to five short excerpts of this site's previously corrected comments — plain text, email-shaped content redacted, no moderator identities (the corrections log stores none) — to the same provider. With the Pro add-on's model routing switched on, a verdict the first model is unsure about is sent a second time, with the same content, to a second model on the same provider and connection, and with ensemble voting on, each verdict is sent to up to five models on the same provider and connection until the outcome is settled. Specifically:

* **OpenAI** &#8211; sent to `api.openai.com`. [Terms of use](https://openai.com/policies/terms-of-use) &#8211; [Privacy policy](https://openai.com/policies/privacy-policy).
* **Claude (Anthropic)** &#8211; sent to `api.anthropic.com`. [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) &#8211; [Privacy policy](https://www.anthropic.com/legal/privacy).
* **Google Gemini** &#8211; sent to `generativelanguage.googleapis.com`. [Gemini API terms](https://ai.google.dev/gemini-api/terms) &#8211; [Privacy policy](https://policies.google.com/privacy).
* **OpenRouter** &#8211; sent to `openrouter.ai`, which then forwards it to whichever underlying model you selected from its catalog; that underlying model provider is a party to the request too. [Terms of Service](https://openrouter.ai/terms) &#8211; [Privacy policy](https://openrouter.ai/privacy).
* **OpenAI-Compatible (custom URL)** &#8211; sent to whatever base URL you enter on the AI Connection screen. This option exists so you can point the plugin at infrastructure you choose &#8211; a self-hosted runtime, a corporate gateway, or a third-party hosted API &#8211; so there is no single provider to link to here; if you point it at a hosted third-party API, that service's own terms and privacy policy govern the data you send it, and you are responsible for reviewing them before you configure it.

If you connect Ollama instead and point it at a self-hosted server, that data stays on infrastructure you control and nothing leaves it through this plugin.

**Webhook notifications.** Nothing, until you set a webhook URL under Settings. Once set, the plugin posts a JSON payload (event type, timestamp, your site's URL and name, and the relevant comment data) to that URL when a comment gets a toxic or a spam verdict (you choose which of the two under Settings). The service behind that URL is yours to choose; its own terms and privacy policy apply.

**Remote Site Sync (Pro add-on only).** This plugin never contacts another WordPress site. With the separate Pro add-on installed and licensed and a remote site added on its Remote Sites screen, the add-on connects to that other WordPress site's REST API with the application password you provide, to fetch its comments and send your moderation decisions back. It only contacts sites you add, which are sites you administer.

== Installation ==

1. Upload the plugin through `Plugins > Add New > Upload Plugin`, or copy it to `wp-content/plugins/`.
2. Activate AI Comment Moderation through the `Plugins` screen.
3. Go to 404ops > AI Comment Moderation in the WordPress admin sidebar. That opens the plugin's Dashboard; every other screen is reached from the navigation row at the top of the page, not from the sidebar.
4. Open the AI Connection screen (404ops > AI Connection), choose a provider, enter its URL or API key and model, and tick AI Comment Moderation under "Plugins using this connection". Settings, in the navigation row, shows whether the plugin is connected and links there.
5. Create or edit prompts on the Prompts screen, then process comments from the Dashboard or the Batch Process screen.

== Frequently Asked Questions ==

= Does this plugin send comment data to an external service? =

Only to the AI provider you connect on the AI Connection screen, and only once you have enabled this plugin on it. See **External services** above for exactly what is sent, where, and each provider's terms and privacy policy.

= What does the Pro add-on add? =

The Pro add-on is a separate, paid plugin that is installed alongside this one; it is not included in this plugin or available through it. It adds:

* **Remote site sync**: pull comments in from other WordPress sites you administer (connected through their REST API and an application password you supply), moderate them from one dashboard, and send your decisions back.
* **Reputation analytics**: a dashboard over the reputation scores this plugin already tracks, with totals, averages, and most-trusted and lowest-scoring commenter lists.
* **Weekly digest email**: a summary of the week's comments processed and decisions taken, how many comments await review, and a snapshot of your commenters' reputation, with a button to send it right now and a history of every digest and budget alert this site has tried to send.
* **Model routing**: a cheaper model decides each verdict first, and a stronger model is asked only when the first is less confident than a threshold you set, or its answer cannot be read. Both run on your AI Connection.
* **Ensemble voting**: as an alternative to routing, two to five models you choose vote on each verdict, by majority or unanimously. A split vote goes to the Review Queue with every model's vote shown.

= How do I enter a Pro licence key? =

Install and activate the Pro add-on alongside this plugin, then go to 404ops > AI Comment Moderation and open **Licence** from the navigation row at the top of the page.

= What is Remote Site Sync? =

A feature of the separate **Pro** add-on (its Remote Sites screen, available once the add-on is installed and licensed), not of this plugin, that connects to other WordPress sites you administer via their REST API, using a site URL, username, and application password you provide. It caches their comments locally so you can moderate them from this dashboard, then syncs decisions back. The application password is stored encrypted in this site's database, keyed to this site's secret salts &#8211; still treat this site's database and `wp-config.php` with the same care as the remote site's credentials.

= What does the webhook notification send, and where? =

Nothing, until you set a webhook URL under Settings. Once set, the plugin posts a JSON payload (event type, timestamp, your site's URL and name, and the relevant comment data) to that URL when a comment gets a toxic or a spam verdict (you choose which of the two under Settings).

= How long does the plugin keep logs, remote comments, and credentials? =

The inventory below names the plugin's own data stores. The `wp_` prefix is the normal WordPress database prefix and can differ on your site. This does not change WordPress core's own comment-retention policy.

* `ai_comment_reviews` stores each comment's AI/review outcome, confidence, reason, prompt reference, and manual-review status. It has no automatic expiry. Reset Processing Data deletes it.
* `ai_comment_prompts` stores custom prompt text, names, actions, category, and active state. It has no automatic expiry. Reset Processing Data preserves it.
* `ai_comment_logs` stores the moderation-processing record: comment and prompt references, the AI response or locally generated reason, chosen action, processing time, the surface the verdict came from (`comment`, `registration`, `login`, `form`, or a third-party surface label), and timestamp. Registration/login/form rows use comment reference `0`, because no comment exists for them. `ai_comment_moderation_log_retention_days` keeps rows forever by default; choose a 30-, 90-, or 365-day window on the Settings screen (or set it back to `0`) and a daily task permanently deletes older rows, in bounded batches. Reset Processing Data does not delete it.
* `ai_comment_reputation` stores commenter email address, username, reputation score, approval/spam counts, and activity timestamps. It has no automatic expiry. Reset Processing Data preserves it.
* `ai_background_jobs` stores background-job type, status, progress, job data, error information, and timestamps. It has no automatic expiry. Reset Processing Data preserves it.
* `ai_webhook_log` stores new webhook-delivery metadata: event type, response status, success flag, and timestamp. New rows do not retain the destination URL, outbound JSON payload, response body, or comment data. Older rows created before this change may still contain those legacy fields and remain until the retention task removes them. It has its own window, `ai_comment_moderation_webhook_log_retention_days`, which keeps rows forever by default; choose 30, 90, or 365 days (or `0`) on the Settings screen. Reset Processing Data does not delete it. The webhook request itself can still contain comment data described above when you configure a destination.
* `ai_remote_sites` is the Pro Remote Site Sync connection configuration: site name/URL, username, application password, sync counts/status, and timestamps. It has no automatic expiry. Reset Processing Data preserves it; deleting a remote site or a full-data uninstall removes it.
* `ai_remote_comments` is the Pro Remote Site Sync local cache. It can contain a remote comment's author, email address, content, post information, status, moderation state, and sync state. It has no automatic expiry: it is retained until you delete it, use Reset Processing Data, or perform a full-data uninstall. Reset Processing Data clears this cache.
* `ai_provider_usage` stores provider/model names, token and cost totals, processed-comment counts, and dates. It has no automatic expiry. Reset Processing Data deletes it.
* `ai_corrections` stores moderator overrides, including comment/review references, original/new action, reason, and timestamp. It has no automatic expiry. Reset Processing Data deletes it. The log-retention task never deletes it: corrections are the self-tuning audit trail and are deliberately retained.
* `ai_model_calls` stores one row per model call behind a verdict: provider and model names, the decision and confidence the model gave, token count, estimated cost, the comment id and why a second model was asked, never comment text. Only the Pro add-on's model routing and ensemble voting write it, and it deletes rows older than 90 days. Reset Processing Data deletes it.
* `ai_notifications` stores notification type, recipient (which can be personal data), subject, message, delivery status, `sent_at`, and `created_at`. It has no automatic expiry. Reset Processing Data deletes it.

The related settings are `ai_comment_moderation_log_retention_days` (moderation/processing logs) and `ai_comment_moderation_webhook_log_retention_days` (webhook delivery logs), both defaulting to `0` (keep forever), plus `ai_comment_moderation_keep_data_on_uninstall` (default `1`, enabled). The `ai_comment_moderation_log_retention_last_run` state option records when the daily cleanup last ran and how many rows it deleted per family; the Settings screen shows it as its "last cleanup" line. Webhook URL/events are options too, as is `ai_comment_moderation_budget_alert` (the monthly spend-alert threshold). The AI provider connection is NOT stored by this plugin: its provider, model, sealed API key and base URL live in the shared `ops404_ai_settings` option, which every 404ops plugin on the site reads, together with a per-plugin enabled flag filed under this plugin's own entry (`404ops-ai-comment-moderator`).

The AI provider API key (in `ops404_ai_settings`) and the `ai_remote_sites` application-password field are encrypted at rest (libsodium secretbox, authenticated, with a key derived from this site's secret salts), so a database backup or export alone does not reveal them. Anyone who holds both the database and the site's `wp-config.php` salts can still decrypt them, and if the salts are changed, stored keys can no longer be decrypted and must be entered again.

Current code does not write `ai_comment_moderation_*` WordPress comment meta. Reset Processing Data and a full-data uninstall nevertheless sweep any plugin-owned keys under that prefix. Plugin options (including the webhook configuration and each `ai_comment_moderation_last_sync_page_site_{id}` sync-progress option) are preserved by Reset Processing Data. A default keep-data uninstall preserves them except that it deletes the `ai_comment_moderation_keep_data_on_uninstall` flag itself; a full-data uninstall removes them. The shared `ops404_ai_settings` option belongs to every 404ops plugin, so a full-data uninstall removes only this plugin's own entry from it (its enabled flag) &#8211; never the site-wide connection, its key, or another plugin's entry. Transients, scheduled jobs, and the retention job's own last-cleanup state are ephemeral: uninstall clears them even when **Keep Data on Uninstall** is on.

By default, uninstall keeps the plugin's custom tables and options, but it deletes the **Keep Data on Uninstall** flag itself. To delete the plugin's tables, settings, remote-site configurations, remote-comment cache, and plugin-owned comment meta, uncheck **Keep Data on Uninstall** before uninstalling; that full-data cleanup is irreversible.

= Does this plugin respect my existing Disallowed Comment Keys / Comment Moderation Keys lists? =

Yes. Before any comment is sent to the AI, it's checked against both lists under Settings > Discussion, using the same matching WordPress core itself uses (case-insensitive, substring, checked against the author, email, URL, comment content, IP, and user agent). A match on Disallowed Comment Keys trashes the comment immediately, with no AI call made. A match on Comment Moderation Keys holds it for review, the same as core would. Either way the reason shown on the comment says plainly which list matched and which word &#8211; never attributed to the AI, since the AI was never consulted.

= What does self-tuning actually change, and what does it not? =

Two things read the corrections your moderators have already made. First, prompt calibration: once 5 corrections exist, each verdict prompt carries up to five of the most recent as short calibration examples (an excerpt of the comment, the verdict the AI gave, and the verdict the moderator substituted) — plain-text excerpts with email-shaped content redacted, and no moderator identities, because the corrections log stores none. You can switch this off under Settings > Self-tuning; below 5 corrections it stays off by itself and the prompts are sent unchanged. Second, a confidence-floor recommendation: the plugin looks at the last 30 applied verdicts of each kind and counts how many were overridden. When one side shows at least a 25% override rate over at least 10 verdicts, the Settings screen offers to raise that side's confidence floor by 10 points, with the arithmetic printed next to it. Applying it is one explicit click; the change is recorded on the Analytics screen's Self-tuning panel, and recommendations only ever propose raising a floor (routing more comments to human review) — never lowering one. The plugin never changes a floor by itself.

What it does not do: measure or claim accuracy. A correction is a recorded disagreement, not a proven error, and the Self-tuning panel shows the raw inputs — corrections count, examples in use, current floors, override counts, recorded changes — with nothing estimated.

= What exactly does the login surface do, and when? =

Nothing at all until one address has failed to log in 5 times within 15 minutes (the credential-stuffing shape). A single failed login &#8211; or a second, third, or fourth &#8211; triggers no AI call, no log entry, and no block; WordPress core's own behaviour is untouched. When the threshold is crossed, three things happen, in this order: the events are logged in the same processing log every other verdict uses (labelled with the `login` surface); the attempted username &#8211; and nothing else, never a password, which no code in this plugin handles, stores, or forwards &#8211; is classified once by the same provider and prompt that classify your comments; and only a SPAM verdict blocks that address from logging in for the next 15 minutes, with a message that says so and how long to wait. An ordinary-looking username means no block, whatever the failure count. A successful login immediately clears that address's failure history and any block. In Testing Mode the verdict is still computed and logged, but nothing is ever blocked. The threshold, window, and block duration can be changed with the `ai_comment_moderation_login_failure_threshold`, `ai_comment_moderation_login_failure_window`, and `ai_comment_moderation_login_block_seconds` filters.

= How do I classify submissions from my own form plugin (WPForms, Contact Form 7, ...)? =

Form plugins with their own storage and hooks cannot be auto-wired, so this plugin ships a documented filter seam that runs any submission through the same classifier your comments use. One line where your plugin handles a submission:

`$verdict = apply_filters( 'acm_classify_submission', null, array( 'surface' => 'my-form', 'surface_label' => 'Contact form submission', 'author_name' => $name, 'author_email' => $email, 'content' => $message ) );`

It returns `null` when the classifier did not run (surfaces off, or no provider configured), so treat that as "no opinion". Otherwise `$verdict['decision']` uses the same vocabulary as every other surface &#8211; `'spam'` means your plugin should reject or hold the submission &#8211; and `$verdict['testing_mode']` tells you when to record a verdict without acting on it, mirroring the built-in surfaces. Every verdict through the seam is written to the plugin's processing log under your own surface label.

= What happens if I uninstall the plugin? =

By default, "Keep Data on Uninstall" is enabled (Settings > Data Management), so all custom tables, prompts, remote site configurations, and history are preserved if you delete the plugin. The plugin still deletes the keep-data flag itself. Uncheck it beforehand if you want a full cleanup, including all reviews, prompts, and remote sites, removed on uninstall. The shared AI connection (provider and key) is never removed by uninstalling one plugin; only this plugin's own opt-in is.

== Credits ==

The admin screens use two typefaces bundled with the plugin and served from its own files, never from a remote host: Space Grotesk (Copyright 2020 The Space Grotesk Project Authors) and JetBrains Mono (Copyright 2020 The JetBrains Mono Project Authors), both licensed under the SIL Open Font License, Version 1.1 (https://openfontlicense.org). Each licence text ships beside its font in `vendor-prefixed/404ops/ui-kit/assets/fonts/`. The shared admin interface kit (`404ops/ui-kit`) and the AI client (`404ops/ai-client`, shipped under a plugin-specific namespace in `vendor-prefixed/`) are 404ops' own libraries, licensed GPL-2.0-or-later like this plugin. The plugin bundles no other third-party code.

== Screenshots ==

1. The AI Moderator dashboard and quick actions.
2. AI connection status and general settings.
3. The manual review queue when no comments need attention.
4. Prompt management with per-outcome actions.

== Changelog ==

= 2.11.4 =
* Changed (Pro): sending a moderation decision back to a remote site is now part of the Pro add-on only, and needs its licence to be active. This plugin no longer contains that code and never contacts another WordPress site.
* Maintenance: the readme now describes the Pro add-on as a separate plugin, lists fewer past releases, and corrects the webhook, weekly digest and font-licence wording.

= 2.11.3 =
* Fix (Pro): adding a remote site whose address is already in your list now says "This site is already added" instead of printing a database error, and so does changing a site to another site's address. A second sync of the same site running at the same moment no longer prints an error either.

= 2.11.2 =
* Security (Pro): Remote Sites no longer connects to an address only your own server can reach (this machine, a private network, a cloud metadata address). The connection test, the comment sync and sending a decision back now refuse such an address before any request is made, check every address a name resolves to, connect only to an address they checked, and never follow a redirect.
* Fix (Pro): deleting a remote site now returns you to the list with a confirmation. It used to delete the site and then show two PHP warnings.
* Fix (Pro): the Quick Add form on the Remote Sites screen now has a label on each field.

= 2.11.1 =
* Fix: updating the plugin now creates any database table a new version adds, on the next page load. Before, a table added by an update (the Pro model-routing record, added in 2.9.0) was created only after deactivating and reactivating the plugin, and the Pro Models screen could not read it until then.

= 2.11.0 =
* New (Pro): the Reputation screen can send the weekly digest right now, whether or not the weekly email is switched on, so you can check that mail from your site arrives, and it now lists the last 20 digests and budget alerts this site tried to send, with whether the mail system accepted each one.
* Fix (Pro): the weekly digest now covers exactly the last seven days by your site's own clock; on a site that is not on UTC it covered a little less or a little more.

= 2.10.0 =
* New (Pro): ensemble voting. As an alternative to model routing, two to five models you choose vote on each AI verdict, by majority (more than half of the listed models) or unanimously. When they agree the verdict goes ahead; when they split, or too few answer, the comment goes to the Review Queue with every model's vote shown beside it, whatever each model's confidence. A model that cannot be reached counts against the vote rather than being ignored, and asking stops as soon as the outcome is settled. The Models screen shows how often the models agreed, how many calls were made and what they cost, with the most recent votes.

= 2.9.0 =
* New (Pro): model routing. A cheaper model decides each AI verdict first, and a stronger model is asked only when the first one is less confident than a threshold you set, cannot be read, or fails. If the stronger model is unavailable the comment is left as WordPress had it rather than acted on a verdict you called too weak. A new Models screen sets the two models and the threshold and shows which model decided and why, with an estimate of what routing saved; the Review Queue notes the deciding model beside each held comment.
* Added: a data table that records one row per model call behind a routed verdict (model names, decision, confidence, token count and estimated cost, never comment text). Pro deletes rows older than 90 days.

= 2.8.1 =
* Maintenance: Bundled libraries updated; translatable strings now use this plugin's text domain.

= 2.8.0 =
* Changed: the plugin's settings, options and stored data now live under new names that follow the plugin's WordPress.org slug. Settings saved by earlier versions are not carried over, so after updating re-enter the AI key and review your settings.
* Maintenance: tested up to WordPress 7.1 and still requires WordPress 7.0 or newer.

= 2.7.1 =
* Maintenance: the plugin is tested up to WordPress 7.1 and still requires WordPress 7.0 or newer. No change to behaviour.

= 2.7.0 =
* Maintenance: the plugin now bundles its own prefixed copy of the 404ops UI kit, so its admin screens share one 404ops menu and stylesheet with the other 404ops plugins and each plugin can update its UI independently. The shared AI connection now uses the updated private AI client (0.4).

== Upgrade Notice ==

= 2.11.4 =
Pro: sending a decision back to a remote site now needs an active licence. The free plugin behaves as before. Safe to update.

= 2.11.3 =
Pro: adding a remote site twice now says so in plain words. Safe to update.

= 2.11.2 =
Pro: Remote Sites refuses addresses only your own server can reach, and deleting a site now confirms it. Safe to update.

= 2.11.1 =
Creates any missing database table when you update, so nothing needs to be deactivated and reactivated. Safe to update.

= 2.11.0 =
Pro: send the weekly digest on demand, see a history of the emails this plugin sends, and a fix to the digest's seven-day window. Safe to update.
