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

285 lines 8.6 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 so a site's existing provider
18 * config (`includes/ai-copilot/settings.php`) is the single source of
19 * truth for API keys, provider selection, and rate-limiting.
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 comment, queue an AI analysis job — but only when the
73 * site has the Comments AI toggle on AND the AI Copilot has a
74 * resolvable admin / API key. Idempotent: the AI job pipeline
75 * dedupes on `comment_<id>`, so a re-fire from `edit_comment`
76 * doesn't double-spend tokens.
77 *
78 * Runs at priority 25 — AFTER the AI Copilot's own `priority 20`
79 * hook in `includes/ai-copilot/hooks.php`. The Copilot's hook runs
80 * for every install where any admin has AI enabled; we layer on top
81 * to also fire when an anonymous commenter posts on a site whose
82 * admin opted into Comments AI but didn't enable the Copilot more
83 * broadly.
84 *
85 * @since 0.8.3
86 *
87 * @param int $comment_id Newly-inserted comment id.
88 */
89 function desktop_mode_comments_ai_on_new_comment( $comment_id ) {
90 $comment_id = (int) $comment_id;
91 if ( $comment_id <= 0 || ! desktop_mode_comments_ai_is_enabled() ) {
92 return;
93 }
94
95 $comment = get_comment( $comment_id );
96 if ( ! $comment instanceof WP_Comment ) {
97 return;
98 }
99 if ( '' !== $comment->comment_type && 'comment' !== $comment->comment_type ) {
100 return;
101 }
102
103 if ( ! function_exists( 'desktop_mode_ai_resolve_user_id' )
104 || ! function_exists( 'desktop_mode_ai_is_enabled' )
105 || ! function_exists( 'desktop_mode_ai_schedule_job' )
106 ) {
107 return;
108 }
109
110 $user_id = (int) $comment->user_id;
111 if ( $user_id <= 0 || ! desktop_mode_ai_is_enabled( $user_id ) ) {
112 $user_id = (int) desktop_mode_ai_resolve_user_id();
113 }
114 if ( $user_id <= 0 || ! desktop_mode_ai_is_enabled( $user_id ) ) {
115 return;
116 }
117
118 desktop_mode_ai_schedule_job(
119 'desktop_mode_ai_analyze_comment',
120 array( $comment_id, $user_id ),
121 'comment_' . $comment_id
122 );
123 }
124 add_action( 'wp_insert_comment', 'desktop_mode_comments_ai_on_new_comment', 25, 1 );
125
126 /**
127 * Fold the AI verdict into the per-row spam-confidence score.
128 *
129 * If a comment has been analyzed and the verdict came back with
130 * `spam === true`, push the score firmly into the "high" tone
131 * (≥ 70). `harmful === true` adds 20 — harmful but on-topic comments
132 * deserve moderation attention even when they don't tip into spam.
133 *
134 * The `analyzed_at` field is intentionally ignored — we always trust
135 * the latest verdict the AI produced, even if it's a few days old.
136 * Re-analysis happens automatically on `edit_comment`, so the meta
137 * stays fresh under normal moderation flows.
138 *
139 * @since 0.8.3
140 *
141 * @param int $score Default heuristic score (0–100).
142 * @param WP_Comment $comment Comment object.
143 * @return int Adjusted score, clamped to 0–100.
144 */
145 function desktop_mode_comments_ai_filter_spam_score( $score, $comment ) {
146 if ( ! $comment instanceof WP_Comment ) {
147 return $score;
148 }
149 if ( ! function_exists( 'desktop_mode_ai_get_meta' ) ) {
150 return $score;
151 }
152 $meta = desktop_mode_ai_get_meta( 'comment', (int) $comment->comment_ID );
153 if ( ! is_array( $meta ) ) {
154 return $score;
155 }
156 $score = (int) $score;
157 if ( ! empty( $meta['spam'] ) ) {
158 // Pin to high-tone territory. Heuristics may already have it
159 // at 80; we just guarantee a floor.
160 $score = max( $score, 75 );
161 }
162 if ( ! empty( $meta['harmful'] ) ) {
163 $score = min( 100, $score + 20 );
164 }
165 return $score;
166 }
167 add_filter(
168 'desktop_mode_comments_window_spam_score',
169 'desktop_mode_comments_ai_filter_spam_score',
170 10,
171 2
172 );
173
174 /**
175 * REST: GET / POST `desktop-mode/v1/comments/ai-settings`.
176 *
177 * Read returns `{ enabled: bool, providerConfigured: bool }`.
178 * Write accepts `{ enabled: bool }`. Both `manage_options`-gated.
179 *
180 * `providerConfigured` is a convenience the UI uses to disable the
181 * toggle with a "Configure an AI provider first" hint when no admin
182 * on the site has wired up an API key. It's a UX cue — the toggle
183 * stays writable even when it's `false` so an admin who's about to
184 * configure the provider can flip this on first.
185 *
186 * @since 0.8.3
187 */
188 function desktop_mode_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' => 'desktop_mode_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' => 'desktop_mode_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', 'desktop_mode_comments_ai_register_rest_route' );
217
218 /**
219 * REST GET handler — returns the current state + provider hint.
220 *
221 * @since 0.8.3
222 *
223 * @return WP_REST_Response
224 */
225 function desktop_mode_comments_ai_rest_get() {
226 return new WP_REST_Response(
227 array(
228 'enabled' => desktop_mode_comments_ai_is_enabled(),
229 'providerConfigured' => desktop_mode_comments_ai_provider_configured(),
230 ),
231 200
232 );
233 }
234
235 /**
236 * REST POST handler — updates the toggle.
237 *
238 * @since 0.8.3
239 *
240 * @param WP_REST_Request $request Request.
241 * @return WP_REST_Response
242 */
243 function desktop_mode_comments_ai_rest_post( WP_REST_Request $request ) {
244 $enabled = (bool) $request['enabled'];
245 update_option( DESKTOP_MODE_COMMENTS_AI_OPTION, $enabled, false );
246
247 /**
248 * Fires after the Comments AI moderation toggle is changed.
249 *
250 * @since 0.8.3
251 *
252 * @param bool $enabled New state.
253 */
254 do_action( 'desktop_mode_comments_ai_toggled', $enabled );
255
256 return new WP_REST_Response(
257 array(
258 'enabled' => $enabled,
259 'providerConfigured' => desktop_mode_comments_ai_provider_configured(),
260 ),
261 200
262 );
263 }
264
265 /**
266 * Whether the site has at least one admin with AI configured.
267 *
268 * Cheap; reads the resolved-admin id and checks
269 * `desktop_mode_ai_is_enabled` against it. Returns `false` when the
270 * AI Copilot bundle isn't loaded.
271 *
272 * @since 0.8.3
273 *
274 * @return bool
275 */
276 function desktop_mode_comments_ai_provider_configured() {
277 if ( ! function_exists( 'desktop_mode_ai_resolve_user_id' )
278 || ! function_exists( 'desktop_mode_ai_is_enabled' )
279 ) {
280 return false;
281 }
282 $user_id = (int) desktop_mode_ai_resolve_user_id();
283 return $user_id > 0 && desktop_mode_ai_is_enabled( $user_id );
284 }
285