PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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
← All changes | includes/presence.php +212 -180 0.8.91.1.10 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 defined( 'ABSPATH' ) || exit;
3 3 /**
4 - * Desktop Mode — framework-level presence.
4 + * OpenStation — framework-level presence.
5 5 *
6 - * Tracks who's currently in the desktop-mode WP-Admin and what
6 + * Tracks who's currently in the openstation WP-Admin and what
7 7 * their state is — `online`, `inactive`, `offline`. Lives at
8 8 * framework level so any plugin can consume presence without
9 9 * depending on chat / collaboration / co-editing features being
10 10 * enabled.
@@ -17,68 +17,59 @@
17 17 * - **inactive** — Heartbeat seen within `_offline_after` but no
18 18 * user activity within `_inactive_after`.
19 19 * - **offline** — no Heartbeat in `_offline_after` (default 120s).
20 20 *
21 - * Storage is a single autoload=false option (`_desktop_mode_presence`)
22 - * shaped `array<int user_id, array{ last_seen_ms, last_active_ms }>`.
23 - * Single-row keeps autoload happy and avoids per-user options.
21 + * Storage uses a site-scoped table with one row per user.
22 + * The legacy `_desktop_mode_presence` option is retained for recovery.
24 23 *
25 24 * **Public surface.** PHP helpers:
26 25 *
27 - * - `desktop_mode_presence_record( $user_id, $active )`
28 - * - `desktop_mode_presence_status_for_user( $user_id )`
29 - * - `desktop_mode_presence_get_all()`
30 - * - `desktop_mode_presence_snapshot( $user_ids = null )`
26 + * - `openstation_presence_record( $user_id, $active )`
27 + * - `openstation_presence_status_for_user( $user_id )`
28 + * - `openstation_presence_get_all()`
29 + * - `openstation_presence_snapshot( $user_ids = null )`
31 30 *
32 31 * Filters:
33 32 *
34 - * - `desktop_mode_presence_inactive_after` — int seconds. Default 300.
35 - * - `desktop_mode_presence_offline_after` — int seconds. Default 120.
36 - * - `desktop_mode_presence_can_track` — bool, $user_id. Veto.
37 - * - `desktop_mode_presence_visible_users` — int[], $viewer_id.
33 + * - `openstation_presence_inactive_after` — int seconds. Default 300.
34 + * - `openstation_presence_offline_after` — int seconds. Default 120.
35 + * - `openstation_presence_can_track` — bool, $user_id. Veto.
36 + * - `openstation_presence_visible_users` — int[], $viewer_id.
38 37 * Privacy gate for who's
39 38 * surfaced to a given user.
40 39 *
41 40 * Actions:
42 41 *
43 - * - `desktop_mode_presence_recorded( $user_id, $record )` — on every
42 + * - `openstation_presence_recorded( $user_id, $record )` — on every
44 43 * bump.
45 - * - `desktop_mode_presence_changed( $user_id, $new, $old )` — on
44 + * - `openstation_presence_changed( $user_id, $new, $old )` — on
46 45 * state transitions only.
47 46 *
48 47 * REST: `/desktop-mode/v1/presence` (GET snapshot, POST mark active /
49 48 * inactive).
50 49 *
51 - * @package WPDesktopMode
52 - * @since 0.5.5
50 + * @package OpenStation
53 51 */
54 52
55 -const DESKTOP_MODE_PRESENCE_OPTION = '_desktop_mode_presence';
53 +/**
54 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
55 + * persisted or externally-visible identifier, so renaming it would
56 + * orphan data already written by live installs (or break a live
57 + * URL). The mismatch between this constant's name and its value is
58 + * deliberate — it is NOT a half-finished rename.
59 + */
60 +const OPENSTATION_PRESENCE_OPTION = '_desktop_mode_presence';
56 61
62 +require_once __DIR__ . '/presence-store.php';
63 +
57 64 /**
58 - * Read the entire presence map. Single autoload=false option.
65 + * Read the current site's presence map.
59 66 *
60 - * @since 0.5.5
61 - *
62 67 * @return array<int,array{last_seen_ms:int,last_active_ms:int}>
63 68 */
64 -function desktop_mode_presence_get_all() {
65 - $raw = get_option( DESKTOP_MODE_PRESENCE_OPTION, array() );
66 - if ( ! is_array( $raw ) ) {
67 - return array();
68 - }
69 - $out = array();
70 - foreach ( $raw as $uid => $record ) {
71 - $uid = (int) $uid;
72 - if ( $uid <= 0 || ! is_array( $record ) ) {
73 - continue;
74 - }
75 - $out[ $uid ] = array(
76 - 'last_seen_ms' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0,
77 - 'last_active_ms' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0,
78 - );
79 - }
80 - return $out;
69 +function openstation_presence_get_all() {
70 + $records = openstation_presence_read_records();
71 + return is_wp_error( $records ) ? array() : $records;
81 72 }
82 73
83 74 /**
84 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
@@ -84,28 +75,39 @@
84 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
85 76 * `$active` is true, also bumps `last_active_ms` (the user just
86 77 * interacted, not just held a tab open).
87 78 *
88 - * Pass-through to the option; cheap enough to call every Heartbeat
89 - * tick. Fires `desktop_mode_presence_recorded` on every call and
90 - * `desktop_mode_presence_changed` only when the computed status moves
91 - * between `online | inactive | offline`.
79 + * Writes are throttled unless status changes or a persisted timestamp is
80 + * behind by half the offline threshold (capped at 60s). Each write atomically
81 + * merges only this user's timestamps. Fires `openstation_presence_recorded`
82 + * on every accepted bump, including throttled bumps, and
83 + * `openstation_presence_changed` when this call observes a status transition.
92 84 *
93 - * The `desktop_mode_presence_can_track` filter is the per-user opt-out:
85 + * The `openstation_presence_can_track` filter is the per-user opt-out:
94 86 * a plugin that hides specific accounts (compliance, "set yourself
95 87 * invisible", etc.) returns false to skip the bump entirely.
96 88 *
97 - * @since 0.5.5
98 - *
99 89 * @param int $user_id User to record.
100 90 * @param bool $active Pass `true` when the heartbeat is paired with
101 91 * explicit user activity (mousedown, keydown).
102 - * @return bool True if recorded; false if vetoed by filter or invalid id.
92 + * @return bool True if accepted; false on invalid id, tracking veto or storage failure.
103 93 */
104 -function desktop_mode_presence_record( $user_id, $active = true ) {
94 +function openstation_presence_record( $user_id, $active = true ) {
95 + return true === openstation_presence_record_result( $user_id, $active );
96 +}
97 +
98 +/**
99 + * Record presence while preserving a distinct veto and storage failure result.
100 + *
101 + * @internal
102 + * @param int $user_id User to record.
103 + * @param bool $active Whether this request carries activity.
104 + * @return true|WP_Error
105 + */
106 +function openstation_presence_record_result( $user_id, $active = true ) {
105 107 $user_id = (int) $user_id;
106 108 if ( $user_id <= 0 ) {
107 - return false;
109 + return new WP_Error( 'openstation_presence_invalid_user', __( 'A user id is required.', 'desktop-mode' ) );
108 110 }
109 111
110 112 /**
111 113 * Per-user veto on presence tracking. Return false to skip the
@@ -112,46 +114,57 @@
112 114 * bump entirely — useful for "appear offline" toggles, audit
113 115 * exemptions for sensitive accounts, or forcing a non-admin
114 116 * never-tracked policy.
115 117 *
116 - * @since 0.5.5
117 - *
118 118 * @param bool $can Default true.
119 119 * @param int $user_id The user being tracked.
120 120 */
121 - $can = (bool) apply_filters( 'desktop_mode_presence_can_track', true, $user_id );
121 + $can = (bool) apply_filters( 'openstation_presence_can_track', true, $user_id );
122 122 if ( ! $can ) {
123 - return false;
123 + return new WP_Error( 'openstation_presence_tracking_veto' );
124 124 }
125 125
126 126 $now_ms = (int) round( microtime( true ) * 1000 );
127 - $all = desktop_mode_presence_get_all();
128 - $prev = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
127 + $all = openstation_presence_read_records( $user_id );
128 + if ( is_wp_error( $all ) ) {
129 + return $all;
130 + }
131 + $prev = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
129 132 'last_seen_ms' => 0,
130 133 'last_active_ms' => 0,
131 134 );
132 - $prev_status = desktop_mode_presence_status_from_record( $prev );
135 + $prev_status = openstation_presence_status_from_record( $prev );
133 136
134 137 $next = array(
135 138 'last_seen_ms' => $now_ms,
136 139 'last_active_ms' => $active ? $now_ms : (int) $prev['last_active_ms'],
137 140 );
138 - $all[ $user_id ] = $next;
139 - update_option( DESKTOP_MODE_PRESENCE_OPTION, $all, false );
140 141
141 - $next_status = desktop_mode_presence_status_from_record( $next );
142 + $next_status = openstation_presence_status_from_record( $next );
142 143
144 + if ( openstation_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) ) {
145 + $write = $next;
146 + $write['last_active_ms'] = $active ? $now_ms : 0;
147 + if ( ! openstation_presence_write_record( $user_id, $write ) ) {
148 + return new WP_Error( 'openstation_presence_write_failed', __( 'Could not save presence.', 'desktop-mode' ), array( 'status' => 503 ) );
149 + }
150 + $stored = openstation_presence_read_records( $user_id );
151 + if ( is_wp_error( $stored ) ) {
152 + return $stored;
153 + }
154 + $next = $stored[ $user_id ] ?? $prev;
155 + $next_status = openstation_presence_status_from_record( $next );
156 + }
157 +
143 158 /**
144 159 * Fires on every recorded heartbeat — useful for audit logging
145 160 * or third-party "who's around right now" dashboards. Fires
146 161 * regardless of whether the computed status changed.
147 162 *
148 - * @since 0.5.5
149 - *
150 163 * @param int $user_id
151 164 * @param array $record { last_seen_ms, last_active_ms }
152 165 */
153 - do_action( 'desktop_mode_presence_recorded', $user_id, $next );
166 + do_action( 'openstation_presence_recorded', $user_id, $next );
154 167
155 168 if ( $next_status !== $prev_status ) {
156 169 /**
157 170 * Fires when a user's computed presence status transitions.
@@ -158,20 +171,64 @@
158 171 * Plugins driving "user came online / went offline" UI hook
159 172 * here — the recorded action above fires every tick whether
160 173 * the state changed or not, which would be too noisy.
161 174 *
162 - * @since 0.5.5
163 - *
164 175 * @param int $user_id
165 176 * @param string $new_status One of `online | inactive | offline`.
166 177 * @param string $old_status One of `online | inactive | offline`.
167 178 */
168 - do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status );
179 + do_action( 'openstation_presence_changed', $user_id, $next_status, $prev_status );
169 180 }
170 181 return true;
171 182 }
172 183
173 184 /**
185 + * Decide whether a presence bump needs to hit the database.
186 + *
187 + * A bump must persist when:
188 + *
189 + * - the user isn't in the map yet (first sighting),
190 + * - the computed status transitioned (viewers must see it), or
191 + * - a persisted timestamp has drifted by at least the throttle
192 + * window — half the offline threshold, capped at 60s — so stored
193 + * `last_seen_ms` can never age anywhere near the offline cutoff
194 + * while the user is genuinely present.
195 + *
196 + * Everything else is a redundant rewrite and is skipped. Skipped
197 + * bumps still fire `openstation_presence_recorded` with the fresh
198 + * record — only the persisted copy lags.
199 + *
200 + * @param array $all Stored presence map.
201 + * @param int $user_id User being bumped.
202 + * @param array $prev Stored record for the user (zeros if new).
203 + * @param string $prev_status Status computed from the stored record.
204 + * @param string $next_status Status computed from the fresh record.
205 + * @param bool $active Whether this bump carries user activity.
206 + * @param int $now_ms Current epoch milliseconds.
207 + * @return bool True to persist, false to skip the write.
208 + */
209 +function openstation_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) {
210 + if ( ! isset( $all[ $user_id ] ) ) {
211 + return true;
212 + }
213 + if ( $next_status !== $prev_status ) {
214 + return true;
215 + }
216 +
217 + /** This filter is documented in includes/presence.php */
218 + $offline_after = (int) apply_filters( 'openstation_presence_offline_after', 120 );
219 + $throttle_ms = (int) min( 60 * 1000, $offline_after * 500 );
220 +
221 + if ( ( $now_ms - (int) $prev['last_seen_ms'] ) >= $throttle_ms ) {
222 + return true;
223 + }
224 + if ( $active && ( $now_ms - (int) $prev['last_active_ms'] ) >= $throttle_ms ) {
225 + return true;
226 + }
227 + return false;
228 +}
229 +
230 +/**
174 231 * Compute presence status from a record.
175 232 *
176 233 * Pure function — given the same `(record, now)`, always returns
177 234 * the same answer. Filters override the thresholds, not the logic
@@ -177,14 +234,12 @@
177 234 * the same answer. Filters override the thresholds, not the logic
178 235 * order; an `online` user transitions through `inactive` to
179 236 * `offline` if they're idle long enough.
180 237 *
181 - * @since 0.5.5
182 - *
183 238 * @param array $record { last_seen_ms?: int, last_active_ms?: int }
184 239 * @return string `online | inactive | offline`
185 240 */
186 -function desktop_mode_presence_status_from_record( $record ) {
241 +function openstation_presence_status_from_record( $record ) {
187 242 $now_ms = (int) round( microtime( true ) * 1000 );
188 243 $last_seen = isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0;
189 244 $last_active = isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0;
190 245
@@ -192,13 +247,11 @@
192 247 * Inactive threshold (default 300s = 5 min). Online users
193 248 * transition to `inactive` when they haven't moused / typed
194 249 * for this long, even if Heartbeat keeps firing.
195 250 *
196 - * @since 0.5.5
197 - *
198 251 * @param int $seconds
199 252 */
200 - $inactive_after = (int) apply_filters( 'desktop_mode_presence_inactive_after', 300 );
253 + $inactive_after = (int) apply_filters( 'openstation_presence_inactive_after', 300 );
201 254
202 255 /**
203 256 * Offline threshold (default 120s = 2 min). Inactive / online
204 257 * users transition to `offline` when the last Heartbeat is
@@ -203,13 +256,11 @@
203 256 * Offline threshold (default 120s = 2 min). Inactive / online
204 257 * users transition to `offline` when the last Heartbeat is
205 258 * older than this.
206 259 *
207 - * @since 0.5.5
208 - *
209 260 * @param int $seconds
210 261 */
211 - $offline_after = (int) apply_filters( 'desktop_mode_presence_offline_after', 120 );
262 + $offline_after = (int) apply_filters( 'openstation_presence_offline_after', 120 );
212 263
213 264 if ( $now_ms - $last_seen > $offline_after * 1000 ) {
214 265 return 'offline';
215 266 }
@@ -221,17 +272,16 @@
221 272
222 273 /**
223 274 * Look up presence status for a single user.
224 275 *
225 - * @since 0.5.5
226 - *
227 276 * @param int $user_id
228 277 * @return string `online | inactive | offline`
229 278 */
230 -function desktop_mode_presence_status_for_user( $user_id ) {
231 - $all = desktop_mode_presence_get_all();
279 +function openstation_presence_status_for_user( $user_id ) {
280 + $all = openstation_presence_read_records( (int) $user_id );
281 + $all = is_wp_error( $all ) ? array() : $all;
232 282 $record = isset( $all[ (int) $user_id ] ) ? $all[ (int) $user_id ] : array();
233 - return desktop_mode_presence_status_from_record( (array) $record );
283 + return openstation_presence_status_from_record( (array) $record );
234 284 }
235 285
236 286 /**
237 287 * Build a presence snapshot. With `$user_ids = null` returns every
@@ -241,15 +291,13 @@
241 291 *
242 292 * Output shape uses string keys so the JSON encoder produces an
243 293 * object (not a sparse array) when the smallest id isn't 1.
244 294 *
245 - * @since 0.5.5
246 - *
247 295 * @param int[]|null $user_ids Restrict to these ids. `null` = all.
248 296 * @return array<string,array{ status:string, lastSeenMs:int, lastActiveMs:int }>
249 297 */
250 -function desktop_mode_presence_snapshot( $user_ids = null ) {
251 - $all = desktop_mode_presence_get_all();
298 +function openstation_presence_snapshot( $user_ids = null ) {
299 + $all = openstation_presence_get_all();
252 300 $out = array();
253 301
254 302 if ( null === $user_ids ) {
255 303 $ids = array_keys( $all );
@@ -263,11 +311,11 @@
263 311 }
264 312 }
265 313
266 314 foreach ( $ids as $uid ) {
267 - $record = isset( $all[ $uid ] ) ? $all[ $uid ] : array();
315 + $record = isset( $all[ $uid ] ) ? $all[ $uid ] : array();
268 316 $out[ (string) $uid ] = array(
269 - 'status' => desktop_mode_presence_status_from_record( $record ),
317 + 'status' => openstation_presence_status_from_record( $record ),
270 318 'lastSeenMs' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0,
271 319 'lastActiveMs' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0,
272 320 );
273 321 }
@@ -277,20 +325,21 @@
277 325 /**
278 326 * Filter a list of candidate user ids down to those a given viewer
279 327 * is allowed to see presence for. Defaults to passing the list
280 328 * through unchanged — plugins implementing per-team / per-role
281 - * privacy boundaries hook `desktop_mode_presence_visible_users`
329 + * privacy boundaries hook `openstation_presence_visible_users`
282 330 * (e.g., "subscribers can only see other subscribers' presence").
283 331 *
284 - * @since 0.5.5
285 - *
286 332 * @param int[] $candidate_user_ids
287 333 * @param int $viewer_id Defaults to the current user.
288 334 * @return int[]
289 335 */
290 -function desktop_mode_presence_visible_users( $candidate_user_ids, $viewer_id = 0 ) {
291 - $viewer_id = (int) $viewer_id ?: get_current_user_id();
292 - $ids = array();
336 +function openstation_presence_visible_users( $candidate_user_ids, $viewer_id = 0 ) {
337 + $viewer_id = (int) $viewer_id;
338 + if ( ! $viewer_id ) {
339 + $viewer_id = get_current_user_id();
340 + }
341 + $ids = array();
293 342 foreach ( (array) $candidate_user_ids as $uid ) {
294 343 $uid = (int) $uid;
295 344 if ( $uid > 0 ) {
296 345 $ids[] = $uid;
@@ -303,139 +352,113 @@
303 352 * `$viewer_id`. Default behaviour: all candidates pass. Hook
304 353 * to enforce privacy — e.g., subscribers only see other
305 354 * subscribers; admins see everyone; an opt-out list never shows.
306 355 *
307 - * @since 0.5.5
308 - *
309 356 * @param int[] $ids Candidate user ids.
310 357 * @param int $viewer_id The user requesting visibility.
311 358 */
312 - return (array) apply_filters( 'desktop_mode_presence_visible_users', $ids, $viewer_id );
359 + return (array) apply_filters( 'openstation_presence_visible_users', $ids, $viewer_id );
313 360 }
314 361
315 362 /**
316 363 * Daily cron: prune presence entries for users idle >14 days.
317 - * Keeps the option compact even on long-running sites.
318 - *
319 - * @since 0.5.5
364 + * Deletes only rows still expired when the statement executes.
320 365 */
321 -function desktop_mode_presence_cron_prune() {
322 - $all = desktop_mode_presence_get_all();
323 - if ( empty( $all ) ) {
366 +function openstation_presence_cron_prune() {
367 + global $wpdb;
368 + // Do not rewrite the shared legacy map if migration is unavailable.
369 + if ( ! openstation_presence_migrate_storage() ) {
324 370 return;
325 371 }
326 - $threshold = (int) round( microtime( true ) * 1000 ) - ( 14 * DAY_IN_SECONDS * 1000 );
327 - $pruned = array();
328 - foreach ( $all as $uid => $record ) {
329 - if ( ( (int) $record['last_seen_ms'] ) < $threshold ) {
330 - continue;
331 - }
332 - $pruned[ (int) $uid ] = $record;
333 - }
334 - if ( count( $pruned ) !== count( $all ) ) {
335 - update_option( DESKTOP_MODE_PRESENCE_OPTION, $pruned, false );
336 - }
372 + $table = openstation_presence_table();
373 + $cutoff = (int) round( microtime( true ) * 1000 ) - 14 * DAY_IN_SECONDS * 1000;
374 + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE last_seen_ms < %d", $cutoff ) );
375 + openstation_presence_invalidate_records();
337 376 }
338 -add_action( 'desktop_mode_presence_daily_prune', 'desktop_mode_presence_cron_prune' );
377 +add_action( 'desktop_mode_presence_daily_prune', 'openstation_presence_cron_prune' );
339 378
340 379 /**
341 380 * Schedule the daily cron once. Idempotent.
342 - *
343 - * @since 0.5.5
344 381 */
345 -function desktop_mode_presence_schedule_cron() {
382 +function openstation_presence_schedule_cron() {
346 383 if ( ! wp_next_scheduled( 'desktop_mode_presence_daily_prune' ) ) {
347 384 wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'desktop_mode_presence_daily_prune' );
348 385 }
349 386 }
350 -add_action( 'init', 'desktop_mode_presence_schedule_cron', 50 );
387 +add_action( 'init', 'openstation_presence_schedule_cron', 50 );
351 388
352 -/* -------------------------------------------------------------------------
389 +/*
390 +-------------------------------------------------------------------------
353 391 * Heartbeat integration
354 - * ----------------------------------------------------------------------- */
392 + * -----------------------------------------------------------------------
393 + */
355 394
356 395 /**
357 - * Heartbeat handler — bumps presence on every tick a desktop-mode
396 + * Heartbeat handler — bumps presence on every tick a openstation
358 397 * user is on the page. Returns the visible-presence snapshot in
359 398 * the response so the client store can update without a separate
360 399 * REST round-trip.
361 400 *
362 - * Triggered by the client opting in via `desktop_mode_presence_active:
401 + * Triggered by the client opting in via `openstation_presence_active:
363 402 * true` in the heartbeat-send payload, with optional
364 - * `desktop_mode_user_active` (mousedown / keydown within the
403 + * `openstation_user_active` (mousedown / keydown within the
365 404 * inactive-threshold window).
366 405 *
367 - * @since 0.5.5
368 - *
369 406 * @param array $response Pre-filtered response.
370 407 * @param array $data Client-sent payload.
371 408 * @return array
372 409 */
373 -function desktop_mode_presence_heartbeat_received( $response, $data ) {
410 +function openstation_presence_heartbeat_received( $response, $data ) {
374 411 if ( ! is_array( $response ) ) {
375 412 $response = array();
376 413 }
377 - if ( empty( $data['desktop_mode_presence_active'] ) ) {
414 + if ( empty( $data['openstation_presence_active'] ) ) {
378 415 return $response;
379 416 }
380 - if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
417 + if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
381 418 return $response;
382 419 }
383 420 $user_id = (int) get_current_user_id();
384 - $user_active = ! empty( $data['desktop_mode_user_active'] );
421 + $user_active = ! empty( $data['openstation_user_active'] );
385 422
386 - desktop_mode_presence_record( $user_id, $user_active );
423 + openstation_presence_migration_tick();
424 + openstation_presence_record( $user_id, $user_active );
387 425
388 426 // Snapshot the users this viewer is allowed to see — by default
389 427 // all tracked users; plugins can narrow via the
390 - // `desktop_mode_presence_visible_users` filter.
391 - $all_ids = array_keys( desktop_mode_presence_get_all() );
392 - $visible = desktop_mode_presence_visible_users( $all_ids, $user_id );
428 + // `openstation_presence_visible_users` filter.
429 + $all_ids = array_keys( openstation_presence_get_all() );
430 + $visible = openstation_presence_visible_users( $all_ids, $user_id );
393 431
394 - $response['desktop_mode_presence'] = array(
395 - 'snapshot' => desktop_mode_presence_snapshot( $visible ),
432 + $response['openstation_presence'] = array(
433 + 'snapshot' => openstation_presence_snapshot( $visible ),
396 434 'serverTimeMs' => (int) round( microtime( true ) * 1000 ),
397 435 );
398 436 return $response;
399 437 }
400 -add_filter( 'heartbeat_received', 'desktop_mode_presence_heartbeat_received', 5, 2 );
438 +add_filter( 'heartbeat_received', 'openstation_presence_heartbeat_received', 5, 2 );
401 439
402 -/* -------------------------------------------------------------------------
440 +/*
441 +-------------------------------------------------------------------------
403 442 * REST endpoints
404 - * ----------------------------------------------------------------------- */
443 + * -----------------------------------------------------------------------
444 + */
405 445
406 446 /**
407 447 * Permission gate for presence endpoints — login required +
408 - * desktop mode enabled.
448 + * OpenStation enabled. Delegates to the shared
449 + * {@see openstation_rest_require_enabled()} gate.
409 450 *
410 - * @since 0.5.5
411 - *
412 - * @return bool|WP_Error
451 + * @return true|WP_Error
413 452 */
414 -function desktop_mode_presence_rest_permission() {
415 - if ( ! is_user_logged_in() ) {
416 - return new WP_Error(
417 - 'rest_forbidden',
418 - __( 'Authentication required.', 'desktop-mode' ),
419 - array( 'status' => 401 )
420 - );
421 - }
422 - if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
423 - return new WP_Error(
424 - 'rest_forbidden',
425 - __( 'Desktop mode is not enabled for your account.', 'desktop-mode' ),
426 - array( 'status' => 403 )
427 - );
428 - }
429 - return true;
453 +function openstation_presence_rest_permission() {
454 + return openstation_rest_require_enabled();
430 455 }
431 456
432 457 /**
433 458 * Register `/desktop-mode/v1/presence` routes.
434 - *
435 - * @since 0.5.5
436 459 */
437 -function desktop_mode_presence_register_rest_routes() {
460 +function openstation_presence_register_rest_routes() {
438 461 register_rest_route(
439 462 'desktop-mode/v1',
440 463 '/presence',
441 464 array(
@@ -440,15 +463,15 @@
440 463 '/presence',
441 464 array(
442 465 array(
443 466 'methods' => WP_REST_Server::READABLE,
444 - 'permission_callback' => 'desktop_mode_presence_rest_permission',
445 - 'callback' => 'desktop_mode_presence_rest_get',
467 + 'permission_callback' => 'openstation_presence_rest_permission',
468 + 'callback' => 'openstation_presence_rest_get',
446 469 ),
447 470 array(
448 471 'methods' => WP_REST_Server::CREATABLE,
449 - 'permission_callback' => 'desktop_mode_presence_rest_permission',
450 - 'callback' => 'desktop_mode_presence_rest_post',
472 + 'permission_callback' => 'openstation_presence_rest_permission',
473 + 'callback' => 'openstation_presence_rest_post',
451 474 'args' => array(
452 475 'active' => array( 'type' => 'boolean' ),
453 476 'inactive' => array( 'type' => 'boolean' ),
454 477 ),
@@ -455,21 +478,22 @@
455 478 ),
456 479 )
457 480 );
458 481 }
459 -add_action( 'rest_api_init', 'desktop_mode_presence_register_rest_routes' );
482 +add_action( 'rest_api_init', 'openstation_presence_register_rest_routes' );
460 483
461 484 /**
462 485 * GET /desktop-mode/v1/presence — current snapshot, narrowed by the
463 486 * visibility filter.
464 487 */
465 -function desktop_mode_presence_rest_get() {
488 +function openstation_presence_rest_get() {
489 + openstation_presence_migration_tick();
466 490 $viewer_id = (int) get_current_user_id();
467 - $all_ids = array_keys( desktop_mode_presence_get_all() );
468 - $visible = desktop_mode_presence_visible_users( $all_ids, $viewer_id );
491 + $all_ids = array_keys( openstation_presence_get_all() );
492 + $visible = openstation_presence_visible_users( $all_ids, $viewer_id );
469 493 return rest_ensure_response(
470 494 array(
471 - 'snapshot' => desktop_mode_presence_snapshot( $visible ),
495 + 'snapshot' => openstation_presence_snapshot( $visible ),
472 496 'serverTimeMs' => (int) round( microtime( true ) * 1000 ),
473 497 )
474 498 );
475 499 }
@@ -485,11 +509,12 @@
485 509 *
486 510 * Defaults to `{ active: true }` when neither flag is supplied —
487 511 * the simplest "I'm here" call.
488 512 */
489 -function desktop_mode_presence_rest_post( WP_REST_Request $request ) {
490 - $user_id = (int) get_current_user_id();
491 - $active = $request->get_param( 'active' );
513 +function openstation_presence_rest_post( WP_REST_Request $request ) {
514 + openstation_presence_migration_tick();
515 + $user_id = (int) get_current_user_id();
516 + $active = $request->get_param( 'active' );
492 517 $inactive = (bool) $request->get_param( 'inactive' );
493 518
494 519 if ( $inactive ) {
495 520 // Set the user immediately to `inactive`: bump last_seen
@@ -494,27 +519,34 @@
494 519 if ( $inactive ) {
495 520 // Set the user immediately to `inactive`: bump last_seen
496 521 // (still alive) but force last_active to zero (no recent
497 522 // interaction).
498 - $all = desktop_mode_presence_get_all();
499 - $rec = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
523 + $all = openstation_presence_read_records( $user_id );
524 + if ( is_wp_error( $all ) ) {
525 + return $all;
526 + }
527 + $rec = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
500 528 'last_seen_ms' => 0,
501 529 'last_active_ms' => 0,
502 530 );
503 - $prev_status = desktop_mode_presence_status_from_record( $rec );
531 + $prev_status = openstation_presence_status_from_record( $rec );
504 532 $rec['last_seen_ms'] = (int) round( microtime( true ) * 1000 );
505 533 $rec['last_active_ms'] = 0;
506 - $all[ $user_id ] = $rec;
507 - update_option( DESKTOP_MODE_PRESENCE_OPTION, $all, false );
534 + if ( ! openstation_presence_write_record( $user_id, $rec, true ) ) {
535 + return new WP_Error( 'openstation_presence_write_failed', __( 'Could not save presence.', 'desktop-mode' ), array( 'status' => 503 ) );
536 + }
508 537
509 - $next_status = desktop_mode_presence_status_from_record( $rec );
510 - do_action( 'desktop_mode_presence_recorded', $user_id, $rec );
538 + $next_status = openstation_presence_status_from_record( $rec );
539 + do_action( 'openstation_presence_recorded', $user_id, $rec );
511 540 if ( $next_status !== $prev_status ) {
512 - do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status );
541 + do_action( 'openstation_presence_changed', $user_id, $next_status, $prev_status );
513 542 }
514 543 } else {
515 - $flag = ( null === $active ) ? true : (bool) $active;
516 - desktop_mode_presence_record( $user_id, $flag );
544 + $flag = ( null === $active ) ? true : (bool) $active;
545 + $result = openstation_presence_record_result( $user_id, $flag );
546 + if ( is_wp_error( $result ) && 'openstation_presence_tracking_veto' !== $result->get_error_code() ) {
547 + return $result;
548 + }
517 549 }
518 550
519 551 return rest_ensure_response( array( 'ok' => true ) );
520 552 }