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 +167 -146 0.9.81.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,65 +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
50 + * @package OpenStation
52 51 */
53 52
54 -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';
55 61
62 +require_once __DIR__ . '/presence-store.php';
63 +
56 64 /**
57 - * Read the entire presence map. Single autoload=false option.
65 + * Read the current site's presence map.
58 66 *
59 67 * @return array<int,array{last_seen_ms:int,last_active_ms:int}>
60 68 */
61 -function desktop_mode_presence_get_all() {
62 - $raw = get_option( DESKTOP_MODE_PRESENCE_OPTION, array() );
63 - if ( ! is_array( $raw ) ) {
64 - return array();
65 - }
66 - $out = array();
67 - foreach ( $raw as $uid => $record ) {
68 - $uid = (int) $uid;
69 - if ( $uid <= 0 || ! is_array( $record ) ) {
70 - continue;
71 - }
72 - $out[ $uid ] = array(
73 - 'last_seen_ms' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0,
74 - 'last_active_ms' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0,
75 - );
76 - }
77 - return $out;
69 +function openstation_presence_get_all() {
70 + $records = openstation_presence_read_records();
71 + return is_wp_error( $records ) ? array() : $records;
78 72 }
79 73
80 74 /**
81 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
@@ -81,21 +75,15 @@
81 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
82 76 * `$active` is true, also bumps `last_active_ms` (the user just
83 77 * interacted, not just held a tab open).
84 78 *
85 - * Cheap enough to call every Heartbeat tick: the option write is
86 - * throttled — a bump that neither transitions the computed status
87 - * nor moves a persisted timestamp by at least half the offline
88 - * threshold (capped at 60s) skips the `update_option()` call, so N
89 - * idle users no longer rewrite the shared row every tick. Persisted
90 - * timestamps can therefore lag real activity by up to the throttle
91 - * window — always well inside the offline threshold, so computed
92 - * statuses stay correct. Fires `desktop_mode_presence_recorded` on
93 - * every call (with the fresh, un-throttled record) and
94 - * `desktop_mode_presence_changed` only when the computed status moves
95 - * 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.
96 84 *
97 - * 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:
98 86 * a plugin that hides specific accounts (compliance, "set yourself
99 87 * invisible", etc.) returns false to skip the bump entirely.
100 88 *
101 89 * @param int $user_id User to record.
@@ -100,14 +88,26 @@
100 88 *
101 89 * @param int $user_id User to record.
102 90 * @param bool $active Pass `true` when the heartbeat is paired with
103 91 * explicit user activity (mousedown, keydown).
104 - * @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.
105 93 */
106 -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 ) {
107 107 $user_id = (int) $user_id;
108 108 if ( $user_id <= 0 ) {
109 - return false;
109 + return new WP_Error( 'openstation_presence_invalid_user', __( 'A user id is required.', 'desktop-mode' ) );
110 110 }
111 111
112 112 /**
113 113 * Per-user veto on presence tracking. Return false to skip the
@@ -117,20 +117,23 @@
117 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'],
@@ -135,13 +138,22 @@
135 138 'last_seen_ms' => $now_ms,
136 139 'last_active_ms' => $active ? $now_ms : (int) $prev['last_active_ms'],
137 140 );
138 141
139 - $next_status = desktop_mode_presence_status_from_record( $next );
142 + $next_status = openstation_presence_status_from_record( $next );
140 143
141 - if ( desktop_mode_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) ) {
142 - $all[ $user_id ] = $next;
143 - update_option( DESKTOP_MODE_PRESENCE_OPTION, $all, false );
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 );
144 156 }
145 157
146 158 /**
147 159 * Fires on every recorded heartbeat — useful for audit logging
@@ -150,9 +162,9 @@
150 162 *
151 163 * @param int $user_id
152 164 * @param array $record { last_seen_ms, last_active_ms }
153 165 */
154 - do_action( 'desktop_mode_presence_recorded', $user_id, $next );
166 + do_action( 'openstation_presence_recorded', $user_id, $next );
155 167
156 168 if ( $next_status !== $prev_status ) {
157 169 /**
158 170 * Fires when a user's computed presence status transitions.
@@ -163,9 +175,9 @@
163 175 * @param int $user_id
164 176 * @param string $new_status One of `online | inactive | offline`.
165 177 * @param string $old_status One of `online | inactive | offline`.
166 178 */
167 - do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status );
179 + do_action( 'openstation_presence_changed', $user_id, $next_status, $prev_status );
168 180 }
169 181 return true;
170 182 }
171 183
@@ -171,12 +183,9 @@
171 183
172 184 /**
173 185 * Decide whether a presence bump needs to hit the database.
174 186 *
175 - * The presence map is a single shared option row: with N concurrent
176 - * users an unconditional write per Heartbeat tick means N full-row
177 - * rewrites (plus option-cache invalidations) every ~15s, almost all
178 - * of them recording no meaningful change. A bump must persist when:
187 + * A bump must persist when:
179 188 *
180 189 * - the user isn't in the map yet (first sighting),
181 190 * - the computed status transitioned (viewers must see it), or
182 191 * - a persisted timestamp has drifted by at least the throttle
@@ -184,9 +193,9 @@
184 193 * `last_seen_ms` can never age anywhere near the offline cutoff
185 194 * while the user is genuinely present.
186 195 *
187 196 * Everything else is a redundant rewrite and is skipped. Skipped
188 - * bumps still fire `desktop_mode_presence_recorded` with the fresh
197 + * bumps still fire `openstation_presence_recorded` with the fresh
189 198 * record — only the persisted copy lags.
190 199 *
191 200 * @param array $all Stored presence map.
192 201 * @param int $user_id User being bumped.
@@ -196,9 +205,9 @@
196 205 * @param bool $active Whether this bump carries user activity.
197 206 * @param int $now_ms Current epoch milliseconds.
198 207 * @return bool True to persist, false to skip the write.
199 208 */
200 -function desktop_mode_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) {
209 +function openstation_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) {
201 210 if ( ! isset( $all[ $user_id ] ) ) {
202 211 return true;
203 212 }
204 213 if ( $next_status !== $prev_status ) {
@@ -205,9 +214,9 @@
205 214 return true;
206 215 }
207 216
208 217 /** This filter is documented in includes/presence.php */
209 - $offline_after = (int) apply_filters( 'desktop_mode_presence_offline_after', 120 );
218 + $offline_after = (int) apply_filters( 'openstation_presence_offline_after', 120 );
210 219 $throttle_ms = (int) min( 60 * 1000, $offline_after * 500 );
211 220
212 221 if ( ( $now_ms - (int) $prev['last_seen_ms'] ) >= $throttle_ms ) {
213 222 return true;
@@ -228,9 +237,9 @@
228 237 *
229 238 * @param array $record { last_seen_ms?: int, last_active_ms?: int }
230 239 * @return string `online | inactive | offline`
231 240 */
232 -function desktop_mode_presence_status_from_record( $record ) {
241 +function openstation_presence_status_from_record( $record ) {
233 242 $now_ms = (int) round( microtime( true ) * 1000 );
234 243 $last_seen = isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0;
235 244 $last_active = isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0;
236 245
@@ -240,9 +249,9 @@
240 249 * for this long, even if Heartbeat keeps firing.
241 250 *
242 251 * @param int $seconds
243 252 */
244 - $inactive_after = (int) apply_filters( 'desktop_mode_presence_inactive_after', 300 );
253 + $inactive_after = (int) apply_filters( 'openstation_presence_inactive_after', 300 );
245 254
246 255 /**
247 256 * Offline threshold (default 120s = 2 min). Inactive / online
248 257 * users transition to `offline` when the last Heartbeat is
@@ -249,9 +258,9 @@
249 258 * older than this.
250 259 *
251 260 * @param int $seconds
252 261 */
253 - $offline_after = (int) apply_filters( 'desktop_mode_presence_offline_after', 120 );
262 + $offline_after = (int) apply_filters( 'openstation_presence_offline_after', 120 );
254 263
255 264 if ( $now_ms - $last_seen > $offline_after * 1000 ) {
256 265 return 'offline';
257 266 }
@@ -266,12 +275,13 @@
266 275 *
267 276 * @param int $user_id
268 277 * @return string `online | inactive | offline`
269 278 */
270 -function desktop_mode_presence_status_for_user( $user_id ) {
271 - $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;
272 282 $record = isset( $all[ (int) $user_id ] ) ? $all[ (int) $user_id ] : array();
273 - return desktop_mode_presence_status_from_record( (array) $record );
283 + return openstation_presence_status_from_record( (array) $record );
274 284 }
275 285
276 286 /**
277 287 * Build a presence snapshot. With `$user_ids = null` returns every
@@ -284,10 +294,10 @@
284 294 *
285 295 * @param int[]|null $user_ids Restrict to these ids. `null` = all.
286 296 * @return array<string,array{ status:string, lastSeenMs:int, lastActiveMs:int }>
287 297 */
288 -function desktop_mode_presence_snapshot( $user_ids = null ) {
289 - $all = desktop_mode_presence_get_all();
298 +function openstation_presence_snapshot( $user_ids = null ) {
299 + $all = openstation_presence_get_all();
290 300 $out = array();
291 301
292 302 if ( null === $user_ids ) {
293 303 $ids = array_keys( $all );
@@ -301,11 +311,11 @@
301 311 }
302 312 }
303 313
304 314 foreach ( $ids as $uid ) {
305 - $record = isset( $all[ $uid ] ) ? $all[ $uid ] : array();
315 + $record = isset( $all[ $uid ] ) ? $all[ $uid ] : array();
306 316 $out[ (string) $uid ] = array(
307 - 'status' => desktop_mode_presence_status_from_record( $record ),
317 + 'status' => openstation_presence_status_from_record( $record ),
308 318 'lastSeenMs' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0,
309 319 'lastActiveMs' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0,
310 320 );
311 321 }
@@ -315,9 +325,9 @@
315 325 /**
316 326 * Filter a list of candidate user ids down to those a given viewer
317 327 * is allowed to see presence for. Defaults to passing the list
318 328 * through unchanged — plugins implementing per-team / per-role
319 - * privacy boundaries hook `desktop_mode_presence_visible_users`
329 + * privacy boundaries hook `openstation_presence_visible_users`
320 330 * (e.g., "subscribers can only see other subscribers' presence").
321 331 *
322 332 * @param int[] $candidate_user_ids
323 333 * @param int $viewer_id Defaults to the current user.
@@ -322,11 +332,14 @@
322 332 * @param int[] $candidate_user_ids
323 333 * @param int $viewer_id Defaults to the current user.
324 334 * @return int[]
325 335 */
326 -function desktop_mode_presence_visible_users( $candidate_user_ids, $viewer_id = 0 ) {
327 - $viewer_id = (int) $viewer_id ?: get_current_user_id();
328 - $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();
329 342 foreach ( (array) $candidate_user_ids as $uid ) {
330 343 $uid = (int) $uid;
331 344 if ( $uid > 0 ) {
332 345 $ids[] = $uid;
@@ -342,57 +355,53 @@
342 355 *
343 356 * @param int[] $ids Candidate user ids.
344 357 * @param int $viewer_id The user requesting visibility.
345 358 */
346 - return (array) apply_filters( 'desktop_mode_presence_visible_users', $ids, $viewer_id );
359 + return (array) apply_filters( 'openstation_presence_visible_users', $ids, $viewer_id );
347 360 }
348 361
349 362 /**
350 363 * Daily cron: prune presence entries for users idle >14 days.
351 - * Keeps the option compact even on long-running sites.
364 + * Deletes only rows still expired when the statement executes.
352 365 */
353 -function desktop_mode_presence_cron_prune() {
354 - $all = desktop_mode_presence_get_all();
355 - 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() ) {
356 370 return;
357 371 }
358 - $threshold = (int) round( microtime( true ) * 1000 ) - ( 14 * DAY_IN_SECONDS * 1000 );
359 - $pruned = array();
360 - foreach ( $all as $uid => $record ) {
361 - if ( ( (int) $record['last_seen_ms'] ) < $threshold ) {
362 - continue;
363 - }
364 - $pruned[ (int) $uid ] = $record;
365 - }
366 - if ( count( $pruned ) !== count( $all ) ) {
367 - update_option( DESKTOP_MODE_PRESENCE_OPTION, $pruned, false );
368 - }
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();
369 376 }
370 -add_action( 'desktop_mode_presence_daily_prune', 'desktop_mode_presence_cron_prune' );
377 +add_action( 'desktop_mode_presence_daily_prune', 'openstation_presence_cron_prune' );
371 378
372 379 /**
373 380 * Schedule the daily cron once. Idempotent.
374 381 */
375 -function desktop_mode_presence_schedule_cron() {
382 +function openstation_presence_schedule_cron() {
376 383 if ( ! wp_next_scheduled( 'desktop_mode_presence_daily_prune' ) ) {
377 384 wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'desktop_mode_presence_daily_prune' );
378 385 }
379 386 }
380 -add_action( 'init', 'desktop_mode_presence_schedule_cron', 50 );
387 +add_action( 'init', 'openstation_presence_schedule_cron', 50 );
381 388
382 -/* -------------------------------------------------------------------------
389 +/*
390 +-------------------------------------------------------------------------
383 391 * Heartbeat integration
384 - * ----------------------------------------------------------------------- */
392 + * -----------------------------------------------------------------------
393 + */
385 394
386 395 /**
387 - * Heartbeat handler — bumps presence on every tick a desktop-mode
396 + * Heartbeat handler — bumps presence on every tick a openstation
388 397 * user is on the page. Returns the visible-presence snapshot in
389 398 * the response so the client store can update without a separate
390 399 * REST round-trip.
391 400 *
392 - * Triggered by the client opting in via `desktop_mode_presence_active:
401 + * Triggered by the client opting in via `openstation_presence_active:
393 402 * true` in the heartbeat-send payload, with optional
394 - * `desktop_mode_user_active` (mousedown / keydown within the
403 + * `openstation_user_active` (mousedown / keydown within the
395 404 * inactive-threshold window).
396 405 *
397 406 * @param array $response Pre-filtered response.
398 407 * @param array $data Client-sent payload.
@@ -397,56 +406,59 @@
397 406 * @param array $response Pre-filtered response.
398 407 * @param array $data Client-sent payload.
399 408 * @return array
400 409 */
401 -function desktop_mode_presence_heartbeat_received( $response, $data ) {
410 +function openstation_presence_heartbeat_received( $response, $data ) {
402 411 if ( ! is_array( $response ) ) {
403 412 $response = array();
404 413 }
405 - if ( empty( $data['desktop_mode_presence_active'] ) ) {
414 + if ( empty( $data['openstation_presence_active'] ) ) {
406 415 return $response;
407 416 }
408 - if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
417 + if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
409 418 return $response;
410 419 }
411 420 $user_id = (int) get_current_user_id();
412 - $user_active = ! empty( $data['desktop_mode_user_active'] );
421 + $user_active = ! empty( $data['openstation_user_active'] );
413 422
414 - desktop_mode_presence_record( $user_id, $user_active );
423 + openstation_presence_migration_tick();
424 + openstation_presence_record( $user_id, $user_active );
415 425
416 426 // Snapshot the users this viewer is allowed to see — by default
417 427 // all tracked users; plugins can narrow via the
418 - // `desktop_mode_presence_visible_users` filter.
419 - $all_ids = array_keys( desktop_mode_presence_get_all() );
420 - $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 );
421 431
422 - $response['desktop_mode_presence'] = array(
423 - 'snapshot' => desktop_mode_presence_snapshot( $visible ),
432 + $response['openstation_presence'] = array(
433 + 'snapshot' => openstation_presence_snapshot( $visible ),
424 434 'serverTimeMs' => (int) round( microtime( true ) * 1000 ),
425 435 );
426 436 return $response;
427 437 }
428 -add_filter( 'heartbeat_received', 'desktop_mode_presence_heartbeat_received', 5, 2 );
438 +add_filter( 'heartbeat_received', 'openstation_presence_heartbeat_received', 5, 2 );
429 439
430 -/* -------------------------------------------------------------------------
440 +/*
441 +-------------------------------------------------------------------------
431 442 * REST endpoints
432 - * ----------------------------------------------------------------------- */
443 + * -----------------------------------------------------------------------
444 + */
433 445
434 446 /**
435 447 * Permission gate for presence endpoints — login required +
436 - * desktop mode enabled. Delegates to the shared
437 - * {@see desktop_mode_rest_require_enabled()} gate.
448 + * OpenStation enabled. Delegates to the shared
449 + * {@see openstation_rest_require_enabled()} gate.
438 450 *
439 451 * @return true|WP_Error
440 452 */
441 -function desktop_mode_presence_rest_permission() {
442 - return desktop_mode_rest_require_enabled();
453 +function openstation_presence_rest_permission() {
454 + return openstation_rest_require_enabled();
443 455 }
444 456
445 457 /**
446 458 * Register `/desktop-mode/v1/presence` routes.
447 459 */
448 -function desktop_mode_presence_register_rest_routes() {
460 +function openstation_presence_register_rest_routes() {
449 461 register_rest_route(
450 462 'desktop-mode/v1',
451 463 '/presence',
452 464 array(
@@ -451,15 +463,15 @@
451 463 '/presence',
452 464 array(
453 465 array(
454 466 'methods' => WP_REST_Server::READABLE,
455 - 'permission_callback' => 'desktop_mode_presence_rest_permission',
456 - 'callback' => 'desktop_mode_presence_rest_get',
467 + 'permission_callback' => 'openstation_presence_rest_permission',
468 + 'callback' => 'openstation_presence_rest_get',
457 469 ),
458 470 array(
459 471 'methods' => WP_REST_Server::CREATABLE,
460 - 'permission_callback' => 'desktop_mode_presence_rest_permission',
461 - 'callback' => 'desktop_mode_presence_rest_post',
472 + 'permission_callback' => 'openstation_presence_rest_permission',
473 + 'callback' => 'openstation_presence_rest_post',
462 474 'args' => array(
463 475 'active' => array( 'type' => 'boolean' ),
464 476 'inactive' => array( 'type' => 'boolean' ),
465 477 ),
@@ -466,21 +478,22 @@
466 478 ),
467 479 )
468 480 );
469 481 }
470 -add_action( 'rest_api_init', 'desktop_mode_presence_register_rest_routes' );
482 +add_action( 'rest_api_init', 'openstation_presence_register_rest_routes' );
471 483
472 484 /**
473 485 * GET /desktop-mode/v1/presence — current snapshot, narrowed by the
474 486 * visibility filter.
475 487 */
476 -function desktop_mode_presence_rest_get() {
488 +function openstation_presence_rest_get() {
489 + openstation_presence_migration_tick();
477 490 $viewer_id = (int) get_current_user_id();
478 - $all_ids = array_keys( desktop_mode_presence_get_all() );
479 - $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 );
480 493 return rest_ensure_response(
481 494 array(
482 - 'snapshot' => desktop_mode_presence_snapshot( $visible ),
495 + 'snapshot' => openstation_presence_snapshot( $visible ),
483 496 'serverTimeMs' => (int) round( microtime( true ) * 1000 ),
484 497 )
485 498 );
486 499 }
@@ -496,11 +509,12 @@
496 509 *
497 510 * Defaults to `{ active: true }` when neither flag is supplied —
498 511 * the simplest "I'm here" call.
499 512 */
500 -function desktop_mode_presence_rest_post( WP_REST_Request $request ) {
501 - $user_id = (int) get_current_user_id();
502 - $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' );
503 517 $inactive = (bool) $request->get_param( 'inactive' );
504 518
505 519 if ( $inactive ) {
506 520 // Set the user immediately to `inactive`: bump last_seen
@@ -505,27 +519,34 @@
505 519 if ( $inactive ) {
506 520 // Set the user immediately to `inactive`: bump last_seen
507 521 // (still alive) but force last_active to zero (no recent
508 522 // interaction).
509 - $all = desktop_mode_presence_get_all();
510 - $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(
511 528 'last_seen_ms' => 0,
512 529 'last_active_ms' => 0,
513 530 );
514 - $prev_status = desktop_mode_presence_status_from_record( $rec );
531 + $prev_status = openstation_presence_status_from_record( $rec );
515 532 $rec['last_seen_ms'] = (int) round( microtime( true ) * 1000 );
516 533 $rec['last_active_ms'] = 0;
517 - $all[ $user_id ] = $rec;
518 - 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 + }
519 537
520 - $next_status = desktop_mode_presence_status_from_record( $rec );
521 - 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 );
522 540 if ( $next_status !== $prev_status ) {
523 - do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status );
541 + do_action( 'openstation_presence_changed', $user_id, $next_status, $prev_status );
524 542 }
525 543 } else {
526 - $flag = ( null === $active ) ? true : (bool) $active;
527 - 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 + }
528 549 }
529 550
530 551 return rest_ensure_response( array( 'ok' => true ) );
531 552 }