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