PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.7
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 / apps / comments / parts / ai-moderation.php

ai-moderation.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.7, at apps/comments/parts/ai-moderation.php

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