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