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