PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.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 / includes / comments-window / ai-moderation.php

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

286 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — 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 * `desktop_mode_ai_save_meta()`; the Comments window reads it back
12 * through the `desktop_mode_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 WPDesktopMode
36 * @since 0.8.3
37 */
38
39 defined( 'ABSPATH' ) || exit;
40
41 /** Site option storing the on/off state. */
42 const DESKTOP_MODE_COMMENTS_AI_OPTION = 'desktop_mode_comments_ai_moderation';
43
44 /**
45 * Returns whether AI moderation for new comments is currently enabled.
46 *
47 * @since 0.8.3
48 *
49 * @return bool
50 */
51 function desktop_mode_comments_ai_is_enabled() {
52 $raw = get_option( DESKTOP_MODE_COMMENTS_AI_OPTION, false );
53 /**
54 * Filter whether AI moderation is enabled for new comments.
55 *
56 * Site-wide; not per-user. Hooks here override the
57 * `desktop_mode_comments_ai_moderation` site option — useful for
58 * gating by environment (staging vs. production) or by feature
59 * flag.
60 *
61 * @since 0.8.3
62 *
63 * @param bool $enabled Current option value.
64 */
65 return (bool) apply_filters(
66 'desktop_mode_comments_ai_is_enabled',
67 (bool) $raw
68 );
69 }
70
71 /**
72 * On every new or edited comment, queue an AI analysis job — but only when
73 * the site has the Comments AI toggle on AND a text-generation provider is
74 * configured in Settings → Connectors. Idempotent: the AI job pipeline
75 * dedupes on `comment_<id>` while a job is pending, so overlapping fires
76 * (e.g. `wp_insert_comment` + a quick `edit_comment`) don't double-spend
77 * tokens, while an edit after a prior verdict re-analyzes to keep the
78 * spam-confidence meta fresh.
79 *
80 * This is the sole scheduler for comment analysis: the assistant being
81 * enabled no longer triggers analysis on its own (comment scoring is an
82 * opt-in Comments-window feature, off by default).
83 *
84 * @since 0.8.3
85 *
86 * @param int $comment_id The comment id (from `wp_insert_comment` or `edit_comment`).
87 */
88 function desktop_mode_comments_ai_on_new_comment( $comment_id ) {
89 $comment_id = (int) $comment_id;
90 if ( $comment_id <= 0 || ! desktop_mode_comments_ai_is_enabled() ) {
91 return;
92 }
93
94 // No usable provider configured in Connectors — stay inert so the toggle
95 // is safe to leave on before a provider is set up.
96 if ( ! desktop_mode_comments_ai_provider_configured() ) {
97 return;
98 }
99
100 $comment = get_comment( $comment_id );
101 if ( ! $comment instanceof WP_Comment ) {
102 return;
103 }
104 if ( '' !== $comment->comment_type && 'comment' !== $comment->comment_type ) {
105 return;
106 }
107
108 if ( ! function_exists( 'desktop_mode_ai_schedule_job' ) ) {
109 return;
110 }
111
112 // Comment scoring is a site-wide moderation feature: it is gated ONLY by
113 // the "Score new comments with AI" toggle + a configured Connector, never
114 // by any user's per-user assistant toggle. The user id below is passed
115 // through purely for attribution/observability — the analysis job reads
116 // none of it (the provider comes from Connectors), so the commenter's own
117 // id (0 for anonymous) is fine.
118 $user_id = (int) $comment->user_id;
119
120 desktop_mode_ai_schedule_job(
121 'desktop_mode_ai_analyze_comment',
122 array( $comment_id, $user_id ),
123 'comment_' . $comment_id
124 );
125 }
126 add_action( 'wp_insert_comment', 'desktop_mode_comments_ai_on_new_comment', 25, 1 );
127 // Re-analyze on edit so the spam-confidence meta stays fresh under normal
128 // moderation flows (the verdict filter always trusts the latest analysis).
129 add_action( 'edit_comment', 'desktop_mode_comments_ai_on_new_comment', 25, 1 );
130
131 /**
132 * Fold the AI verdict into the per-row spam-confidence score.
133 *
134 * If a comment has been analyzed and the verdict came back with
135 * `spam === true`, push the score firmly into the "high" tone
136 * (≥ 70). `harmful === true` adds 20 — harmful but on-topic comments
137 * deserve moderation attention even when they don't tip into spam.
138 *
139 * The `analyzed_at` field is intentionally ignored — we always trust
140 * the latest verdict the AI produced, even if it's a few days old.
141 * Re-analysis happens automatically on `edit_comment`, so the meta
142 * stays fresh under normal moderation flows.
143 *
144 * @since 0.8.3
145 *
146 * @param int $score Default heuristic score (0–100).
147 * @param WP_Comment $comment Comment object.
148 * @return int Adjusted score, clamped to 0–100.
149 */
150 function desktop_mode_comments_ai_filter_spam_score( $score, $comment ) {
151 if ( ! $comment instanceof WP_Comment ) {
152 return $score;
153 }
154 if ( ! function_exists( 'desktop_mode_ai_get_meta' ) ) {
155 return $score;
156 }
157 $meta = desktop_mode_ai_get_meta( 'comment', (int) $comment->comment_ID );
158 if ( ! is_array( $meta ) ) {
159 return $score;
160 }
161 $score = (int) $score;
162 if ( ! empty( $meta['spam'] ) ) {
163 // Pin to high-tone territory. Heuristics may already have it
164 // at 80; we just guarantee a floor.
165 $score = max( $score, 75 );
166 }
167 if ( ! empty( $meta['harmful'] ) ) {
168 $score = min( 100, $score + 20 );
169 }
170 return $score;
171 }
172 add_filter(
173 'desktop_mode_comments_window_spam_score',
174 'desktop_mode_comments_ai_filter_spam_score',
175 10,
176 2
177 );
178
179 /**
180 * REST: GET / POST `desktop-mode/v1/comments/ai-settings`.
181 *
182 * Read returns `{ enabled: bool, providerConfigured: bool }`.
183 * Write accepts `{ enabled: bool }`. Both `manage_options`-gated.
184 *
185 * `providerConfigured` is a convenience the UI uses to disable the
186 * toggle with a "Configure an AI provider first" hint when no admin
187 * on the site has wired up an API key. It's a UX cue — the toggle
188 * stays writable even when it's `false` so an admin who's about to
189 * configure the provider can flip this on first.
190 *
191 * @since 0.8.3
192 */
193 function desktop_mode_comments_ai_register_rest_route() {
194 register_rest_route(
195 'desktop-mode/v1',
196 '/comments/ai-settings',
197 array(
198 array(
199 'methods' => WP_REST_Server::READABLE,
200 'callback' => 'desktop_mode_comments_ai_rest_get',
201 'permission_callback' => static function () {
202 return current_user_can( 'manage_options' );
203 },
204 ),
205 array(
206 'methods' => WP_REST_Server::CREATABLE,
207 'callback' => 'desktop_mode_comments_ai_rest_post',
208 'permission_callback' => static function () {
209 return current_user_can( 'manage_options' );
210 },
211 'args' => array(
212 'enabled' => array(
213 'required' => true,
214 'type' => 'boolean',
215 ),
216 ),
217 ),
218 )
219 );
220 }
221 add_action( 'rest_api_init', 'desktop_mode_comments_ai_register_rest_route' );
222
223 /**
224 * REST GET handler — returns the current state + provider hint.
225 *
226 * @since 0.8.3
227 *
228 * @return WP_REST_Response
229 */
230 function desktop_mode_comments_ai_rest_get() {
231 return new WP_REST_Response(
232 array(
233 'enabled' => desktop_mode_comments_ai_is_enabled(),
234 'providerConfigured' => desktop_mode_comments_ai_provider_configured(),
235 ),
236 200
237 );
238 }
239
240 /**
241 * REST POST handler — updates the toggle.
242 *
243 * @since 0.8.3
244 *
245 * @param WP_REST_Request $request Request.
246 * @return WP_REST_Response
247 */
248 function desktop_mode_comments_ai_rest_post( WP_REST_Request $request ) {
249 $enabled = (bool) $request['enabled'];
250 update_option( DESKTOP_MODE_COMMENTS_AI_OPTION, $enabled, false );
251
252 /**
253 * Fires after the Comments AI moderation toggle is changed.
254 *
255 * @since 0.8.3
256 *
257 * @param bool $enabled New state.
258 */
259 do_action( 'desktop_mode_comments_ai_toggled', $enabled );
260
261 return new WP_REST_Response(
262 array(
263 'enabled' => $enabled,
264 'providerConfigured' => desktop_mode_comments_ai_provider_configured(),
265 ),
266 200
267 );
268 }
269
270 /**
271 * Whether a usable AI text-generation provider is configured in Connectors.
272 *
273 * Delegates to the AI Copilot's capability check
274 * ({@see desktop_mode_ai_provider_configured()}), which inspects the WordPress
275 * AI Client's provider registry without making a network request. Returns
276 * `false` when the AI Copilot bundle isn't loaded.
277 *
278 * @since 0.8.3
279 *
280 * @return bool
281 */
282 function desktop_mode_comments_ai_provider_configured() {
283 return function_exists( 'desktop_mode_ai_provider_configured' )
284 && desktop_mode_ai_provider_configured();
285 }
286