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