PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / comments-window / ai-moderation.php

ai-moderation.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.1, at includes/comments-window/ai-moderation.php

275 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Native Comments Window: AI moderation.
4 *
5 * Lives on its own toggle ("Use AI to score new comments", surfaced
6 * in OS Settings → Features for users with `manage_options`). Off by
7 * default; when on, every new comment posted to the site is queued
8 * for AI analysis through the AI Copilot's existing
9 * `desktop_mode_ai_analyze_comment` job. The structured verdict
10 * (`{ harmful, spam, topic, ai_summary }`) lands in comment meta via
11 * `openstation_ai_save_meta()`; the Comments window reads it back
12 * through the `openstation_comments_window_spam_score` filter to
13 * bump the per-row chip, and surfaces the prose summary in a new
14 * REST field so the bundle can render it on hover.
15 *
16 * The Comments window NEVER runs the AI itself — it routes everything
17 * through the AI Copilot pipeline, which routes generation through the
18 * WordPress AI Client — so a site's Settings → Connectors config is the
19 * single source of truth for provider credentials and selection.
20 *
21 * SECURITY POSTURE
22 * ================
23 *
24 * - The option is `manage_options`-gated for read AND write — the
25 * REST permission callback rejects anyone without the cap, and
26 * the OS Settings UI hides the toggle for non-admins.
27 * - The hook on `wp_insert_comment` is a no-op when:
28 * a) the option is off,
29 * b) no admin on the site has AI configured, OR
30 * c) the comment is a pingback / trackback (only `comment_type
31 * === 'comment'` is analyzed).
32 * This makes the toggle safe to enable on sites that don't have
33 * an AI provider — it just stays inert.
34 *
35 * @package OpenStation
36 */
37
38 defined( 'ABSPATH' ) || exit;
39
40 /**
41 * Site option storing the on/off state.
42 *
43 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
44 * persisted or externally-visible identifier, so renaming it would
45 * orphan data already written by live installs (or break a live
46 * URL). The mismatch between this constant's name and its value is
47 * deliberate — it is NOT a half-finished rename.
48 */
49 const OPENSTATION_COMMENTS_AI_OPTION = 'desktop_mode_comments_ai_moderation';
50
51 /**
52 * Returns whether AI moderation for new comments is currently enabled.
53 *
54 * @return bool
55 */
56 function openstation_comments_ai_is_enabled() {
57 $raw = get_option( OPENSTATION_COMMENTS_AI_OPTION, false );
58 /**
59 * Filter whether AI moderation is enabled for new comments.
60 *
61 * Site-wide; not per-user. Hooks here override the
62 * `desktop_mode_comments_ai_moderation` site option — useful for
63 * gating by environment (staging vs. production) or by feature
64 * flag.
65 *
66 * @param bool $enabled Current option value.
67 */
68 return (bool) apply_filters(
69 'openstation_comments_ai_is_enabled',
70 (bool) $raw
71 );
72 }
73
74 /**
75 * On every new or edited comment, queue an AI analysis job — but only when
76 * the site has the Comments AI toggle on AND a text-generation provider is
77 * configured in Settings → Connectors. Idempotent: the AI job pipeline
78 * dedupes on `comment_<id>` while a job is pending, so overlapping fires
79 * (e.g. `wp_insert_comment` + a quick `edit_comment`) don't double-spend
80 * tokens, while an edit after a prior verdict re-analyzes to keep the
81 * spam-confidence meta fresh.
82 *
83 * This is the sole scheduler for comment analysis: the assistant being
84 * enabled no longer triggers analysis on its own (comment scoring is an
85 * opt-in Comments-window feature, off by default).
86 *
87 * @param int $comment_id The comment id (from `wp_insert_comment` or `edit_comment`).
88 */
89 function openstation_comments_ai_on_new_comment( $comment_id ) {
90 $comment_id = (int) $comment_id;
91 if ( $comment_id <= 0 || ! openstation_comments_ai_is_enabled() ) {
92 return;
93 }
94
95 // No usable provider configured in Connectors — stay inert so the toggle
96 // is safe to leave on before a provider is set up.
97 if ( ! openstation_comments_ai_provider_configured() ) {
98 return;
99 }
100
101 $comment = get_comment( $comment_id );
102 if ( ! $comment instanceof WP_Comment ) {
103 return;
104 }
105 if ( '' !== $comment->comment_type && 'comment' !== $comment->comment_type ) {
106 return;
107 }
108
109 if ( ! function_exists( 'openstation_ai_schedule_job' ) ) {
110 return;
111 }
112
113 // Comment scoring is a site-wide moderation feature: it is gated ONLY by
114 // the "Score new comments with AI" toggle + a configured Connector, never
115 // by any user's per-user assistant toggle. The user id below is passed
116 // through purely for attribution/observability — the analysis job reads
117 // none of it (the provider comes from Connectors), so the commenter's own
118 // id (0 for anonymous) is fine.
119 $user_id = (int) $comment->user_id;
120
121 openstation_ai_schedule_job(
122 'desktop_mode_ai_analyze_comment',
123 array( $comment_id, $user_id ),
124 'comment_' . $comment_id
125 );
126 }
127 add_action( 'wp_insert_comment', 'openstation_comments_ai_on_new_comment', 25, 1 );
128 // Re-analyze on edit so the spam-confidence meta stays fresh under normal
129 // moderation flows (the verdict filter always trusts the latest analysis).
130 add_action( 'edit_comment', 'openstation_comments_ai_on_new_comment', 25, 1 );
131
132 /**
133 * Fold the AI verdict into the per-row spam-confidence score.
134 *
135 * If a comment has been analyzed and the verdict came back with
136 * `spam === true`, push the score firmly into the "high" tone
137 * (≥ 70). `harmful === true` adds 20 — harmful but on-topic comments
138 * deserve moderation attention even when they don't tip into spam.
139 *
140 * The `analyzed_at` field is intentionally ignored — we always trust
141 * the latest verdict the AI produced, even if it's a few days old.
142 * Re-analysis happens automatically on `edit_comment`, so the meta
143 * stays fresh under normal moderation flows.
144 *
145 * @param int $score Default heuristic score (0–100).
146 * @param WP_Comment $comment Comment object.
147 * @return int Adjusted score, clamped to 0–100.
148 */
149 function openstation_comments_ai_filter_spam_score( $score, $comment ) {
150 if ( ! $comment instanceof WP_Comment ) {
151 return $score;
152 }
153 if ( ! function_exists( 'openstation_ai_get_meta' ) ) {
154 return $score;
155 }
156 $meta = openstation_ai_get_meta( 'comment', (int) $comment->comment_ID );
157 if ( ! is_array( $meta ) ) {
158 return $score;
159 }
160 $score = (int) $score;
161 if ( ! empty( $meta['spam'] ) ) {
162 // Pin to high-tone territory. Heuristics may already have it
163 // at 80; we just guarantee a floor.
164 $score = max( $score, 75 );
165 }
166 if ( ! empty( $meta['harmful'] ) ) {
167 $score = min( 100, $score + 20 );
168 }
169 return $score;
170 }
171 add_filter(
172 'openstation_comments_window_spam_score',
173 'openstation_comments_ai_filter_spam_score',
174 10,
175 2
176 );
177
178 /**
179 * REST: GET / POST `desktop-mode/v1/comments/ai-settings`.
180 *
181 * Read returns `{ enabled: bool, providerConfigured: bool }`.
182 * Write accepts `{ enabled: bool }`. Both `manage_options`-gated.
183 *
184 * `providerConfigured` is a convenience the UI uses to disable the
185 * toggle with a "Configure an AI provider first" hint when no admin
186 * on the site has wired up an API key. It's a UX cue — the toggle
187 * stays writable even when it's `false` so an admin who's about to
188 * configure the provider can flip this on first.
189 */
190 function openstation_comments_ai_register_rest_route() {
191 register_rest_route(
192 'desktop-mode/v1',
193 '/comments/ai-settings',
194 array(
195 array(
196 'methods' => WP_REST_Server::READABLE,
197 'callback' => 'openstation_comments_ai_rest_get',
198 'permission_callback' => static function () {
199 return current_user_can( 'manage_options' );
200 },
201 ),
202 array(
203 'methods' => WP_REST_Server::CREATABLE,
204 'callback' => 'openstation_comments_ai_rest_post',
205 'permission_callback' => static function () {
206 return current_user_can( 'manage_options' );
207 },
208 'args' => array(
209 'enabled' => array(
210 'required' => true,
211 'type' => 'boolean',
212 ),
213 ),
214 ),
215 )
216 );
217 }
218 add_action( 'rest_api_init', 'openstation_comments_ai_register_rest_route' );
219
220 /**
221 * REST GET handler — returns the current state + provider hint.
222 *
223 * @return WP_REST_Response
224 */
225 function openstation_comments_ai_rest_get() {
226 return new WP_REST_Response(
227 array(
228 'enabled' => openstation_comments_ai_is_enabled(),
229 'providerConfigured' => openstation_comments_ai_provider_configured(),
230 ),
231 200
232 );
233 }
234
235 /**
236 * REST POST handler — updates the toggle.
237 *
238 * @param WP_REST_Request $request Request.
239 * @return WP_REST_Response
240 */
241 function openstation_comments_ai_rest_post( WP_REST_Request $request ) {
242 $enabled = (bool) $request['enabled'];
243 update_option( OPENSTATION_COMMENTS_AI_OPTION, $enabled, false );
244
245 /**
246 * Fires after the Comments AI moderation toggle is changed.
247 *
248 * @param bool $enabled New state.
249 */
250 do_action( 'openstation_comments_ai_toggled', $enabled );
251
252 return new WP_REST_Response(
253 array(
254 'enabled' => $enabled,
255 'providerConfigured' => openstation_comments_ai_provider_configured(),
256 ),
257 200
258 );
259 }
260
261 /**
262 * Whether a usable AI text-generation provider is configured in Connectors.
263 *
264 * Delegates to the AI Copilot's capability check
265 * ({@see openstation_ai_provider_configured()}), which inspects the WordPress
266 * AI Client's provider registry without making a network request. Returns
267 * `false` when the AI Copilot bundle isn't loaded.
268 *
269 * @return bool
270 */
271 function openstation_comments_ai_provider_configured() {
272 return function_exists( 'openstation_ai_provider_configured' )
273 && openstation_ai_provider_configured();
274 }
275