Skip to main content

koan_server/
clients.rs

1//! koan clients linked to this server, and the way to reach them.
2//!
3//! A client that syncs from this server holds a WebSocket open at
4//! `/rest/koanLink` (see `koan_core::remote::link`). Each is registered here
5//! under the account it signed in as, so GraphQL (and MCP through it) can hand
6//! one a list of tracks to play: build a playlist on the server, hear it on a
7//! phone.
8
9use std::sync::LazyLock;
10
11use koan_core::db::queries::{self, UidKind};
12use koan_core::remote::acks::{AckOutcome, Envelope};
13use koan_core::remote::link::{LinkCommand, LinkDevice, LinkState};
14use outbox::Absent;
15use parking_lot::Mutex;
16use tokio::sync::mpsc::UnboundedSender;
17
18/// A linked client, as listed.
19#[derive(Debug, Clone)]
20pub struct ClientInfo {
21    pub id: String,
22    /// The device's own id, stable across its reconnects.
23    pub device: String,
24    pub name: String,
25    pub platform: String,
26    pub username: String,
27    /// Unix seconds.
28    pub connected_at: i64,
29    /// What the client last said it was doing.
30    pub state: LinkState,
31    /// Unix seconds; when it was last seen playing, if ever since linking.
32    pub last_played_at: Option<i64>,
33    /// When `state` was reported, in Unix milliseconds.
34    pub state_at: i64,
35    /// Whether the client has reported its state at all. An app older than
36    /// the reports never does, and its `state` then says nothing about it.
37    pub reports: bool,
38    /// Reached by a notification it shows rather than over its link: iOS had
39    /// suspended it. What was sent runs when someone taps the notification.
40    pub notified: bool,
41    /// It answers commands sent with an id: see `koan_core::remote::acks`.
42    pub acks: bool,
43}
44
45impl ClientInfo {
46    /// Where the playhead is now, from where it was reported to be.
47    pub fn position_ms(&self) -> u64 {
48        let pos = self.state.position_ms;
49        if !self.state.playing {
50            return pos;
51        }
52        let run = (chrono::Utc::now().timestamp_millis() - self.state_at).max(0) as u64;
53        let pos = pos + run;
54        if self.state.duration_ms > 0 {
55            pos.min(self.state.duration_ms)
56        } else {
57            pos
58        }
59    }
60}
61
62struct Entry {
63    info: ClientInfo,
64    /// The client's own id for itself, so a reconnect replaces its entry.
65    device: String,
66    tx: UnboundedSender<Envelope>,
67    /// Sent the account's other devices whenever one changes. Asked for by
68    /// the client; one that predates them would log each as a bad command.
69    wants_devices: bool,
70    /// Sent `LinkCommand::DeviceKeys`: it registered a key of its own, so it
71    /// knows the command.
72    wants_keys: bool,
73}
74
75/// A command relayed with an id, and what to do with the answer: `None` when
76/// the device it went to gives none.
77pub struct Acking {
78    pub id: u64,
79    pub reply: Box<dyn FnOnce(Option<AckOutcome>) + Send>,
80}
81
82/// An answer the server is waiting for.
83struct Waiting {
84    reply: Box<dyn FnOnce(Option<AckOutcome>) + Send>,
85    /// The device said the command came off its link.
86    received: bool,
87    /// The timer that looks at it, stopped once the answer is in, so an
88    /// answered command costs nothing more.
89    timer: Option<tokio::task::AbortHandle>,
90}
91
92impl Waiting {
93    fn answer(mut self, outcome: Option<AckOutcome>) {
94        let reply = std::mem::replace(&mut self.reply, Box::new(|_| {}));
95        reply(outcome);
96    }
97}
98
99impl Drop for Waiting {
100    fn drop(&mut self) {
101        if let Some(timer) = self.timer.take() {
102            timer.abort();
103        }
104    }
105}
106
107/// How long a linked device has to say a command came off its link before it
108/// is reached as one that is away; and how long an answer to one it did take
109/// is waited for, however long the command runs, before the asker is told
110/// only that it arrived.
111const FIRST_LOOK: std::time::Duration = std::time::Duration::from_millis(2500);
112const LONGEST_ANSWER: std::time::Duration = std::time::Duration::from_secs(120);
113
114/// A Live Activity on a phone showing another device, and where to push its
115/// updates.
116struct Activity {
117    username: String,
118    /// The phone showing it.
119    watcher: String,
120    /// The device it shows.
121    target: String,
122    token: String,
123    sandbox: bool,
124    /// What it was last sent, so only a change goes out: Apple budgets these.
125    sent: Option<crate::push::ActivityState>,
126}
127
128/// "When this album is in the library, queue it on my device": a request made
129/// before the album exists, fulfilled by the scan that finds it.
130#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
131pub struct Order {
132    pub id: String,
133    /// Whose devices it may go to.
134    pub username: Option<String>,
135    /// A client id or name; `None` for whichever `send` would pick then.
136    pub client: Option<String>,
137    pub artist: String,
138    pub album: String,
139    /// Insert after the current track rather than at the end.
140    pub play_next: bool,
141    /// Add to this playlist rather than a device's queue.
142    #[serde(default)]
143    pub playlist: Option<i64>,
144    /// Only these tracks of the album (title substrings, in this order);
145    /// empty for all of it.
146    #[serde(default)]
147    pub titles: Vec<String>,
148    /// Unix seconds.
149    pub created_at: i64,
150}
151
152/// An order nobody's scan has fulfilled in this long is dropped.
153const ORDER_TTL: i64 = 24 * 60 * 60;
154
155/// The owner of `device` lets the account `grantee` control it: playback and
156/// the queue, as a device on the same network may, and nothing of the
157/// owner's account.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct Grant {
160    pub device: String,
161    pub owner: String,
162    pub grantee: String,
163}
164
165#[derive(Default)]
166pub struct Registry {
167    entries: Mutex<Vec<Entry>>,
168    orders: Mutex<Vec<Order>>,
169    activities: Mutex<Vec<Activity>>,
170    grants: Mutex<Vec<Grant>>,
171    /// Which devices have another's playing bars on screen. Held in memory
172    /// only, and forgotten with the watcher's link.
173    level_watches: Mutex<Vec<LevelWatch>>,
174    /// Where each device last linked from, by device id: its account and
175    /// address. Kept in `link_devices` too, so a restart keeps it.
176    addresses: Mutex<std::collections::HashMap<String, (String, std::net::IpAddr)>>,
177    /// Answers awaited, by the account and device asked and the command's
178    /// id. A device id is the client's own choice, so another account's
179    /// device can claim one of this account's: only a link of the account the
180    /// command went to can answer it.
181    answers: Mutex<std::collections::HashMap<(String, String, u64), Waiting>>,
182}
183
184/// `watcher`, a device of `watcher_user`, has `target`'s playing bars on
185/// screen; `target` is `owner`'s. The two accounts differ for a device shared
186/// with the watcher's.
187struct LevelWatch {
188    owner: String,
189    watcher_user: String,
190    watcher: String,
191    target: String,
192}
193
194/// One registry per process: the WebSocket route and the GraphQL schema are
195/// built in different places and both need it.
196pub fn registry() -> &'static Registry {
197    static REGISTRY: LazyLock<Registry> = LazyLock::new(|| {
198        let registry = Registry::default();
199        *registry.orders.lock() = outbox::load_orders();
200        *registry.grants.lock() = outbox::load_grants();
201        *registry.addresses.lock() = outbox::load_addresses();
202        registry
203    });
204    &REGISTRY
205}
206
207impl Registry {
208    /// Add a client, replacing any earlier connection from the same device
209    /// and account. Returns its id.
210    #[allow(clippy::too_many_arguments)]
211    pub fn register(
212        &self,
213        username: &str,
214        name: &str,
215        platform: &str,
216        device: &str,
217        tx: UnboundedSender<Envelope>,
218        wants_devices: bool,
219        acks: bool,
220    ) -> String {
221        let id = uuid::Uuid::now_v7().to_string();
222        if let Some((sent, how)) = WOKEN
223            .lock()
224            .remove(&(device.to_string(), username.to_string()))
225        {
226            log::info!(
227                "wake: {name} linked {}ms after its {how}",
228                sent.elapsed().as_millis()
229            );
230        }
231        // What waited for this device while it was away goes down the new
232        // link first.
233        let live = self.live();
234        for envelope in outbox::take_and_remember(username, device, name, platform, &live) {
235            let _ = tx.send(envelope);
236        }
237        let mut entries = self.entries.lock();
238        entries.retain(|e| !(e.device == device && e.info.username == username));
239        entries.push(Entry {
240            info: ClientInfo {
241                id: id.clone(),
242                device: device.to_string(),
243                name: name.to_string(),
244                platform: platform.to_string(),
245                username: username.to_string(),
246                connected_at: chrono::Utc::now().timestamp(),
247                state: LinkState::default(),
248                last_played_at: None,
249                state_at: chrono::Utc::now().timestamp_millis(),
250                reports: false,
251                notified: false,
252                acks,
253            },
254            device: device.to_string(),
255            tx,
256            wants_devices,
257            wants_keys: false,
258        });
259        drop(entries);
260        // Always, empty or not: an app signed in elsewhere before holds that
261        // server's list.
262        self.send_shares(username, device, None);
263        self.announce(username);
264        // A device that relinked has a new session, which holds no watch:
265        // tell it again that it is watched, if it is.
266        if self
267            .level_watches
268            .lock()
269            .iter()
270            .any(|w| w.owner == username && w.target == device)
271        {
272            self.send_live(username, device, LinkCommand::WatchLevels { on: true });
273        }
274        id
275    }
276
277    /// Send the link `id` the account's device keys from now on.
278    pub fn wants_keys(&self, id: &str) {
279        if let Some(e) = self.entries.lock().iter_mut().find(|e| e.info.id == id) {
280            e.wants_keys = true;
281        }
282    }
283
284    /// Hand `username`'s links that take them the device keys they check the
285    /// local network against.
286    pub fn publish_keys(&self, username: &str, keys: Vec<koan_core::remote::link::LinkDeviceKey>) {
287        for e in self
288            .entries
289            .lock()
290            .iter()
291            .filter(|e| e.info.username == username && e.wants_keys)
292        {
293            let _ =
294                e.tx.send(LinkCommand::DeviceKeys { keys: keys.clone() }.into());
295        }
296    }
297
298    /// Every account with a link that takes device keys.
299    pub fn keyed_accounts(&self) -> Vec<String> {
300        let mut accounts: Vec<String> = self
301            .entries
302            .lock()
303            .iter()
304            .filter(|e| e.wants_keys)
305            .map(|e| e.info.username.clone())
306            .collect();
307        accounts.sort();
308        accounts.dedup();
309        accounts
310    }
311
312    /// The accounts `owner` shares `device` with.
313    pub fn grantees_of(&self, owner: &str, device: &str) -> Vec<String> {
314        self.grants
315            .lock()
316            .iter()
317            .filter(|g| g.owner == owner && g.device == device)
318            .map(|g| g.grantee.clone())
319            .collect()
320    }
321
322    /// Record what a client says it is doing.
323    pub fn report(&self, id: &str, state: LinkState) {
324        let mut entries = self.entries.lock();
325        if let Some(e) = entries.iter_mut().find(|e| e.info.id == id) {
326            if state.playing || e.info.state.playing {
327                e.info.last_played_at = Some(chrono::Utc::now().timestamp());
328            }
329            e.info.state = state;
330            e.info.state_at = chrono::Utc::now().timestamp_millis();
331            e.info.reports = true;
332            let (username, device) = (e.info.username.clone(), e.device.clone());
333            drop(entries);
334            self.announce(&username);
335            self.update_activities(&username, &device);
336        }
337    }
338
339    /// Record where Apple's push service reaches this device.
340    pub fn set_push(&self, username: &str, device: &str, token: &str, sandbox: bool) {
341        outbox::save_push(username, device, token, sandbox);
342    }
343
344    /// Close every link `username` has open. Dropping an entry's sender ends
345    /// its session, and the client must then sign in again to reconnect.
346    pub fn disconnect(&self, username: &str) {
347        self.entries.lock().retain(|e| e.info.username != username);
348    }
349
350    pub fn unregister(&self, id: &str) {
351        let mut entries = self.entries.lock();
352        let gone = entries
353            .iter()
354            .find(|e| e.info.id == id)
355            .map(|e| (e.device.clone(), e.info.username.clone()));
356        entries.retain(|e| e.info.id != id);
357        drop(entries);
358        if let Some((device, username)) = gone {
359            outbox::touch(&[(device.clone(), username.clone())]);
360            self.announce(&username);
361            // A controller that went stops watching whatever it watched.
362            let targets: Vec<String> = self
363                .level_watches
364                .lock()
365                .iter()
366                .filter(|w| w.watcher_user == username && w.watcher == device)
367                .map(|w| w.target.clone())
368                .collect();
369            for target in targets {
370                self.watch_levels(&username, &device, &target, false);
371            }
372        }
373    }
374
375    /// `watcher` has `target`'s playing bars on screen, or no longer has. The
376    /// target is told to send its levels while anyone watches, and to stop
377    /// when the last one goes. Only over a live link: levels are no reason to
378    /// wake a phone or to queue a command for later.
379    pub fn watch_levels(&self, username: &str, watcher: &str, target: &str, on: bool) {
380        // A device shared with `username` is watched as its owner's.
381        let owner = self
382            .shared_owner(username, target)
383            .unwrap_or_else(|| username.to_string());
384        let mut watches = self.level_watches.lock();
385        watches.retain(|w| {
386            !(w.watcher_user == username && w.watcher == watcher && w.target == target)
387        });
388        if on {
389            watches.push(LevelWatch {
390                owner: owner.clone(),
391                watcher_user: username.into(),
392                watcher: watcher.into(),
393                target: target.into(),
394            });
395        }
396        let watched = watches
397            .iter()
398            .any(|w| w.owner == owner && w.target == target);
399        drop(watches);
400        if on || !watched {
401            self.send_live(&owner, target, LinkCommand::WatchLevels { on: watched });
402        }
403    }
404
405    /// A frame of `from`'s levels, for each device watching it. Not stored:
406    /// one that cannot be delivered now is of no use later.
407    pub fn levels(&self, username: &str, from: &str, f: koan_core::remote::levels::Frame) {
408        let watchers: Vec<(String, String)> = self
409            .level_watches
410            .lock()
411            .iter()
412            .filter(|w| w.owner == username && w.target == from)
413            .map(|w| (w.watcher_user.clone(), w.watcher.clone()))
414            .collect();
415        for (watcher_user, watcher) in watchers {
416            self.send_live(
417                &watcher_user,
418                &watcher,
419                LinkCommand::Levels {
420                    from: from.to_string(),
421                    f,
422                },
423            );
424        }
425    }
426
427    /// Send `cmd` to `device` if it is linked now, and nowhere else.
428    pub(crate) fn send_live(&self, username: &str, device: &str, cmd: LinkCommand) {
429        if let Some(e) = self
430            .entries
431            .lock()
432            .iter()
433            .find(|e| e.info.username == username && e.device == device)
434        {
435            let _ = e.tx.send(cmd.into());
436        }
437    }
438
439    /// Send each of `username`'s links that asked for them the account's
440    /// other devices: those linked, with what each is doing, and those a push
441    /// can wake.
442    fn announce(&self, username: &str) {
443        self.announce_to(username);
444        // Those `username` shares a device with see it change too.
445        let grantees: Vec<String> = {
446            let mut g: Vec<String> = self
447                .grants
448                .lock()
449                .iter()
450                .filter(|g| g.owner == username)
451                .map(|g| g.grantee.clone())
452                .collect();
453            g.sort();
454            g.dedup();
455            g
456        };
457        for grantee in grantees {
458            self.announce_to(&grantee);
459        }
460    }
461
462    /// `announce` for `username` alone.
463    fn announce_to(&self, username: &str) {
464        let listening = |e: &Entry| e.info.username == username && e.wants_devices;
465        if !self.entries.lock().iter().any(listening) {
466            return;
467        }
468        let asleep = outbox::push_targets(Some(username));
469        // Waking takes a push key as well as the device's token.
470        let can_push = crate::push::pusher().is_some();
471        let entries = self.entries.lock();
472        let ours: Vec<&Entry> = entries
473            .iter()
474            .filter(|e| e.info.username == username)
475            .collect();
476        if !ours.iter().any(|e| e.wants_devices) {
477            return;
478        }
479        let mut all: Vec<LinkDevice> = ours
480            .iter()
481            .map(|e| LinkDevice {
482                id: e.device.clone(),
483                name: e.info.name.clone(),
484                platform: e.info.platform.clone(),
485                linked: true,
486                state: e.info.reports.then(|| LinkState {
487                    position_ms: e.info.position_ms(),
488                    ..e.info.state.clone()
489                }),
490                last_seen: None,
491                wakeable: Some(can_push && asleep.iter().any(|t| t.device == e.device)),
492                owner: None,
493                acks: e.info.acks,
494            })
495            .collect();
496        for t in asleep {
497            if !all.iter().any(|d| d.id == t.device) {
498                all.push(LinkDevice {
499                    id: t.device,
500                    name: t.name,
501                    platform: t.platform,
502                    linked: false,
503                    state: None,
504                    last_seen: Some(t.last_seen),
505                    wakeable: Some(can_push),
506                    owner: None,
507                    acks: false,
508                });
509            }
510        }
511        all.extend(self.shared_devices(username, &entries, can_push));
512        for e in ours.iter().filter(|e| e.wants_devices) {
513            let devices = all.iter().filter(|d| d.id != e.device).cloned().collect();
514            let _ = e.tx.send(LinkCommand::Devices { devices }.into());
515        }
516    }
517
518    /// The devices shared with `grantee`, as `grantee` sees them: their
519    /// state, with nothing of their owner's own (the outputs are the owner's
520    /// to choose); and absent ones only where a push can wake them.
521    fn shared_devices(&self, grantee: &str, entries: &[Entry], can_push: bool) -> Vec<LinkDevice> {
522        let grants: Vec<Grant> = self
523            .grants
524            .lock()
525            .iter()
526            .filter(|g| g.grantee == grantee)
527            .cloned()
528            .collect();
529        let mut out = Vec::new();
530        for g in grants {
531            let linked = entries
532                .iter()
533                .find(|e| e.device == g.device && e.info.username == g.owner);
534            match linked {
535                Some(e) => out.push(LinkDevice {
536                    id: e.device.clone(),
537                    name: e.info.name.clone(),
538                    platform: e.info.platform.clone(),
539                    linked: true,
540                    state: e.info.reports.then(|| LinkState {
541                        position_ms: e.info.position_ms(),
542                        ..e.info.state.clone()
543                    }),
544                    last_seen: None,
545                    wakeable: Some(can_push),
546                    owner: Some(g.owner.clone()),
547                    acks: e.info.acks,
548                }),
549                None => {
550                    if let Some(t) = outbox::push_targets(Some(&g.owner))
551                        .into_iter()
552                        .find(|t| t.device == g.device)
553                    {
554                        out.push(LinkDevice {
555                            id: t.device,
556                            name: t.name,
557                            platform: t.platform,
558                            linked: false,
559                            state: None,
560                            last_seen: Some(t.last_seen),
561                            wakeable: Some(can_push),
562                            owner: Some(g.owner.clone()),
563                            acks: false,
564                        });
565                    }
566                }
567            }
568        }
569        out
570    }
571
572    /// Let `grantee` control `owner`'s device `device`, or stop letting it.
573    /// Asked by the device itself; the caller has checked `grantee` is an
574    /// account on this server.
575    pub fn share(
576        &self,
577        owner: &str,
578        device: &str,
579        grantee: &str,
580        allow: bool,
581    ) -> Result<(), String> {
582        if grantee == owner {
583            return Err("a device is already its own account's".into());
584        }
585        let grant = Grant {
586            device: device.to_string(),
587            owner: owner.to_string(),
588            grantee: grantee.to_string(),
589        };
590        {
591            let mut grants = self.grants.lock();
592            grants.retain(|g| *g != grant);
593            if allow {
594                grants.push(grant.clone());
595            }
596        }
597        if allow {
598            outbox::save_grant(&grant);
599        } else {
600            outbox::drop_grant(&grant);
601        }
602        log::info!(
603            "share: {owner}'s {device} {} {grantee}",
604            if allow {
605                "shared with"
606            } else {
607                "no longer shared with"
608            }
609        );
610        self.send_shares(owner, device, None);
611        self.announce_to(grantee);
612        Ok(())
613    }
614
615    /// Tell `owner`'s device `device` whom it is shared with, and why its last
616    /// request was refused, if it was.
617    pub fn send_shares(&self, owner: &str, device: &str, error: Option<String>) {
618        let mut grantees: Vec<String> = self
619            .grants
620            .lock()
621            .iter()
622            .filter(|g| g.owner == owner && g.device == device)
623            .map(|g| g.grantee.clone())
624            .collect();
625        grantees.sort();
626        if let Some(e) = self
627            .entries
628            .lock()
629            .iter()
630            .find(|e| e.info.username == owner && e.device == device && e.wants_devices)
631        {
632            let accounts = outbox::accounts()
633                .into_iter()
634                .filter(|a| a != owner)
635                .collect();
636            let _ = e.tx.send(Envelope::from(LinkCommand::Shares {
637                grantees,
638                error,
639                accounts,
640            }));
641        }
642    }
643
644    /// `device`, of `username`'s, linked from `addr`.
645    pub fn seen_at(&self, device: &str, username: &str, addr: std::net::IpAddr) {
646        self.addresses
647            .lock()
648            .insert(device.to_string(), (username.to_string(), addr));
649        outbox::save_address(device, username, addr);
650    }
651
652    /// Whose push token wakes `to` when `username`'s device `from` asks: the
653    /// owner of a device shared with `username`; the account of one last seen
654    /// at the same address as `from` is now, which is to say behind the same
655    /// router, a household's; else `username`'s own. A wake grants nothing
656    /// else: what may then be done to the device follows the share or the
657    /// network as ever.
658    fn wake_owner(&self, username: &str, from: &str, to: &str) -> String {
659        if let Some(owner) = self.shared_owner(username, to) {
660            return owner;
661        }
662        let addresses = self.addresses.lock();
663        let here = addresses
664            .get(from)
665            .filter(|(user, _)| user == username)
666            .map(|(_, addr)| *addr);
667        match (here, addresses.get(to)) {
668            (Some(here), Some((owner, there))) if *there == here => owner.clone(),
669            _ => username.to_string(),
670        }
671    }
672
673    /// The account `to` belongs to, when `from`, a device of `owner`'s, is
674    /// shared with it.
675    fn grantee_of(&self, owner: &str, from: &str, to: &str) -> Option<String> {
676        let to_user = self
677            .entries
678            .lock()
679            .iter()
680            .find(|e| e.device == to && e.info.username != owner)
681            .map(|e| e.info.username.clone())?;
682        self.grants
683            .lock()
684            .iter()
685            .any(|g| g.owner == owner && g.device == from && g.grantee == to_user)
686            .then_some(to_user)
687    }
688
689    /// Whose device `to` is, when it is not `username`'s own but shared with
690    /// it: the owner.
691    fn shared_owner(&self, username: &str, to: &str) -> Option<String> {
692        self.grants
693            .lock()
694            .iter()
695            .find(|g| g.grantee == username && g.device == to)
696            .map(|g| g.owner.clone())
697    }
698
699    /// Wake `to`, one of `username`'s devices that is not linked, for `from`
700    /// (a device id), which chose it: with a background push, or with
701    /// `notify` a notification asking to be tapped. Logged with the push's
702    /// round trip, and remembered so the link that follows is logged with how
703    /// long it took.
704    pub fn wake(&self, username: &str, from: &str, to: &str, notify: bool) {
705        let from_name = self
706            .list(Some(username))
707            .into_iter()
708            .find(|c| c.device == from)
709            .map_or_else(|| "Another device".to_string(), |c| c.name);
710        let owner = self.wake_owner(username, from, to);
711        if owner != username {
712            log::info!("wake: {to} is {owner}'s, woken for {username}");
713        }
714        let username = &owner;
715        if self.list(Some(username)).iter().any(|c| c.device == to) {
716            log::info!("wake: {to} is already linked");
717            return;
718        }
719        let Some(pusher) = crate::push::pusher() else {
720            log::info!("wake: no push key, so {to} cannot be woken");
721            return;
722        };
723        let Some(target) = outbox::push_targets(Some(username))
724            .into_iter()
725            .find(|t| t.device == to)
726        else {
727            log::info!("wake: {to} has given no push token");
728            return;
729        };
730        let (push, how) = if notify {
731            (
732                crate::push::Push::Summon {
733                    title: format!("{from_name} wants to play here"),
734                    body: "Tap to open kōan and let it.".into(),
735                },
736                "notification",
737            )
738        } else {
739            (crate::push::Push::WakeNow, "wake push")
740        };
741        WOKEN.lock().insert(
742            (target.device.clone(), target.username.clone()),
743            (std::time::Instant::now(), how),
744        );
745        std::thread::spawn(move || {
746            let started = std::time::Instant::now();
747            deliver_push(pusher, &target, &push);
748            log::info!(
749                "wake: {how} for {} answered by APNs in {}ms",
750                target.name,
751                started.elapsed().as_millis()
752            );
753        });
754    }
755
756    /// Forget `device`, one of `username`'s that is not linked: what the
757    /// server keeps of it, its push token included, so nothing is pushed to
758    /// it again; and every linked device of the account drops it. Another
759    /// account's device is out of reach of this, by construction: every
760    /// record is keyed by account. A device that links again is recorded
761    /// afresh.
762    pub fn forget(&self, username: &str, device: &str) -> Result<(), String> {
763        // Another account's device, shared with this one: forgetting it
764        // declines the share, or the next list would bring it straight back.
765        if let Some(owner) = self.shared_owner(username, device) {
766            return self.share(&owner, device, username, false);
767        }
768        if self.list(Some(username)).iter().any(|c| c.device == device) {
769            return Err(format!("{device} is linked; it would be back at once"));
770        }
771        outbox::forget_device(device, username);
772        log::info!("devices: {username} forgot {device}");
773        self.broadcast(
774            Some(username),
775            LinkCommand::Forgotten {
776                device: device.to_string(),
777            },
778        );
779        self.announce(username);
780        Ok(())
781    }
782
783    /// Relay `command` from one of `username`'s devices to another, `to`.
784    pub fn relay(
785        &self,
786        username: &str,
787        to: &str,
788        command: LinkCommand,
789    ) -> Result<ClientInfo, String> {
790        self.relay_from(username, None, to, command)
791    }
792
793    /// `relay`, knowing which of `username`'s devices, `from`, sent it: what
794    /// lets a device shared with another account hand its music to that
795    /// account's device, the one way a grant runs backwards.
796    pub fn relay_from(
797        &self,
798        username: &str,
799        from: Option<&str>,
800        to: &str,
801        command: LinkCommand,
802    ) -> Result<ClientInfo, String> {
803        self.relay_acked(username, from, to, command, None)
804    }
805
806    /// `relay_from`, with the device's answer handed to `acking`'s reply.
807    pub fn relay_acked(
808        &self,
809        username: &str,
810        from: Option<&str>,
811        to: &str,
812        command: LinkCommand,
813        acking: Option<Acking>,
814    ) -> Result<ClientInfo, String> {
815        // What one device may have another do, and nothing the server says
816        // itself: relayed, a forged device list or key list would reach the
817        // device as the server's own.
818        if !command.relayable() {
819            return Err("not a command".into());
820        }
821        // A device shared with `username` takes the playback set, marked as
822        // another account's so it runs as that account's request and never
823        // with its owner's powers.
824        if let Some(owner) = self.shared_owner(username, to) {
825            if !command.allowed_playback() {
826                return Err(format!("{to} is shared for playback only"));
827            }
828            let command = LinkCommand::Shared {
829                command: Box::new(command),
830            };
831            return self.send_with(Some(&owner), Some(to), command, acking);
832        }
833        // The other way: a device shared with `to`'s account hands its music
834        // there, as "Move here" from that account asks it to. That and only
835        // that: a grant does not let the owner's device command the grantee's.
836        if let Some(from) = from
837            && let Some(grantee) = self.grantee_of(username, from, to)
838        {
839            if !matches!(command, LinkCommand::Play { handoff: true, .. }) {
840                return Err(format!("{to} is another account's"));
841            }
842            let command = LinkCommand::Shared {
843                command: Box::new(command),
844            };
845            return self.send_with(Some(&grantee), Some(to), command, acking);
846        }
847        self.send_with(Some(username), Some(to), command, acking)
848    }
849
850    /// Where to push a Live Activity's updates: `watcher` shows `target`.
851    /// `None` ends it.
852    pub fn set_activity(
853        &self,
854        username: &str,
855        watcher: &str,
856        activity: Option<(String, String, bool)>,
857    ) {
858        let mut activities = self.activities.lock();
859        activities.retain(|a| !(a.username == username && a.watcher == watcher));
860        let Some((token, target, sandbox)) = activity else {
861            return;
862        };
863        activities.push(Activity {
864            username: username.to_string(),
865            watcher: watcher.to_string(),
866            target: target.clone(),
867            token,
868            sandbox,
869            sent: None,
870        });
871        drop(activities);
872        self.update_activities(username, &target);
873    }
874
875    /// Push `target`'s state to every Live Activity showing it, where it has
876    /// changed in a way the activity shows.
877    fn update_activities(&self, username: &str, target: &str) {
878        let Some(pusher) = crate::push::pusher() else {
879            return;
880        };
881        // A device shared with `username` is its owner's to look up, and the
882        // owner's reports reach those it is shared with too.
883        let owner = self
884            .shared_owner(username, target)
885            .unwrap_or_else(|| username.to_string());
886        let Some(info) = self
887            .list(Some(&owner))
888            .into_iter()
889            .find(|c| c.device == target)
890        else {
891            return;
892        };
893        let watchers: Vec<String> = self
894            .grants
895            .lock()
896            .iter()
897            .filter(|g| g.owner == owner && g.device == target)
898            .map(|g| g.grantee.clone())
899            .chain(std::iter::once(owner.clone()))
900            .collect();
901        let state = crate::push::ActivityState::of(&info);
902        let mut due = Vec::new();
903        for a in self.activities.lock().iter_mut() {
904            if watchers.contains(&a.username)
905                && a.target == target
906                && a.sent.as_ref().is_none_or(|s| s.differs(&state))
907            {
908                a.sent = Some(state.clone());
909                due.push((
910                    a.token.clone(),
911                    a.sandbox,
912                    a.watcher.clone(),
913                    a.username.clone(),
914                ));
915            }
916        }
917        if due.is_empty() {
918            return;
919        }
920        std::thread::spawn(move || {
921            for (token, sandbox, watcher, username) in due {
922                let push = crate::push::Push::Activity(state.clone());
923                match pusher.send(&token, sandbox, &push) {
924                    crate::push::Outcome::Sent => {}
925                    crate::push::Outcome::Gone => {
926                        log::info!("push: a Live Activity on {watcher} has ended");
927                        registry().set_activity(&username, &watcher, None);
928                    }
929                    crate::push::Outcome::Failed(e) => {
930                        log::warn!("push: Live Activity on {watcher}: {e}");
931                    }
932                }
933            }
934        });
935    }
936
937    /// Clients `username` may command, newest first; every client for `None`.
938    pub fn list(&self, username: Option<&str>) -> Vec<ClientInfo> {
939        let mut out: Vec<ClientInfo> = self
940            .entries
941            .lock()
942            .iter()
943            .filter(|e| username.is_none_or(|u| e.info.username == u))
944            .map(|e| e.info.clone())
945            .collect();
946        out.sort_by_key(|c| std::cmp::Reverse(c.connected_at));
947        out
948    }
949
950    /// Send to `id` (an id or a name), or with none to the client the
951    /// command most likely means: the one playing, else the one that played
952    /// within `RECENT`, else the only one linked. `Err` names the choices when
953    /// there is no telling, so whoever asked can ask the person.
954    pub fn send(
955        &self,
956        username: Option<&str>,
957        id: Option<&str>,
958        cmd: LinkCommand,
959    ) -> Result<ClientInfo, String> {
960        self.send_with(username, id, cmd, None)
961    }
962
963    /// `send`, and with `acking` the device's answer handed to its `reply`.
964    /// A device that is away is answered for: `Queued`, once it has been woken
965    /// to take the command. One whose link has gone quiet since the command
966    /// was sent is taken for away, and woken; one that is only slow is given
967    /// longer. One that gives no answers is answered `None` at once.
968    pub fn send_with(
969        &self,
970        username: Option<&str>,
971        id: Option<&str>,
972        cmd: LinkCommand,
973        acking: Option<Acking>,
974    ) -> Result<ClientInfo, String> {
975        let clients = self.list(username);
976        let target = match id {
977            Some(id) => clients
978                .iter()
979                .find(|c| c.id == id || c.device == id || c.name.eq_ignore_ascii_case(id)),
980            None if clients.is_empty() => None,
981            None => Some(pick(&clients, chrono::Utc::now().timestamp())?),
982        };
983        // Not linked: a phone iOS has suspended is woken to take it.
984        let Some(target) = target else {
985            let reached = reach_absent(username, id, &cmd, acking.as_ref().map(|a| a.id))
986                .unwrap_or_else(|| {
987                    Err(match id {
988                        Some(id) => format!("no linked client {id}; see `clients`"),
989                        None => {
990                            "no koan app is linked to this server; open koan on the device".into()
991                        }
992                    })
993                });
994            if let (Ok(_), Some(acking)) = (&reached, acking) {
995                (acking.reply)(Some(AckOutcome::Queued));
996            }
997            return reached;
998        };
999        let target = target.clone();
1000        let entries = self.entries.lock();
1001        let entry = entries
1002            .iter()
1003            .find(|e| e.info.id == target.id)
1004            .ok_or("that client has just gone")?;
1005        let (account, device) = (entry.info.username.clone(), entry.device.clone());
1006        // Listened for before it is sent, so a quick answer is not missed.
1007        // Nothing is answered while the entries are held: an answer may relay
1008        // to another link.
1009        let (ack, unanswered) = match acking {
1010            Some(acking) if entry.info.acks => {
1011                self.answers.lock().insert(
1012                    (account.clone(), device.clone(), acking.id),
1013                    Waiting {
1014                        reply: acking.reply,
1015                        received: false,
1016                        timer: None,
1017                    },
1018                );
1019                (Some(acking.id), None)
1020            }
1021            other => (None, other),
1022        };
1023        let sent = entry.tx.send(Envelope {
1024            command: cmd.clone(),
1025            ack,
1026        });
1027        drop(entries);
1028        if sent.is_err() {
1029            if let Some(w) = ack.and_then(|ack| self.answers.lock().remove(&(account, device, ack)))
1030            {
1031                w.answer(Some(AckOutcome::Failed {
1032                    error: "its link has just gone".into(),
1033                }));
1034            }
1035            return Err("that client has just gone".to_string());
1036        }
1037        if let Some(acking) = unanswered {
1038            (acking.reply)(None);
1039        }
1040        if let Some(ack) = ack {
1041            self.watch(account, device, ack, cmd);
1042        }
1043        Ok(target)
1044    }
1045
1046    /// Look at the command sent to `device` under `ack` after `FIRST_LOOK`,
1047    /// and give up on its answer after `LONGEST_ANSWER`: a timer on the
1048    /// server's runtime, kept with the waiting answer and stopped when the
1049    /// answer comes. Outside a runtime (a test) nothing looks.
1050    fn watch(&self, username: String, device: String, ack: u64, cmd: LinkCommand) {
1051        let Ok(runtime) = tokio::runtime::Handle::try_current() else {
1052            return;
1053        };
1054        let key = (username.clone(), device.clone(), ack);
1055        let timer = runtime.spawn(async move {
1056            tokio::time::sleep(FIRST_LOOK).await;
1057            let (looking, looking_in) = (device.clone(), username.clone());
1058            let taken = tokio::task::spawn_blocking(move || {
1059                registry().look(&looking_in, &looking, ack, &cmd)
1060            })
1061            .await
1062            .unwrap_or(false);
1063            if taken {
1064                tokio::time::sleep(LONGEST_ANSWER.saturating_sub(FIRST_LOOK)).await;
1065                registry().give_up(&username, &device, ack);
1066            }
1067        });
1068        match self.answers.lock().get_mut(&key) {
1069            Some(w) => w.timer = Some(timer.abort_handle()),
1070            // Answered already.
1071            None => timer.abort(),
1072        }
1073    }
1074
1075    /// `device`, linked as `username`, answered the command it was sent
1076    /// under `ack`.
1077    pub fn answered(&self, username: &str, device: &str, ack: u64, outcome: AckOutcome) {
1078        let key = (username.to_string(), device.to_string(), ack);
1079        let waiting = self.answers.lock().remove(&key);
1080        if let Some(w) = waiting {
1081            w.answer(Some(outcome));
1082        }
1083    }
1084
1085    /// `device`, linked as `username`, says the command sent under `ack` came
1086    /// off its link.
1087    pub fn received(&self, username: &str, device: &str, ack: u64) {
1088        let key = (username.to_string(), device.to_string(), ack);
1089        if let Some(w) = self.answers.lock().get_mut(&key) {
1090            w.received = true;
1091        }
1092    }
1093
1094    /// The first look at a command sent to `device` under `ack`. Taken off its
1095    /// link: its answer is waited for, however long the command runs, and
1096    /// `true` says so. Not taken: the link is not delivering, though it may be
1097    /// open (a phone iOS has suspended, a socket not yet closed), so the
1098    /// device is reached as one that is away, with the command kept under its
1099    /// id so the device acts on it once whichever copy arrives first. The link
1100    /// is left to end of its own accord.
1101    fn look(&self, username: &str, device: &str, ack: u64, cmd: &LinkCommand) -> bool {
1102        let key = (username.to_string(), device.to_string(), ack);
1103        let mut answers = self.answers.lock();
1104        match answers.get(&key) {
1105            None => return false,
1106            Some(w) if w.received => return true,
1107            Some(_) => {}
1108        }
1109        let w = answers.remove(&key).expect("just seen");
1110        drop(answers);
1111        log::info!("link: {device} has not taken a command; reaching it as away");
1112        let outcome = match reach_absent(Some(username), Some(device), cmd, Some(ack)) {
1113            Some(Ok(_)) => AckOutcome::Queued,
1114            _ => AckOutcome::Failed {
1115                error: format!("{device} did not take it"),
1116            },
1117        };
1118        w.answer(Some(outcome));
1119        false
1120    }
1121
1122    /// Stop waiting for the answer to a command `device` took long ago; the
1123    /// asker learns only that it arrived.
1124    fn give_up(&self, username: &str, device: &str, ack: u64) {
1125        let key = (username.to_string(), device.to_string(), ack);
1126        let waiting = self.answers.lock().remove(&key);
1127        if let Some(w) = waiting {
1128            w.answer(None);
1129        }
1130    }
1131}
1132
1133impl Registry {
1134    pub fn add_order(&self, order: Order) {
1135        outbox::save_order(&order);
1136        self.orders.lock().push(order);
1137    }
1138
1139    pub fn orders(&self, username: Option<&str>) -> Vec<Order> {
1140        self.orders
1141            .lock()
1142            .iter()
1143            .filter(|o| username.is_none() || o.username.as_deref() == username)
1144            .cloned()
1145            .collect()
1146    }
1147
1148    pub fn cancel_order(&self, username: Option<&str>, id: &str) -> bool {
1149        let mut orders = self.orders.lock();
1150        let before = orders.len();
1151        orders
1152            .retain(|o| !(o.id == id && (username.is_none() || o.username.as_deref() == username)));
1153        let gone = orders.len() != before;
1154        if gone {
1155            outbox::drop_order(id);
1156        }
1157        gone
1158    }
1159
1160    fn done(&self, id: &str) {
1161        self.orders.lock().retain(|o| o.id != id);
1162        outbox::drop_order(id);
1163    }
1164
1165    /// Send every order whose album the library now holds, and drop it.
1166    /// `find` answers an order with the album's track uids, in order.
1167    pub fn fulfil_orders(&self, find: impl Fn(&Order) -> Option<Vec<String>>) {
1168        let now = chrono::Utc::now().timestamp();
1169        let pending: Vec<Order> = {
1170            let mut orders = self.orders.lock();
1171            for o in orders.iter().filter(|o| now - o.created_at >= ORDER_TTL) {
1172                outbox::drop_order(&o.id);
1173            }
1174            orders.retain(|o| now - o.created_at < ORDER_TTL);
1175            orders.clone()
1176        };
1177        for order in pending.into_iter().filter(|o| o.playlist.is_none()) {
1178            let Some(ids) = find(&order).filter(|ids| !ids.is_empty()) else {
1179                continue;
1180            };
1181            let track_ids = ids;
1182            let cmd = if order.play_next {
1183                LinkCommand::PlayNext { track_ids }
1184            } else {
1185                LinkCommand::Enqueue {
1186                    track_ids,
1187                    play: false,
1188                }
1189            };
1190            match self.send(order.username.as_deref(), order.client.as_deref(), cmd) {
1191                Ok(c) => {
1192                    log::info!(
1193                        "link: {} — {} arrived; queued on {}",
1194                        order.artist,
1195                        order.album,
1196                        c.name
1197                    );
1198                    self.done(&order.id);
1199                }
1200                // No device to send to yet: kept, and tried after the next scan.
1201                Err(e) => log::info!("link: {} — {} arrived but {e}", order.artist, order.album),
1202            }
1203        }
1204    }
1205}
1206
1207/// Fulfil standing orders against the library at `db_path`.
1208pub fn fulfil_from(db_path: &std::path::Path) {
1209    let registry = registry();
1210    if registry.orders.lock().is_empty() {
1211        return;
1212    }
1213    let Ok(db) = koan_core::db::connection::Database::open_existing(db_path) else {
1214        return;
1215    };
1216    // Playlist orders are the server's own to carry out: add the tracks, and
1217    // the playlist reaches every device like any other edit.
1218    let for_playlists: Vec<Order> = registry
1219        .orders
1220        .lock()
1221        .iter()
1222        .filter(|o| o.playlist.is_some())
1223        .cloned()
1224        .collect();
1225    let mut edited = false;
1226    for order in for_playlists {
1227        let Some(playlist) = order.playlist else {
1228            continue;
1229        };
1230        let Some(ids) = order_tracks(&db.conn, &order) else {
1231            continue;
1232        };
1233        match koan_core::db::queries::add_tracks(&db.conn, playlist, &ids) {
1234            Ok(_) => {
1235                // Tests would push to whatever server this machine signs in to.
1236                if !cfg!(test) {
1237                    koan_core::playlists::push_to_remote(playlist);
1238                }
1239                log::info!(
1240                    "link: {} — {} arrived; added {} tracks to playlist {playlist}",
1241                    order.artist,
1242                    order.album,
1243                    ids.len()
1244                );
1245                registry.done(&order.id);
1246                edited = true;
1247            }
1248            Err(e) => log::warn!("link: could not add to playlist {playlist}: {e}"),
1249        }
1250    }
1251    if edited {
1252        changed();
1253    }
1254    registry.fulfil_orders(|order| {
1255        let rows = order_tracks(&db.conn, order)?;
1256        queries::uids_in_order(&db.conn, UidKind::Track, &rows).ok()
1257    });
1258}
1259
1260/// The tracks an order asks for, once its album is in the library: all of
1261/// it, or the named ones in the order named. `None` until they are there.
1262fn order_tracks(conn: &rusqlite::Connection, order: &Order) -> Option<Vec<i64>> {
1263    let tracks = album_tracks(conn, &order.artist, &order.album)?;
1264    if order.titles.is_empty() {
1265        return Some(tracks.into_iter().map(|(id, _)| id).collect());
1266    }
1267    let picked: Vec<i64> = order
1268        .titles
1269        .iter()
1270        .filter_map(|want| {
1271            let want = want.to_lowercase();
1272            tracks
1273                .iter()
1274                .find(|(_, t)| t.to_lowercase().contains(&want))
1275                .map(|(id, _)| *id)
1276        })
1277        .collect();
1278    (!picked.is_empty()).then_some(picked)
1279}
1280
1281/// The newest album whose artist and title contain these, as its tracks in
1282/// disc and track order.
1283pub fn album_tracks(
1284    conn: &rusqlite::Connection,
1285    artist: &str,
1286    album: &str,
1287) -> Option<Vec<(i64, String)>> {
1288    let like = |s: &str| format!("%{}%", s.replace(['%', '_'], ""));
1289    let album_id: i64 = conn
1290        .query_row(
1291            "SELECT al.id FROM albums al JOIN artists a ON a.id = al.artist_id
1292              WHERE a.name LIKE ?1 COLLATE NOCASE AND al.title LIKE ?2 COLLATE NOCASE
1293              ORDER BY al.id DESC LIMIT 1",
1294            [like(artist), like(album)],
1295            |r| r.get(0),
1296        )
1297        .ok()?;
1298    let mut stmt = conn
1299        .prepare("SELECT id, title FROM tracks WHERE album_id = ?1 ORDER BY disc, track_number, id")
1300        .ok()?;
1301    let tracks = stmt
1302        .query_map([album_id], |r| Ok((r.get(0)?, r.get(1)?)))
1303        .ok()?
1304        .filter_map(Result::ok)
1305        .collect();
1306    Some(tracks)
1307}
1308
1309impl Registry {
1310    /// Send to every client `username` may command. The names of those it
1311    /// reached.
1312    pub fn broadcast(&self, username: Option<&str>, cmd: LinkCommand) -> Vec<String> {
1313        let ids: Vec<String> = self.list(username).into_iter().map(|c| c.id).collect();
1314        let entries = self.entries.lock();
1315        entries
1316            .iter()
1317            .filter(|e| ids.contains(&e.info.id) && e.tx.send(cmd.clone().into()).is_ok())
1318            .map(|e| e.info.name.clone())
1319            .collect()
1320    }
1321}
1322
1323impl Registry {
1324    /// Send to every device `username` may command: at once to those linked,
1325    /// and to those that have linked before but are away now, when they next
1326    /// link. For commands still right hours later (`Sync`, `Evict`), never
1327    /// playback. The names reached now, and the names it waits for.
1328    pub fn deliver(&self, username: Option<&str>, cmd: LinkCommand) -> (Vec<String>, Vec<String>) {
1329        let (sent, queued) = self.link_or_queue(username, &cmd);
1330        wake(&queued.iter().map(Absent::key).collect::<Vec<_>>());
1331        (sent, queued.into_iter().map(|q| q.name).collect())
1332    }
1333
1334    fn link_or_queue(
1335        &self,
1336        username: Option<&str>,
1337        cmd: &LinkCommand,
1338    ) -> (Vec<String>, Vec<Absent>) {
1339        let sent = self.broadcast(username, cmd.clone());
1340        let queued = outbox::queue_for_absent(username, &self.live(), cmd);
1341        (sent, queued)
1342    }
1343
1344    /// Each linked device, as `(device, username)`.
1345    fn live(&self) -> Vec<(String, String)> {
1346        self.entries
1347            .lock()
1348            .iter()
1349            .map(|e| (e.device.clone(), e.info.username.clone()))
1350            .collect()
1351    }
1352}
1353
1354impl Absent {
1355    fn key(&self) -> (String, String) {
1356        (self.device.clone(), self.username.clone())
1357    }
1358}
1359
1360/// Wake these absent devices, each a `(device, username)`, where a push can:
1361/// each links, and takes what was queued for it.
1362fn wake(devices: &[(String, String)]) {
1363    let Some(pusher) = crate::push::pusher() else {
1364        return;
1365    };
1366    let targets: Vec<outbox::PushTarget> = outbox::push_targets(None)
1367        .into_iter()
1368        .filter(|t| {
1369            devices
1370                .iter()
1371                .any(|(device, username)| *device == t.device && *username == t.username)
1372        })
1373        .collect();
1374    if targets.is_empty() {
1375        return;
1376    }
1377    std::thread::spawn(move || {
1378        for t in targets {
1379            deliver_push(pusher, &t, &crate::push::Push::Wake);
1380        }
1381    });
1382}
1383
1384/// Reach a device that is not linked: the one named, else the one seen most
1385/// recently.
1386///
1387/// Music is sent as a notification to tap, at once: iOS does not let an app it
1388/// woke start audio, so waking it for that only delays the notification. Every
1389/// other command goes into the device's outbox with a background push to wake
1390/// it, and runs as if it had been linked. `None` without a push key, or with no
1391/// device in scope that has given a push token.
1392fn reach_absent(
1393    username: Option<&str>,
1394    id: Option<&str>,
1395    cmd: &LinkCommand,
1396    ack: Option<u64>,
1397) -> Option<Result<ClientInfo, String>> {
1398    if cmd.live_only() {
1399        return None;
1400    }
1401    let envelope = Envelope {
1402        command: cmd.clone(),
1403        ack,
1404    };
1405    let pusher = crate::push::pusher()?;
1406    // Most recently seen first: a reinstall leaves its old entry behind under
1407    // the same name, and the newest is the one in the person's hand.
1408    let target = outbox::push_targets(username)
1409        .into_iter()
1410        .find(|t| id.is_none_or(|id| t.device == id || t.name.eq_ignore_ascii_case(id)))?;
1411    let info = ClientInfo {
1412        id: target.device.clone(),
1413        device: target.device.clone(),
1414        name: target.name.clone(),
1415        platform: target.platform.clone(),
1416        username: target.username.clone(),
1417        connected_at: 0,
1418        state: LinkState::default(),
1419        last_played_at: None,
1420        state_at: 0,
1421        reports: false,
1422        notified: true,
1423        acks: false,
1424    };
1425    // What it asks, whoever asks it.
1426    let asked = match cmd {
1427        LinkCommand::Shared { command } => command.as_ref(),
1428        cmd => cmd,
1429    };
1430    let verb = match asked {
1431        LinkCommand::Play { .. } | LinkCommand::JumpTo { .. } | LinkCommand::Resume => Some("Play"),
1432        _ => None,
1433    };
1434    let push = match verb {
1435        Some(verb) => crate::push::Push::Notify {
1436            title: format!("{verb} on {}", target.name),
1437            body: outbox::describe(asked).unwrap_or_else(|| "From your koan server".into()),
1438            command: serde_json::to_value(&envelope).ok()?,
1439            image: cover_track(asked)
1440                .and_then(outbox::track_row)
1441                .and_then(|t| pusher.cover_link(t)),
1442        },
1443        None => {
1444            outbox::queue_for(&target.device, &target.username, &envelope);
1445            crate::push::Push::Wake
1446        }
1447    };
1448    std::thread::spawn(move || deliver_push(pusher, &target, &push));
1449    Some(Ok(info))
1450}
1451
1452/// The track whose album cover a notification for `cmd` shows. Commands carry
1453/// uids, so this is the id as sent; see `outbox::track_row`.
1454fn cover_track(cmd: &LinkCommand) -> Option<&str> {
1455    match cmd {
1456        LinkCommand::Play {
1457            track_ids,
1458            start_at,
1459            ..
1460        } => track_ids
1461            .get(*start_at as usize)
1462            .or(track_ids.first())
1463            .map(String::as_str),
1464        LinkCommand::JumpTo { track_id } => Some(track_id),
1465        _ => None,
1466    }
1467}
1468
1469/// Send one push, forgetting a token Apple says is no longer good.
1470fn deliver_push(
1471    pusher: &crate::push::Pusher,
1472    target: &outbox::PushTarget,
1473    push: &crate::push::Push,
1474) {
1475    use crate::push::Outcome;
1476    match pusher.send(&target.token, target.sandbox, push) {
1477        Outcome::Sent => log::info!("push: sent to {}", target.name),
1478        Outcome::Gone => {
1479            log::info!(
1480                "push: {}'s token is no longer valid; forgotten",
1481                target.name
1482            );
1483            outbox::forget_push(&target.username, &target.device);
1484        }
1485        Outcome::Failed(e) => log::warn!("push: to {} failed: {e}", target.name),
1486    }
1487}
1488
1489/// Wakes sent and not yet answered by a link, by `(device, username)`: when,
1490/// and which kind. A device that does not link is never answered; the map
1491/// is bounded by the devices that have pushed tokens.
1492type Woken = std::collections::HashMap<(String, String), (std::time::Instant, &'static str)>;
1493
1494static WOKEN: LazyLock<Mutex<Woken>> = LazyLock::new(Default::default);
1495
1496/// After `user` played or favourited something: re-evaluate their smart
1497/// playlists that read it, and have every device pull any that moved, so a
1498/// "most played" list on an idle device does not wait for its next read.
1499pub fn smart_activity(
1500    db: &koan_core::db::connection::Database,
1501    user: i64,
1502    fields: &[koan_core::smart::Field],
1503) {
1504    match koan_core::db::queries::smart::refresh_after_activity(&db.conn, user, fields) {
1505        Ok(moved) if !moved.is_empty() => changed(),
1506        Ok(_) => {}
1507        Err(e) => log::warn!("smart playlists not refreshed after activity: {e}"),
1508    }
1509}
1510
1511/// Have every device pull what the server just changed (a playlist edited,
1512/// albums added): at once where linked, on next link where not. Syncs waiting
1513/// for a device collapse into one, and so do the pushes that wake it: see
1514/// `Wakes`.
1515pub fn changed() {
1516    let (_, queued) = registry().link_or_queue(None, &LinkCommand::Sync { full: false });
1517    if queued.is_empty() || crate::push::pusher().is_none() {
1518        return;
1519    }
1520    let mut wakes = WAKES.lock();
1521    wakes.add(queued.iter().map(Absent::key), std::time::Instant::now());
1522    if !wakes.timer {
1523        wakes.timer = true;
1524        std::thread::spawn(send_wakes);
1525    }
1526}
1527
1528/// How long the library has to stay still before a suspended device is woken
1529/// to sync. A download of several albums scans after each one; iOS rations
1530/// background pushes, and the device needs waking once, at the end.
1531const QUIET: std::time::Duration = std::time::Duration::from_secs(30);
1532
1533/// However busy the library stays, a device waits no longer than this.
1534const LONGEST_WAIT: std::time::Duration = std::time::Duration::from_secs(5 * 60);
1535
1536static WAKES: Mutex<Wakes> = Mutex::new(Wakes {
1537    pending: Vec::new(),
1538    timer: false,
1539});
1540
1541/// Background pushes held back until the library is quiet: at most one
1542/// pending per device.
1543struct Wakes {
1544    /// `(device, username)`, when first asked for, when last asked for.
1545    pending: Vec<((String, String), std::time::Instant, std::time::Instant)>,
1546    /// Whether a thread is waiting to send them.
1547    timer: bool,
1548}
1549
1550impl Wakes {
1551    fn add(
1552        &mut self,
1553        devices: impl IntoIterator<Item = (String, String)>,
1554        now: std::time::Instant,
1555    ) {
1556        for key in devices {
1557            match self.pending.iter_mut().find(|(k, _, _)| *k == key) {
1558                Some((_, _, last)) => *last = now,
1559                None => self.pending.push((key, now, now)),
1560            }
1561        }
1562    }
1563
1564    fn due_at(first: std::time::Instant, last: std::time::Instant) -> std::time::Instant {
1565        (last + QUIET).min(first + LONGEST_WAIT)
1566    }
1567
1568    /// The earliest a pending wake is due.
1569    fn next(&self) -> Option<std::time::Instant> {
1570        self.pending
1571            .iter()
1572            .map(|(_, first, last)| Self::due_at(*first, *last))
1573            .min()
1574    }
1575
1576    /// Take the wakes due by `now`.
1577    fn take_due(&mut self, now: std::time::Instant) -> Vec<(String, String)> {
1578        let (due, waiting) = std::mem::take(&mut self.pending)
1579            .into_iter()
1580            .partition(|(_, first, last)| Self::due_at(*first, *last) <= now);
1581        self.pending = waiting;
1582        due.into_iter().map(|(key, _, _)| key).collect()
1583    }
1584}
1585
1586/// Send each wake once it is due, until none is pending. A device that has
1587/// linked meanwhile took its sync down the link and is not pushed.
1588fn send_wakes() {
1589    loop {
1590        let (due, next) = {
1591            let mut wakes = WAKES.lock();
1592            let due = wakes.take_due(std::time::Instant::now());
1593            let next = wakes.next();
1594            if due.is_empty() && next.is_none() {
1595                wakes.timer = false;
1596                return;
1597            }
1598            (due, next)
1599        };
1600        let live = registry().live();
1601        let absent: Vec<(String, String)> = due.into_iter().filter(|d| !live.contains(d)).collect();
1602        if !absent.is_empty() {
1603            wake(&absent);
1604        }
1605        if let Some(next) = next {
1606            std::thread::sleep(next.saturating_duration_since(std::time::Instant::now()));
1607        }
1608    }
1609}
1610
1611/// After a library scan or sync: if the library holds different tracks or
1612/// albums from when this was last asked, tell every device. A scan that found
1613/// nothing new, which is most of them, sends nothing.
1614pub fn changed_if_library_moved(conn: &rusqlite::Connection) {
1615    static LAST: parking_lot::Mutex<Option<(i64, i64, i64)>> = parking_lot::Mutex::new(None);
1616    let Ok(now) = conn.query_row(
1617        "SELECT (SELECT COUNT(*) FROM tracks), (SELECT COALESCE(MAX(id), 0) FROM tracks),
1618                (SELECT COUNT(*) FROM albums)",
1619        [],
1620        |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
1621    ) else {
1622        return;
1623    };
1624    let before = LAST.lock().replace(now);
1625    if before.is_some_and(|b| b != now) {
1626        changed();
1627    }
1628}
1629
1630/// The server-side record of devices and their waiting commands, in the
1631/// library database so it outlives a restart.
1632mod outbox {
1633    use koan_core::db::queries;
1634    use koan_core::remote::link::LinkCommand;
1635
1636    /// Dropped undelivered after this long: a device away a month re-syncs
1637    /// on its own when opened. A device unseen this long is forgotten with
1638    /// its push token, so the entry a reinstall leaves behind stops being
1639    /// offered as a device; a live one gives its token again when it links.
1640    const KEEP_SECS: i64 = 30 * 24 * 60 * 60;
1641
1642    /// Tests keep to memory: the configured database is whoever ran them.
1643    ///
1644    /// The shared pool, because a state report from every linked device lands
1645    /// here: `Database::open` would run the schema and a checkpoint each time.
1646    fn db() -> Option<koan_core::db::pool::Handle<'static>> {
1647        if cfg!(test) {
1648            return None;
1649        }
1650        koan_core::db::pool::shared().get().ok()
1651    }
1652
1653    pub fn load_orders() -> Vec<super::Order> {
1654        let Some(db) = db() else { return Vec::new() };
1655        db.conn
1656            .prepare("SELECT body FROM link_orders ORDER BY created_at")
1657            .and_then(|mut s| {
1658                s.query_map([], |r| r.get::<_, String>(0))?
1659                    .collect::<Result<Vec<_>, _>>()
1660            })
1661            .unwrap_or_default()
1662            .into_iter()
1663            .filter_map(|b| serde_json::from_str(&b).ok())
1664            .collect()
1665    }
1666
1667    pub fn save_order(order: &super::Order) {
1668        let (Some(db), Ok(body)) = (db(), serde_json::to_string(order)) else {
1669            return;
1670        };
1671        let _ = db.conn.execute(
1672            "INSERT OR REPLACE INTO link_orders (id, body, created_at) VALUES (?1, ?2, ?3)",
1673            rusqlite::params![order.id, body, order.created_at],
1674        );
1675    }
1676
1677    pub fn drop_order(id: &str) {
1678        if let Some(db) = db() {
1679            let _ = db
1680                .conn
1681                .execute("DELETE FROM link_orders WHERE id = ?1", [id]);
1682        }
1683    }
1684
1685    pub fn take_and_remember(
1686        username: &str,
1687        device: &str,
1688        name: &str,
1689        platform: &str,
1690        live: &[(String, String)],
1691    ) -> Vec<koan_core::remote::acks::Envelope> {
1692        let Some(db) = db() else { return Vec::new() };
1693        let now = chrono::Utc::now().timestamp();
1694        let _ = db.conn.execute(
1695            "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, ?2, ?3, ?4, ?5)
1696             ON CONFLICT (device, username) DO UPDATE SET name = ?3, platform = ?4, last_seen = ?5",
1697            rusqlite::params![device, username, name, platform, now],
1698        );
1699        forget_stale(&db.conn, live, now);
1700        let waiting: Vec<(i64, String)> = db
1701            .conn
1702            .prepare("SELECT id, command FROM link_outbox WHERE device = ?1 AND username = ?2 ORDER BY id")
1703            .and_then(|mut s| {
1704                s.query_map([device, username], |r| Ok((r.get(0)?, r.get(1)?)))?
1705                    .collect()
1706            })
1707            .unwrap_or_default();
1708        let _ = db.conn.execute(
1709            "DELETE FROM link_outbox WHERE device = ?1 AND username = ?2",
1710            [device, username],
1711        );
1712        if !waiting.is_empty() {
1713            log::info!("link: {} waiting commands for {name}", waiting.len());
1714        }
1715        waiting
1716            .into_iter()
1717            .filter_map(|(_, c)| serde_json::from_str(&c).ok())
1718            .collect()
1719    }
1720
1721    /// Drop undelivered commands, devices and push tokens older than
1722    /// `KEEP_SECS`. A link held open that long is seen, not stale.
1723    pub(super) fn forget_stale(conn: &rusqlite::Connection, live: &[(String, String)], now: i64) {
1724        touch_with(conn, live, now);
1725        let _ = conn.execute(
1726            "DELETE FROM link_outbox WHERE created_at < ?1",
1727            [now - KEEP_SECS],
1728        );
1729        let _ = conn.execute(
1730            "DELETE FROM link_push WHERE (device, username) IN
1731               (SELECT device, username FROM link_devices WHERE last_seen < ?1)",
1732            [now - KEEP_SECS],
1733        );
1734        let _ = conn.execute(
1735            "DELETE FROM link_devices WHERE last_seen < ?1",
1736            [now - KEEP_SECS],
1737        );
1738    }
1739
1740    /// Mark `(device, username)` pairs seen now.
1741    pub fn touch(devices: &[(String, String)]) {
1742        if let Some(db) = db() {
1743            touch_with(&db.conn, devices, chrono::Utc::now().timestamp());
1744        }
1745    }
1746
1747    fn touch_with(conn: &rusqlite::Connection, devices: &[(String, String)], now: i64) {
1748        for (device, username) in devices {
1749            let _ = conn.execute(
1750                "UPDATE link_devices SET last_seen = ?1 WHERE device = ?2 AND username = ?3",
1751                rusqlite::params![now, device, username],
1752            );
1753        }
1754    }
1755
1756    /// A known device that was not linked when something was queued for it.
1757    pub struct Absent {
1758        pub device: String,
1759        pub username: String,
1760        pub name: String,
1761    }
1762
1763    /// Queue `cmd` for each known device in scope that is not in `live`.
1764    pub fn queue_for_absent(
1765        username: Option<&str>,
1766        live: &[(String, String)],
1767        cmd: &LinkCommand,
1768    ) -> Vec<Absent> {
1769        let Some(db) = db() else { return Vec::new() };
1770        let known: Vec<(String, String, String)> = db
1771            .conn
1772            .prepare("SELECT device, username, name FROM link_devices")
1773            .and_then(|mut s| {
1774                s.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
1775                    .collect()
1776            })
1777            .unwrap_or_default();
1778        let Ok(text) = serde_json::to_string(cmd) else {
1779            return Vec::new();
1780        };
1781        let absent: Vec<_> = known
1782            .into_iter()
1783            .filter(|(device, user, _)| {
1784                !username.is_some_and(|u| u != user)
1785                    && !live.iter().any(|(d, u)| d == device && u == user)
1786            })
1787            .collect();
1788        if absent.is_empty() {
1789            return Vec::new();
1790        }
1791        let is_sync = matches!(cmd, LinkCommand::Sync { .. });
1792        let now = chrono::Utc::now().timestamp();
1793        // Every playlist edit and scan lands here: one transaction, not two
1794        // per device.
1795        koan_core::db::queries::atomically(&db.conn, || {
1796            let mut queued = Vec::new();
1797            for (device, user, name) in absent {
1798                if is_sync {
1799                    // One pending sync is enough; a full one covers an incremental.
1800                    let _ = db.conn.execute(
1801                        "DELETE FROM link_outbox WHERE device = ?1 AND username = ?2 AND command LIKE '{\"type\":\"sync\"%'",
1802                        [&device, &user],
1803                    );
1804                }
1805                if db
1806                    .conn
1807                    .execute(
1808                        "INSERT INTO link_outbox (device, username, command, created_at) VALUES (?1, ?2, ?3, ?4)",
1809                        rusqlite::params![device, user, text, now],
1810                    )
1811                    .is_ok()
1812                {
1813                    queued.push(Absent {
1814                        device,
1815                        username: user,
1816                        name,
1817                    });
1818                }
1819            }
1820            Ok::<_, rusqlite::Error>(queued)
1821        })
1822        .unwrap_or_default()
1823    }
1824
1825    /// A device Apple's push service can reach.
1826    #[derive(Clone)]
1827    pub struct PushTarget {
1828        pub device: String,
1829        pub username: String,
1830        pub name: String,
1831        pub platform: String,
1832        pub token: String,
1833        pub sandbox: bool,
1834        /// Unix seconds.
1835        pub last_seen: i64,
1836    }
1837
1838    /// Keep a command for `device` until it next links, under the id it was
1839    /// sent with, if any, so a device that also got it over its link acts on
1840    /// it once.
1841    pub fn queue_for(device: &str, username: &str, envelope: &koan_core::remote::acks::Envelope) {
1842        let (Some(db), Ok(text)) = (db(), serde_json::to_string(envelope)) else {
1843            return;
1844        };
1845        let _ = db.conn.execute(
1846            "INSERT INTO link_outbox (device, username, command, created_at) VALUES (?1, ?2, ?3, ?4)",
1847            rusqlite::params![device, username, text, chrono::Utc::now().timestamp()],
1848        );
1849    }
1850
1851    /// Every account on the server, by name: what an owner chooses from to
1852    /// share a device. Any account may see them; the server's users trust
1853    /// each other that far.
1854    pub fn accounts() -> Vec<String> {
1855        let Some(db) = db() else { return Vec::new() };
1856        koan_core::db::queries::auth::list_users(&db.conn)
1857            .map(|users| users.into_iter().map(|u| u.username).collect())
1858            .unwrap_or_default()
1859    }
1860
1861    /// Where each device last linked from, by device id: the most recent of
1862    /// its rows, should it have linked as more than one account.
1863    pub fn load_addresses() -> std::collections::HashMap<String, (String, std::net::IpAddr)> {
1864        let Some(db) = db() else {
1865            return Default::default();
1866        };
1867        load_addresses_in(&db.conn)
1868    }
1869
1870    pub(super) fn load_addresses_in(
1871        conn: &rusqlite::Connection,
1872    ) -> std::collections::HashMap<String, (String, std::net::IpAddr)> {
1873        conn.prepare(
1874            "SELECT device, username, addr FROM link_devices WHERE addr IS NOT NULL ORDER BY last_seen",
1875        )
1876        .and_then(|mut s| {
1877            s.query_map([], |r| {
1878                Ok((
1879                    r.get::<_, String>(0)?,
1880                    r.get::<_, String>(1)?,
1881                    r.get::<_, String>(2)?,
1882                ))
1883            })?
1884            .collect::<Result<Vec<_>, _>>()
1885        })
1886        .unwrap_or_default()
1887        .into_iter()
1888        .filter_map(|(device, username, addr)| Some((device, (username, addr.parse().ok()?))))
1889        .collect()
1890    }
1891
1892    pub fn save_address(device: &str, username: &str, addr: std::net::IpAddr) {
1893        if let Some(db) = db() {
1894            save_address_in(&db.conn, device, username, addr);
1895        }
1896    }
1897
1898    pub(super) fn save_address_in(
1899        conn: &rusqlite::Connection,
1900        device: &str,
1901        username: &str,
1902        addr: std::net::IpAddr,
1903    ) {
1904        let _ = conn.execute(
1905            "UPDATE link_devices SET addr = ?1 WHERE device = ?2 AND username = ?3",
1906            rusqlite::params![addr.to_string(), device, username],
1907        );
1908    }
1909
1910    pub fn load_grants() -> Vec<super::Grant> {
1911        let Some(db) = db() else { return Vec::new() };
1912        db.conn
1913            .prepare("SELECT device, owner, grantee FROM link_grants ORDER BY created_at")
1914            .and_then(|mut s| {
1915                s.query_map([], |r| {
1916                    Ok(super::Grant {
1917                        device: r.get(0)?,
1918                        owner: r.get(1)?,
1919                        grantee: r.get(2)?,
1920                    })
1921                })?
1922                .collect()
1923            })
1924            .unwrap_or_default()
1925    }
1926
1927    pub fn save_grant(g: &super::Grant) {
1928        if let Some(db) = db() {
1929            let _ = db.conn.execute(
1930                "INSERT OR IGNORE INTO link_grants (device, owner, grantee, created_at) VALUES (?1, ?2, ?3, ?4)",
1931                rusqlite::params![g.device, g.owner, g.grantee, chrono::Utc::now().timestamp()],
1932            );
1933        }
1934    }
1935
1936    pub fn drop_grant(g: &super::Grant) {
1937        if let Some(db) = db() {
1938            let _ = db.conn.execute(
1939                "DELETE FROM link_grants WHERE device = ?1 AND owner = ?2 AND grantee = ?3",
1940                [&g.device, &g.owner, &g.grantee],
1941            );
1942        }
1943    }
1944
1945    pub fn save_push(username: &str, device: &str, token: &str, sandbox: bool) {
1946        let Some(db) = db() else { return };
1947        let _ = db.conn.execute(
1948            "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)
1949             ON CONFLICT (device, username) DO UPDATE SET token = ?3, sandbox = ?4, updated_at = ?5",
1950            rusqlite::params![device, username, token, sandbox, chrono::Utc::now().timestamp()],
1951        );
1952    }
1953
1954    pub fn forget_push(username: &str, device: &str) {
1955        if let Some(db) = db() {
1956            let _ = db.conn.execute(
1957                "DELETE FROM link_push WHERE device = ?1 AND username = ?2",
1958                [device, username],
1959            );
1960        }
1961    }
1962
1963    /// Devices in scope with a push token, most recently seen first.
1964    pub fn push_targets(username: Option<&str>) -> Vec<PushTarget> {
1965        let Some(db) = db() else { return Vec::new() };
1966        push_targets_in(&db.conn, username)
1967    }
1968
1969    pub(super) fn push_targets_in(
1970        conn: &rusqlite::Connection,
1971        username: Option<&str>,
1972    ) -> Vec<PushTarget> {
1973        conn
1974            .prepare(
1975                "SELECT p.device, p.username, d.name, d.platform, p.token, p.sandbox, d.last_seen
1976                   FROM link_push p JOIN link_devices d ON d.device = p.device AND d.username = p.username
1977                  WHERE ?1 IS NULL OR p.username = ?1
1978                  ORDER BY d.last_seen DESC",
1979            )
1980            .and_then(|mut s| {
1981                s.query_map([username], |r| {
1982                    Ok(PushTarget {
1983                        device: r.get(0)?,
1984                        username: r.get(1)?,
1985                        name: r.get(2)?,
1986                        platform: r.get(3)?,
1987                        token: r.get(4)?,
1988                        sandbox: r.get(5)?,
1989                        last_seen: r.get(6)?,
1990                    })
1991                })?
1992                .collect()
1993            })
1994            .unwrap_or_default()
1995    }
1996
1997    /// Forget `username`'s device `device`: its record, its push token and
1998    /// what waits for it. It is recorded afresh if it links again.
1999    pub fn forget_device(device: &str, username: &str) {
2000        if let Some(db) = db() {
2001            forget_device_in(&db.conn, device, username);
2002        }
2003    }
2004
2005    pub(super) fn forget_device_in(conn: &rusqlite::Connection, device: &str, username: &str) {
2006        for table in ["link_push", "link_outbox", "link_devices"] {
2007            let _ = conn.execute(
2008                &format!("DELETE FROM {table} WHERE device = ?1 AND username = ?2"),
2009                [device, username],
2010            );
2011        }
2012    }
2013
2014    /// The row id of a track a command names by uid or row id.
2015    pub fn track_row(id: &str) -> Option<i64> {
2016        let db = db()?;
2017        queries::resolve_id(&db.conn, queries::UidKind::Track, id)
2018            .ok()
2019            .flatten()
2020    }
2021
2022    /// What a playback command would play, for a notification to say:
2023    /// "Golden Standard — Tony Petersen", or a track and how many follow.
2024    pub fn describe(cmd: &LinkCommand) -> Option<String> {
2025        let ids: Vec<&String> = match cmd {
2026            LinkCommand::Play { track_ids, .. }
2027            | LinkCommand::Enqueue { track_ids, .. }
2028            | LinkCommand::PlayNext { track_ids } => track_ids.iter().collect(),
2029            LinkCommand::JumpTo { track_id } => vec![track_id],
2030            _ => return None,
2031        };
2032        let db = db()?;
2033        let ids: Vec<i64> = ids
2034            .into_iter()
2035            .filter_map(|t| {
2036                queries::resolve_id(&db.conn, queries::UidKind::Track, t)
2037                    .ok()
2038                    .flatten()
2039            })
2040            .collect();
2041        let row = |id: i64| {
2042            db.conn
2043                .query_row(
2044                    "SELECT t.title, COALESCE(a.name, ''), COALESCE(al.title, ''), t.album_id
2045                       FROM tracks t LEFT JOIN artists a ON a.id = t.artist_id
2046                       LEFT JOIN albums al ON al.id = t.album_id WHERE t.id = ?1",
2047                    [id],
2048                    |r| {
2049                        Ok((
2050                            r.get::<_, String>(0)?,
2051                            r.get::<_, String>(1)?,
2052                            r.get::<_, String>(2)?,
2053                            r.get::<_, Option<i64>>(3)?,
2054                        ))
2055                    },
2056                )
2057                .ok()
2058        };
2059        let (title, artist, album, album_id) = row(*ids.first()?)?;
2060        let one_album = ids.len() > 1
2061            && album_id.is_some()
2062            && ids
2063                .iter()
2064                .all(|id| row(*id).is_some_and(|r| r.3 == album_id));
2065        Some(match (one_album, ids.len()) {
2066            (true, _) => format!("{album} — {artist}"),
2067            (false, 1) => format!("{title} — {artist}"),
2068            (false, n) => format!("{title} — {artist}, and {} more", n - 1),
2069        })
2070    }
2071}
2072
2073/// How long ago a client can have stopped playing and still be the obvious
2074/// one to send music to.
2075const RECENT: i64 = 6 * 60 * 60;
2076
2077fn pick(clients: &[ClientInfo], now: i64) -> Result<&ClientInfo, String> {
2078    if let Some(c) = clients.iter().find(|c| c.state.playing) {
2079        return Ok(c);
2080    }
2081    if let Some(c) = clients
2082        .iter()
2083        .filter(|c| c.last_played_at.is_some_and(|t| now - t < RECENT))
2084        .max_by_key(|c| c.last_played_at)
2085    {
2086        return Ok(c);
2087    }
2088    match clients {
2089        [] => Err("no koan app is linked to this server; open koan on the device".into()),
2090        [only] => Ok(only),
2091        several => Err(format!(
2092            "several koan apps are linked and none has played recently: {}. Ask which, then pass `client`",
2093            several
2094                .iter()
2095                .map(|c| format!("{} ({}, id {})", c.name, c.platform, c.device))
2096                .collect::<Vec<_>>()
2097                .join(", ")
2098        )),
2099    }
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104
2105    /// A link that has said nothing for a while is not dead for it: a device
2106    /// that took the command keeps its link and is waited for, however long
2107    /// the command runs. One that has not taken it is reached as away, and
2108    /// its link is still left alone.
2109    #[test]
2110    fn a_quiet_link_that_took_the_command_is_kept_and_waited_for() {
2111        use koan_core::remote::acks::AckOutcome;
2112        let reg = Registry::default();
2113        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
2114        reg.register("q", "mac", "macos", "dev-quiet", tx, false, true);
2115        let answers = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2116        let acking = |id: u64| {
2117            let answers = answers.clone();
2118            Acking {
2119                id,
2120                reply: Box::new(move |outcome| answers.lock().unwrap().push((id, outcome))),
2121            }
2122        };
2123
2124        // Taken off its link, then busy (a sync) and silent: kept, waited for.
2125        reg.relay_acked("q", None, "dev-quiet", LinkCommand::Pause, Some(acking(1)))
2126            .unwrap();
2127        reg.received("q", "dev-quiet", 1);
2128        assert!(reg.look("q", "dev-quiet", 1, &LinkCommand::Pause));
2129        assert!(answers.lock().unwrap().is_empty(), "still waiting");
2130        reg.answered("q", "dev-quiet", 1, AckOutcome::Done);
2131        assert_eq!(
2132            answers.lock().unwrap().last(),
2133            Some(&(1, Some(AckOutcome::Done)))
2134        );
2135
2136        // Not taken: reached as away (no push key here, so not reachable),
2137        // and the link itself left alone.
2138        reg.relay_acked("q", None, "dev-quiet", LinkCommand::Pause, Some(acking(2)))
2139            .unwrap();
2140        assert!(!reg.look("q", "dev-quiet", 2, &LinkCommand::Pause));
2141        assert!(matches!(
2142            answers.lock().unwrap().last(),
2143            Some((2, Some(AckOutcome::Failed { .. })))
2144        ));
2145        assert_eq!(reg.list(Some("q")).len(), 1, "the link is not dropped");
2146    }
2147
2148    /// An answered command costs nothing more: the answer takes its waiting
2149    /// entry, and that stops the timer that would have looked at it.
2150    #[tokio::test]
2151    async fn an_answer_stops_its_timer() {
2152        let timer = tokio::spawn(tokio::time::sleep(std::time::Duration::from_secs(3600)));
2153        let handle = timer.abort_handle();
2154        let waiting = Waiting {
2155            reply: Box::new(|_| {}),
2156            received: true,
2157            timer: Some(timer.abort_handle()),
2158        };
2159        waiting.answer(Some(koan_core::remote::acks::AckOutcome::Done));
2160        assert!(timer.await.unwrap_err().is_cancelled());
2161        assert!(handle.is_finished());
2162    }
2163
2164    #[test]
2165    fn a_command_relayed_with_an_id_is_answered_once() {
2166        use koan_core::remote::acks::AckOutcome;
2167        let reg = Registry::default();
2168        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2169        reg.register("a", "phone", "ios", "dev-answers", tx, false, true);
2170        let (old_tx, mut old_rx) = tokio::sync::mpsc::unbounded_channel();
2171        reg.register("a", "mac", "macos", "dev-old", old_tx, false, false);
2172        let answers = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2173        let acking = |id: u64| {
2174            let answers = answers.clone();
2175            Acking {
2176                id,
2177                reply: Box::new(move |outcome| answers.lock().unwrap().push((id, outcome))),
2178            }
2179        };
2180
2181        // A device that answers is sent the id, and its answer handed on once.
2182        reg.relay_acked(
2183            "a",
2184            None,
2185            "dev-answers",
2186            LinkCommand::Pause,
2187            Some(acking(7)),
2188        )
2189        .unwrap();
2190        assert_eq!(rx.try_recv().unwrap().ack, Some(7));
2191        // Another account's device claiming the same id is not heard.
2192        let (b_tx, _b_rx) = tokio::sync::mpsc::unbounded_channel();
2193        reg.register("b", "phone", "ios", "dev-answers", b_tx, false, true);
2194        reg.received("b", "dev-answers", 7);
2195        reg.answered("b", "dev-answers", 7, AckOutcome::Done);
2196        assert!(
2197            answers.lock().unwrap().is_empty(),
2198            "b cannot answer a's command"
2199        );
2200        reg.answered("a", "dev-answers", 7, AckOutcome::Done);
2201        reg.answered("a", "dev-answers", 7, AckOutcome::Done);
2202
2203        // One that predates answers gets the command plain, and the asker
2204        // learns at once that no answer is coming.
2205        reg.relay_acked("a", None, "dev-old", LinkCommand::Pause, Some(acking(8)))
2206            .unwrap();
2207        assert_eq!(old_rx.try_recv().unwrap().ack, None);
2208
2209        assert_eq!(
2210            *answers.lock().unwrap(),
2211            [(7, Some(AckOutcome::Done)), (8, None)]
2212        );
2213    }
2214
2215    #[test]
2216    fn a_watch_is_never_queued_and_is_renewed_when_the_target_relinks() {
2217        let reg = Registry::default();
2218        let watches = |rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>| {
2219            std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command))
2220                .filter(|c| matches!(c, LinkCommand::WatchLevels { .. }))
2221                .collect::<Vec<_>>()
2222        };
2223        // Not as a relayed command: that path queues and pushes.
2224        assert!(
2225            reg.relay("rl", "rl-phone", LinkCommand::WatchLevels { on: true })
2226                .is_err()
2227        );
2228        // Watched while away: nothing is queued for it.
2229        reg.watch_levels("rl", "rl-mac", "rl-phone", true);
2230        let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2231        reg.register("rl", "phone", "ios", "rl-phone", tx, false, false);
2232        assert_eq!(
2233            watches(&mut phone),
2234            vec![LinkCommand::WatchLevels { on: true }],
2235            "told once, on linking, because it is watched now"
2236        );
2237        // Relinked: a new session, told again.
2238        let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2239        reg.register("rl", "phone", "ios", "rl-phone", tx, false, false);
2240        assert_eq!(
2241            watches(&mut phone),
2242            vec![LinkCommand::WatchLevels { on: true }]
2243        );
2244    }
2245
2246    #[test]
2247    fn levels_reach_a_watcher_only_while_it_watches() {
2248        use koan_core::remote::levels::Frame;
2249        let reg = Registry::default();
2250        let (tx_mac, mut mac) = tokio::sync::mpsc::unbounded_channel();
2251        let (tx_phone, mut phone) = tokio::sync::mpsc::unbounded_channel();
2252        let mac_id = reg.register("lv", "mac", "macos", "lv-mac", tx_mac, false, false);
2253        reg.register("lv", "phone", "ios", "lv-phone", tx_phone, false, false);
2254        let levels = |rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>| {
2255            std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command))
2256                .filter(|c| {
2257                    matches!(
2258                        c,
2259                        LinkCommand::Levels { .. } | LinkCommand::WatchLevels { .. }
2260                    )
2261                })
2262                .collect::<Vec<_>>()
2263        };
2264        let f = Frame(1_000, 1, 2, 3);
2265
2266        reg.levels("lv", "lv-phone", f);
2267        assert!(levels(&mut mac).is_empty(), "nobody watching");
2268
2269        reg.watch_levels("lv", "lv-mac", "lv-phone", true);
2270        assert_eq!(
2271            levels(&mut phone),
2272            vec![LinkCommand::WatchLevels { on: true }]
2273        );
2274        reg.levels("lv", "lv-phone", f);
2275        assert_eq!(
2276            levels(&mut mac),
2277            vec![LinkCommand::Levels {
2278                from: "lv-phone".into(),
2279                f
2280            }]
2281        );
2282
2283        // The watcher's link goes: the phone is told to stop.
2284        reg.unregister(&mac_id);
2285        assert_eq!(
2286            levels(&mut phone),
2287            vec![LinkCommand::WatchLevels { on: false }]
2288        );
2289        reg.watch_levels("lv", "lv-mac", "lv-phone", true);
2290        reg.watch_levels("lv", "lv-mac", "lv-phone", false);
2291        assert_eq!(
2292            levels(&mut phone),
2293            vec![
2294                LinkCommand::WatchLevels { on: true },
2295                LinkCommand::WatchLevels { on: false }
2296            ]
2297        );
2298    }
2299
2300    #[test]
2301    fn a_playlist_order_adds_the_named_tracks_once_they_arrive() {
2302        let dir = tempfile::tempdir().unwrap();
2303        let path = dir.path().join("koan.db");
2304        let db = koan_core::db::connection::Database::open(&path).unwrap();
2305        let playlist = koan_core::db::queries::create_playlist(
2306            &db.conn,
2307            koan_core::db::queries::LOCAL_USER,
2308            "cyberpunk",
2309            None,
2310        )
2311        .unwrap();
2312        let order = Order {
2313            id: "o1".into(),
2314            username: None,
2315            client: None,
2316            artist: "Perturbator".into(),
2317            album: "Dangerous Days".into(),
2318            play_next: false,
2319            playlist: Some(playlist),
2320            titles: vec!["Future Club".into()],
2321            created_at: chrono::Utc::now().timestamp(),
2322        };
2323        registry().add_order(order);
2324
2325        // Not in the library yet: nothing happens, and the order waits.
2326        fulfil_from(&path);
2327        assert!(registry().orders(None).iter().any(|o| o.id == "o1"));
2328
2329        db.conn
2330            .execute_batch(
2331                "INSERT INTO artists (id, name) VALUES (1, 'Perturbator');
2332                 INSERT INTO albums (id, title, artist_id) VALUES (1, 'Dangerous Days', 1);
2333                 INSERT INTO tracks (id, title, album_id, artist_id, track_number, path) VALUES
2334                   (1, 'Welcome Back', 1, 1, 1, '/1.flac'), (2, 'Future Club', 1, 1, 2, '/2.flac');",
2335            )
2336            .unwrap();
2337        fulfil_from(&path);
2338        let held: Vec<i64> = db
2339            .conn
2340            .prepare("SELECT track_id FROM playlist_tracks WHERE playlist_id = ?1")
2341            .unwrap()
2342            .query_map([playlist], |r| r.get(0))
2343            .unwrap()
2344            .collect::<Result<_, _>>()
2345            .unwrap();
2346        assert_eq!(held, [2]);
2347        assert!(!registry().orders(None).iter().any(|o| o.id == "o1"));
2348    }
2349
2350    use super::*;
2351
2352    #[test]
2353    fn a_notification_cover_is_named_by_the_uid_the_command_carries() {
2354        // Commands carry uids; parsing them as row ids found no cover.
2355        let uid = "0190a5b2-7c3d-7e4f-8a1b-2c3d4e5f6a7b".to_string();
2356        let play = LinkCommand::Play {
2357            track_ids: vec!["x".into(), uid.clone()],
2358            start_at: 1,
2359            position_ms: 0,
2360            paused: false,
2361            handoff: false,
2362            mode: koan_core::player::state::QueueMode::Keep,
2363        };
2364        assert_eq!(cover_track(&play), Some(uid.as_str()));
2365    }
2366
2367    #[test]
2368    fn a_reconnect_replaces_the_device_and_commands_reach_it() {
2369        let reg = Registry::default();
2370        let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel();
2371        let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2372        let (tx3, _rx3) = tokio::sync::mpsc::unbounded_channel();
2373        reg.register("j", "phone", "ios", "dev-1", tx1, false, false);
2374        let id = reg.register("j", "phone", "ios", "dev-1", tx2, false, false);
2375        reg.register("someone", "laptop", "macos", "dev-2", tx3, false, false);
2376
2377        assert_eq!(reg.list(Some("j")).len(), 1);
2378        assert_eq!(reg.list(None).len(), 2);
2379
2380        let sent = reg.send(Some("j"), None, LinkCommand::Pause).unwrap();
2381        assert_eq!(sent.id, id);
2382        assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2383
2384        // Another account's device is not this account's to command.
2385        assert!(
2386            reg.send(Some("j"), Some("laptop"), LinkCommand::Pause)
2387                .is_err()
2388        );
2389
2390        reg.unregister(&id);
2391        assert!(reg.send(Some("j"), None, LinkCommand::Pause).is_err());
2392    }
2393
2394    #[test]
2395    fn disconnecting_an_account_closes_only_its_links() {
2396        let reg = Registry::default();
2397        let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel();
2398        let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2399        reg.register("j", "phone", "ios", "dev-1", tx1, false, false);
2400        reg.register("someone", "laptop", "macos", "dev-2", tx2, false, false);
2401
2402        reg.disconnect("j");
2403        assert!(reg.list(Some("j")).is_empty());
2404        // The session sees its channel close, which is what ends it.
2405        assert!(matches!(
2406            rx1.try_recv(),
2407            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
2408        ));
2409        assert_eq!(reg.list(Some("someone")).len(), 1);
2410        assert!(matches!(
2411            rx2.try_recv(),
2412            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2413        ));
2414    }
2415
2416    #[test]
2417    fn a_burst_of_changes_wakes_each_device_once_when_it_goes_quiet() {
2418        let t0 = std::time::Instant::now();
2419        let s = std::time::Duration::from_secs;
2420        let phone = || ("dev-1".to_string(), "j".to_string());
2421        let ipad = || ("dev-2".to_string(), "j".to_string());
2422        let mut wakes = Wakes {
2423            pending: Vec::new(),
2424            timer: false,
2425        };
2426
2427        wakes.add([phone()], t0);
2428        wakes.add([phone(), ipad()], t0 + s(10));
2429        wakes.add([phone()], t0 + s(20));
2430        assert_eq!(wakes.pending.len(), 2);
2431
2432        // Quiet is measured from each device's last change.
2433        assert!(wakes.take_due(t0 + s(39)).is_empty());
2434        assert_eq!(wakes.take_due(t0 + s(40)), [ipad()]);
2435        assert_eq!(wakes.next(), Some(t0 + s(50)));
2436        assert_eq!(wakes.take_due(t0 + s(50)), [phone()]);
2437        assert_eq!(wakes.next(), None);
2438    }
2439
2440    #[test]
2441    fn a_library_that_never_goes_quiet_still_wakes_devices() {
2442        let t0 = std::time::Instant::now();
2443        let phone = || ("dev-1".to_string(), "j".to_string());
2444        let mut wakes = Wakes {
2445            pending: Vec::new(),
2446            timer: false,
2447        };
2448        let mut sent = 0;
2449        for i in 0..40 {
2450            let now = t0 + std::time::Duration::from_secs(i * 10);
2451            sent += wakes.take_due(now).len();
2452            wakes.add([phone()], now);
2453        }
2454        // Six and a half minutes of changes every ten seconds: one push at
2455        // the five-minute mark, and one pending.
2456        assert_eq!(sent, 1);
2457        assert_eq!(wakes.pending.len(), 1);
2458    }
2459
2460    #[test]
2461    fn the_device_playing_is_the_one_meant() {
2462        let reg = Registry::default();
2463        let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel();
2464        let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2465        let mac = reg.register("j", "mac", "macos", "dev-1", tx1, false, false);
2466        let phone = reg.register("j", "phone", "ios", "dev-2", tx2, false, false);
2467
2468        // Two idle devices: no telling, so the caller is told to ask.
2469        let err = reg.send(Some("j"), None, LinkCommand::Pause).unwrap_err();
2470        assert!(err.contains("mac") && err.contains("phone"), "{err}");
2471
2472        reg.report(
2473            &phone,
2474            LinkState {
2475                playing: true,
2476                ..Default::default()
2477            },
2478        );
2479        assert_eq!(
2480            reg.send(Some("j"), None, LinkCommand::Pause).unwrap().id,
2481            phone
2482        );
2483        assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2484
2485        // Stopped a moment ago: still the one meant, over the Mac.
2486        reg.report(&phone, LinkState::default());
2487        assert_eq!(
2488            reg.send(Some("j"), None, LinkCommand::Pause).unwrap().id,
2489            phone
2490        );
2491        let _ = mac;
2492    }
2493
2494    fn drain(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>) -> Vec<LinkCommand> {
2495        std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command)).collect()
2496    }
2497
2498    /// `j` shares the phone with `k`, not the Mac.
2499    fn shared() -> (
2500        Registry,
2501        tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2502        tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2503        tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2504    ) {
2505        let reg = Registry::default();
2506        let (phone_tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2507        let (mac_tx, mac) = tokio::sync::mpsc::unbounded_channel();
2508        let (k_tx, k) = tokio::sync::mpsc::unbounded_channel();
2509        let id = reg.register("j", "phone", "ios", "dev-phone", phone_tx, true, false);
2510        reg.register("j", "mac", "macos", "dev-mac", mac_tx, true, false);
2511        reg.register("k", "laptop", "macos", "dev-k", k_tx, true, false);
2512        reg.report(
2513            &id,
2514            LinkState {
2515                playing: true,
2516                title: Some("Roygbiv".into()),
2517                outputs: Some(Default::default()),
2518                ..Default::default()
2519            },
2520        );
2521        reg.share("j", "dev-phone", "k", true).unwrap();
2522        drain(&mut phone);
2523        (reg, phone, mac, k)
2524    }
2525
2526    #[test]
2527    fn a_grantee_sees_the_shared_device_and_nothing_else_of_the_owner() {
2528        let (_reg, _phone, _mac, mut k) = shared();
2529        let listed = drain(&mut k)
2530            .into_iter()
2531            .rev()
2532            .find_map(|c| match c {
2533                LinkCommand::Devices { devices } => Some(devices),
2534                _ => None,
2535            })
2536            .expect("told of it");
2537        assert_eq!(listed.len(), 1, "the phone, not the Mac");
2538        let phone = &listed[0];
2539        assert_eq!(phone.id, "dev-phone");
2540        assert_eq!(phone.owner.as_deref(), Some("j"));
2541        let state = phone.state.as_ref().expect("its state");
2542        assert_eq!(state.title.as_deref(), Some("Roygbiv"));
2543        assert!(
2544            state.outputs.is_some(),
2545            "outputs, to choose from: the output is in the playback set"
2546        );
2547    }
2548
2549    /// A granted account (`k`, say read-only) controlling the owner's (`j`,
2550    /// say admin) phone gets the playback set and nothing of `j`'s account:
2551    /// what touches the library is refused, and every command arrives marked
2552    /// as `k`'s, so the phone runs it with no power to sync, and no command
2553    /// in the set writes favourites, playlists or history.
2554    #[test]
2555    fn a_grantee_controls_playback_as_itself_and_nothing_of_the_owners_account() {
2556        let (reg, mut phone, mut mac, _k) = shared();
2557        let wrapped = |c: LinkCommand| LinkCommand::Shared {
2558            command: Box::new(c),
2559        };
2560        for cmd in [
2561            LinkCommand::Pause,
2562            LinkCommand::SetRendererVolume { volume: 40 },
2563            LinkCommand::SleepTimer {
2564                timer: Some(koan_core::player::state::SleepTimer::After { minutes: 30 }),
2565            },
2566            LinkCommand::HandOff { to: "dev-k".into() },
2567        ] {
2568            reg.relay("k", "dev-phone", cmd.clone()).unwrap();
2569            assert_eq!(drain(&mut phone), [wrapped(cmd)]);
2570        }
2571        for cmd in [
2572            LinkCommand::Sync { full: false },
2573            LinkCommand::Evict { track_ids: vec![] },
2574            LinkCommand::Shared {
2575                command: Box::new(LinkCommand::Pause),
2576            },
2577        ] {
2578            assert!(!cmd.allowed_playback());
2579            assert!(reg.relay("k", "dev-phone", cmd).is_err());
2580        }
2581        assert!(drain(&mut phone).is_empty());
2582        assert!(
2583            reg.relay("k", "dev-mac", LinkCommand::Pause).is_err(),
2584            "not shared, not reachable"
2585        );
2586        assert!(
2587            drain(&mut mac)
2588                .iter()
2589                .all(|c| matches!(c, LinkCommand::Devices { .. } | LinkCommand::Shares { .. })),
2590            "news, and no command"
2591        );
2592        assert!(reg.shared_owner("k", "dev-mac").is_none(), "nor wakeable");
2593    }
2594
2595    /// "Move here" both ways: the grantee pulls the shared phone's music to
2596    /// its own device, which the phone sends as a hand-off; the grant lets
2597    /// that one command run backwards and nothing else.
2598    #[test]
2599    fn a_hand_off_runs_both_ways_across_a_grant_and_nothing_else_does() {
2600        let (reg, _phone, _mac, mut k) = shared();
2601        drain(&mut k);
2602        let play = LinkCommand::Play {
2603            track_ids: vec!["t".into()],
2604            start_at: 0,
2605            position_ms: 1000,
2606            paused: false,
2607            handoff: true,
2608            mode: koan_core::player::state::QueueMode::Keep,
2609        };
2610        reg.relay_from("j", Some("dev-phone"), "dev-k", play.clone())
2611            .unwrap();
2612        assert!(drain(&mut k).contains(&LinkCommand::Shared {
2613            command: Box::new(play.clone())
2614        }));
2615        assert!(
2616            reg.relay_from("j", Some("dev-phone"), "dev-k", LinkCommand::Pause)
2617                .is_err(),
2618            "the owner's phone does not command the grantee's devices"
2619        );
2620        assert!(
2621            reg.relay_from("j", Some("dev-mac"), "dev-k", play.clone())
2622                .is_err(),
2623            "only the shared device"
2624        );
2625        let not_a_hand_off = LinkCommand::Play {
2626            track_ids: vec!["t".into()],
2627            start_at: 0,
2628            position_ms: 0,
2629            paused: false,
2630            handoff: false,
2631            mode: koan_core::player::state::QueueMode::Keep,
2632        };
2633        assert!(
2634            reg.relay_from("j", Some("dev-phone"), "dev-k", not_a_hand_off)
2635                .is_err(),
2636            "a hand-off, not any play"
2637        );
2638    }
2639
2640    #[test]
2641    fn revoking_ends_control_at_once() {
2642        let (reg, mut phone, _mac, mut k) = shared();
2643        reg.share("j", "dev-phone", "k", false).unwrap();
2644        assert!(reg.relay("k", "dev-phone", LinkCommand::Pause).is_err());
2645        assert!(
2646            drain(&mut phone)
2647                .iter()
2648                .all(|c| !matches!(c, LinkCommand::Pause))
2649        );
2650        assert!(reg.shared_owner("k", "dev-phone").is_none());
2651        let listed = drain(&mut k)
2652            .into_iter()
2653            .rev()
2654            .find_map(|c| match c {
2655                LinkCommand::Devices { devices } => Some(devices),
2656                _ => None,
2657            })
2658            .expect("told it is gone");
2659        assert!(listed.is_empty());
2660    }
2661
2662    #[test]
2663    fn a_device_hears_whom_it_is_shared_with_every_time_it_links() {
2664        let reg = Registry::default();
2665        let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2666        reg.register("j", "phone", "ios", "dev-phone", tx, true, false);
2667        let first = drain(&mut phone);
2668        assert!(
2669            first.contains(&LinkCommand::Shares {
2670                grantees: vec![],
2671                error: None,
2672                accounts: vec![],
2673            }),
2674            "an empty list replaces one from another server"
2675        );
2676        reg.send_shares(
2677            "j",
2678            "dev-phone",
2679            Some("There is no account called x".into()),
2680        );
2681        assert!(drain(&mut phone).iter().any(
2682            |c| matches!(c, LinkCommand::Shares { error: Some(e), .. } if e.contains("no account"))
2683        ));
2684    }
2685
2686    #[test]
2687    fn the_owner_is_told_who_it_shares_with() {
2688        let reg = Registry::default();
2689        let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2690        reg.register("j", "phone", "ios", "dev-phone", tx, true, false);
2691        reg.share("j", "dev-phone", "k", true).unwrap();
2692        reg.share("j", "dev-phone", "m", true).unwrap();
2693        let last = drain(&mut phone).into_iter().rev().find_map(|c| match c {
2694            LinkCommand::Shares { grantees, .. } => Some(grantees),
2695            _ => None,
2696        });
2697        assert_eq!(last, Some(vec!["k".to_string(), "m".to_string()]));
2698        assert!(
2699            reg.share("j", "dev-phone", "j", true).is_err(),
2700            "not with itself"
2701        );
2702    }
2703
2704    /// Whose token wakes a device: another account's, behind the same router
2705    /// as the asker; a shared one's owner from anywhere; and otherwise only
2706    /// the asker's own, so a device elsewhere of another account is refused.
2707    #[test]
2708    fn a_device_is_woken_for_another_account_on_its_network_or_by_grant() {
2709        let reg = Registry::default();
2710        let home: std::net::IpAddr = "203.0.113.7".parse().unwrap();
2711        let away: std::net::IpAddr = "198.51.100.2".parse().unwrap();
2712        reg.seen_at("dev-ipad", "sarita", home);
2713        reg.seen_at("dev-phone", "admin", home);
2714        assert_eq!(
2715            reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2716            "sarita",
2717            "same address: the iPad's own token"
2718        );
2719        reg.seen_at("dev-phone", "admin", away);
2720        assert_eq!(
2721            reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2722            "admin",
2723            "elsewhere, with no grant: only admin's own, which it is not"
2724        );
2725        reg.share("sarita", "dev-ipad", "admin", true).unwrap();
2726        assert_eq!(
2727            reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2728            "sarita",
2729            "shared: from anywhere"
2730        );
2731        // An address is believed only for the account that linked from it.
2732        reg.seen_at("dev-other", "mallory", home);
2733        assert_eq!(reg.wake_owner("admin", "dev-other", "dev-tv"), "admin");
2734    }
2735
2736    /// The server restarts with every release; a backgrounded iPad has to
2737    /// stay wakeable across one without being opened again.
2738    #[test]
2739    fn where_a_device_last_linked_from_survives_a_restart() {
2740        let conn = rusqlite::Connection::open_in_memory().unwrap();
2741        koan_core::db::schema::create_tables(&conn).unwrap();
2742        for (device, user, seen) in [("dev-ipad", "sarita", 1), ("dev-phone", "admin", 2)] {
2743            conn.execute(
2744                "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, ?2, ?1, 'ios', ?3)",
2745                rusqlite::params![device, user, seen],
2746            )
2747            .unwrap();
2748        }
2749        let home: std::net::IpAddr = "203.0.113.7".parse().unwrap();
2750        outbox::save_address_in(&conn, "dev-ipad", "sarita", home);
2751        outbox::save_address_in(&conn, "dev-phone", "admin", home);
2752
2753        // A fresh server, from what was saved.
2754        let reg = Registry::default();
2755        *reg.addresses.lock() = outbox::load_addresses_in(&conn);
2756        assert_eq!(
2757            reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2758            "sarita",
2759            "still woken through its own account after the restart"
2760        );
2761    }
2762
2763    /// Forgetting a device tells the account's linked devices to drop it,
2764    /// and no other account's; a linked device is not forgotten, since it
2765    /// would be back at once.
2766    #[test]
2767    fn a_forgotten_device_is_dropped_by_the_accounts_devices() {
2768        let reg = Registry::default();
2769        let (mac_tx, mut mac) = tokio::sync::mpsc::unbounded_channel();
2770        let (other_tx, mut other) = tokio::sync::mpsc::unbounded_channel();
2771        reg.register("j", "mac", "macos", "dev-mac", mac_tx, true, false);
2772        reg.register("k", "laptop", "macos", "dev-k", other_tx, true, false);
2773        while mac.try_recv().is_ok() {}
2774        while other.try_recv().is_ok() {}
2775
2776        reg.forget("j", "dev-phone").unwrap();
2777        let heard: Vec<LinkCommand> =
2778            std::iter::from_fn(|| mac.try_recv().ok().map(|e| e.command)).collect();
2779        assert!(heard.contains(&LinkCommand::Forgotten {
2780            device: "dev-phone".into()
2781        }));
2782        assert!(
2783            std::iter::from_fn(|| other.try_recv().ok().map(|e| e.command))
2784                .all(|c| !matches!(c, LinkCommand::Forgotten { .. })),
2785            "another account hears nothing of it"
2786        );
2787        assert!(
2788            reg.forget("j", "dev-mac").is_err(),
2789            "linked: it would be back"
2790        );
2791        assert!(
2792            reg.relay(
2793                "j",
2794                "dev-mac",
2795                LinkCommand::Forgotten { device: "x".into() }
2796            )
2797            .is_err(),
2798            "news from the server, not a command a device may send"
2799        );
2800    }
2801
2802    /// A device shared by another account is forgotten by declining the
2803    /// share: its owner keeps it and is told, and it is not listed again.
2804    #[test]
2805    fn forgetting_a_shared_device_declines_the_share() {
2806        let (reg, mut phone, _mac, mut k) = shared();
2807        drain(&mut k);
2808        reg.forget("k", "dev-phone").unwrap();
2809        assert!(reg.shared_owner("k", "dev-phone").is_none());
2810        assert!(
2811            drain(&mut phone)
2812                .iter()
2813                .any(|c| matches!(c, LinkCommand::Shares { grantees, .. } if grantees.is_empty()))
2814        );
2815        let listed = drain(&mut k).into_iter().rev().find_map(|c| match c {
2816            LinkCommand::Devices { devices } => Some(devices),
2817            _ => None,
2818        });
2819        assert_eq!(listed.map(|d| d.len()), Some(0));
2820    }
2821
2822    /// The server forgets a device's push token with it, so nothing is
2823    /// pushed to it again; and only the account that owns it can.
2824    #[test]
2825    fn forgetting_a_device_stops_pushes_to_it_and_only_for_its_account() {
2826        let conn = rusqlite::Connection::open_in_memory().unwrap();
2827        koan_core::db::schema::create_tables(&conn).unwrap();
2828        for user in ["j", "k"] {
2829            conn.execute(
2830                "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES ('dev-phone', ?1, 'phone', 'ios', 1)",
2831                [user],
2832            )
2833            .unwrap();
2834            conn.execute(
2835                "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES ('dev-phone', ?1, 't', 0, 1)",
2836                [user],
2837            )
2838            .unwrap();
2839        }
2840        outbox::forget_device_in(&conn, "dev-phone", "k");
2841        assert_eq!(
2842            outbox::push_targets_in(&conn, Some("j")).len(),
2843            1,
2844            "k forgetting its own leaves j's alone"
2845        );
2846        outbox::forget_device_in(&conn, "dev-phone", "j");
2847        assert!(outbox::push_targets_in(&conn, Some("j")).is_empty());
2848        assert!(outbox::push_targets_in(&conn, None).is_empty());
2849    }
2850
2851    #[test]
2852    fn a_device_hears_its_peers_and_commands_reach_them_by_device_id() {
2853        let reg = Registry::default();
2854        let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel();
2855        let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2856        let (tx3, mut rx3) = tokio::sync::mpsc::unbounded_channel();
2857        reg.register("j", "mac", "macos", "dev-mac", tx1, true, false);
2858        let phone = reg.register("j", "phone", "ios", "dev-phone", tx2, true, false);
2859        reg.register("someone", "laptop", "macos", "dev-other", tx3, true, false);
2860
2861        reg.report(
2862            &phone,
2863            LinkState {
2864                playing: true,
2865                title: Some("Roygbiv".into()),
2866                ..Default::default()
2867            },
2868        );
2869        let devices = drain(&mut rx1)
2870            .into_iter()
2871            .rev()
2872            .find_map(|c| match c {
2873                LinkCommand::Devices { devices } => Some(devices),
2874                _ => None,
2875            })
2876            .expect("the Mac is told of the phone");
2877        assert_eq!(devices.len(), 1, "not itself, not another account's");
2878        assert_eq!(devices[0].id, "dev-phone");
2879        assert_eq!(
2880            devices[0].state.as_ref().and_then(|s| s.title.as_deref()),
2881            Some("Roygbiv")
2882        );
2883        while rx3.try_recv().is_ok() {}
2884        assert!(rx3.try_recv().is_err());
2885
2886        while rx2.try_recv().is_ok() {}
2887        reg.relay("j", "dev-phone", LinkCommand::Pause).unwrap();
2888        assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2889        assert!(
2890            reg.relay("someone", "dev-phone", LinkCommand::Pause)
2891                .is_err(),
2892            "another account cannot reach it"
2893        );
2894        assert!(
2895            reg.relay("j", "dev-phone", LinkCommand::Devices { devices: vec![] })
2896                .is_err()
2897        );
2898    }
2899
2900    #[test]
2901    fn a_device_linked_for_a_month_is_not_forgotten() {
2902        let conn = rusqlite::Connection::open_in_memory().unwrap();
2903        koan_core::db::schema::create_tables(&conn).unwrap();
2904        let now = 100 * 24 * 60 * 60;
2905        let long_ago = now - 40 * 24 * 60 * 60;
2906        for device in ["mac", "old-phone"] {
2907            conn.execute(
2908                "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, 'j', ?1, 'ios', ?2)",
2909                rusqlite::params![device, long_ago],
2910            )
2911            .unwrap();
2912            conn.execute(
2913                "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES (?1, 'j', 't', 0, ?2)",
2914                rusqlite::params![device, long_ago],
2915            )
2916            .unwrap();
2917        }
2918        outbox::forget_stale(&conn, &[("mac".into(), "j".into())], now);
2919        let left = |table: &str| -> Vec<String> {
2920            conn.prepare(&format!("SELECT device FROM {table} ORDER BY device"))
2921                .unwrap()
2922                .query_map([], |r| r.get(0))
2923                .unwrap()
2924                .collect::<Result<_, _>>()
2925                .unwrap()
2926        };
2927        assert_eq!(left("link_devices"), ["mac"]);
2928        assert_eq!(left("link_push"), ["mac"]);
2929    }
2930}