Skip to main content

koan_server/
clients.rs

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