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;
16use tokio::sync::mpsc::UnboundedSender;
17
18#[derive(Debug, Clone)]
20pub struct ClientInfo {
21 pub id: String,
22 pub device: String,
24 pub name: String,
25 pub platform: String,
26 pub username: String,
27 pub connected_at: i64,
29 pub state: LinkState,
31 pub last_played_at: Option<i64>,
33 pub state_at: i64,
35 pub reports: bool,
38 pub notified: bool,
41 pub acks: bool,
43}
44
45impl ClientInfo {
46 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 device: String,
66 tx: UnboundedSender<Envelope>,
67 wants_devices: bool,
70 wants_keys: bool,
73}
74
75pub struct Acking {
78 pub id: u64,
79 pub reply: Box<dyn FnOnce(Option<AckOutcome>) + Send>,
80}
81
82struct Waiting {
84 reply: Box<dyn FnOnce(Option<AckOutcome>) + Send>,
85 received: bool,
87 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
107const FIRST_LOOK: std::time::Duration = std::time::Duration::from_millis(2500);
112const LONGEST_ANSWER: std::time::Duration = std::time::Duration::from_secs(120);
113
114struct Activity {
117 username: String,
118 watcher: String,
120 target: String,
122 token: String,
123 sandbox: bool,
124 sent: Option<crate::push::ActivityState>,
126}
127
128#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
131pub struct Order {
132 pub id: String,
133 pub username: Option<String>,
135 pub client: Option<String>,
137 pub artist: String,
138 pub album: String,
139 pub play_next: bool,
141 #[serde(default)]
143 pub playlist: Option<i64>,
144 #[serde(default)]
147 pub titles: Vec<String>,
148 pub created_at: i64,
150}
151
152const ORDER_TTL: i64 = 24 * 60 * 60;
154
155#[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 level_watches: Mutex<Vec<LevelWatch>>,
174 addresses: Mutex<std::collections::HashMap<String, (String, std::net::IpAddr)>>,
177 answers: Mutex<std::collections::HashMap<(String, String, u64), Waiting>>,
182}
183
184struct LevelWatch {
188 owner: String,
189 watcher_user: String,
190 watcher: String,
191 target: String,
192}
193
194pub 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 ®ISTRY
205}
206
207impl Registry {
208 #[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 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 self.send_shares(username, device, None);
263 self.announce(username);
264 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 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 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 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 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 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 pub fn set_push(&self, username: &str, device: &str, token: &str, sandbox: bool) {
341 outbox::save_push(username, device, token, sandbox);
342 }
343
344 pub fn disconnect(&self, username: &str) {
347 self.entries.lock().retain(|e| e.info.username != username);
348 }
349
350 pub fn unregister(&self, id: &str) {
351 let mut entries = self.entries.lock();
352 let gone = entries
353 .iter()
354 .find(|e| e.info.id == id)
355 .map(|e| (e.device.clone(), e.info.username.clone()));
356 entries.retain(|e| e.info.id != id);
357 drop(entries);
358 if let Some((device, username)) = gone {
359 outbox::touch(&[(device.clone(), username.clone())]);
360 self.announce(&username);
361 let targets: Vec<String> = self
363 .level_watches
364 .lock()
365 .iter()
366 .filter(|w| w.watcher_user == username && w.watcher == device)
367 .map(|w| w.target.clone())
368 .collect();
369 for target in targets {
370 self.watch_levels(&username, &device, &target, false);
371 }
372 }
373 }
374
375 pub fn watch_levels(&self, username: &str, watcher: &str, target: &str, on: bool) {
380 let owner = self
382 .shared_owner(username, target)
383 .unwrap_or_else(|| username.to_string());
384 let mut watches = self.level_watches.lock();
385 watches.retain(|w| {
386 !(w.watcher_user == username && w.watcher == watcher && w.target == target)
387 });
388 if on {
389 watches.push(LevelWatch {
390 owner: owner.clone(),
391 watcher_user: username.into(),
392 watcher: watcher.into(),
393 target: target.into(),
394 });
395 }
396 let watched = watches
397 .iter()
398 .any(|w| w.owner == owner && w.target == target);
399 drop(watches);
400 if on || !watched {
401 self.send_live(&owner, target, LinkCommand::WatchLevels { on: watched });
402 }
403 }
404
405 pub fn levels(&self, username: &str, from: &str, f: koan_core::remote::levels::Frame) {
408 let watchers: Vec<(String, String)> = self
409 .level_watches
410 .lock()
411 .iter()
412 .filter(|w| w.owner == username && w.target == from)
413 .map(|w| (w.watcher_user.clone(), w.watcher.clone()))
414 .collect();
415 for (watcher_user, watcher) in watchers {
416 self.send_live(
417 &watcher_user,
418 &watcher,
419 LinkCommand::Levels {
420 from: from.to_string(),
421 f,
422 },
423 );
424 }
425 }
426
427 pub(crate) fn send_live(&self, username: &str, device: &str, cmd: LinkCommand) {
429 if let Some(e) = self
430 .entries
431 .lock()
432 .iter()
433 .find(|e| e.info.username == username && e.device == device)
434 {
435 let _ = e.tx.send(cmd.into());
436 }
437 }
438
439 fn announce(&self, username: &str) {
443 self.announce_to(username);
444 let grantees: Vec<String> = {
446 let mut g: Vec<String> = self
447 .grants
448 .lock()
449 .iter()
450 .filter(|g| g.owner == username)
451 .map(|g| g.grantee.clone())
452 .collect();
453 g.sort();
454 g.dedup();
455 g
456 };
457 for grantee in grantees {
458 self.announce_to(&grantee);
459 }
460 }
461
462 fn announce_to(&self, username: &str) {
464 let listening = |e: &Entry| e.info.username == username && e.wants_devices;
465 if !self.entries.lock().iter().any(listening) {
466 return;
467 }
468 let asleep = outbox::push_targets(Some(username));
469 let can_push = crate::push::pusher().is_some();
471 let entries = self.entries.lock();
472 let ours: Vec<&Entry> = entries
473 .iter()
474 .filter(|e| e.info.username == username)
475 .collect();
476 if !ours.iter().any(|e| e.wants_devices) {
477 return;
478 }
479 let mut all: Vec<LinkDevice> = ours
480 .iter()
481 .map(|e| LinkDevice {
482 id: e.device.clone(),
483 name: e.info.name.clone(),
484 platform: e.info.platform.clone(),
485 linked: true,
486 state: e.info.reports.then(|| LinkState {
487 position_ms: e.info.position_ms(),
488 ..e.info.state.clone()
489 }),
490 last_seen: None,
491 wakeable: Some(can_push && asleep.iter().any(|t| t.device == e.device)),
492 owner: None,
493 acks: e.info.acks,
494 })
495 .collect();
496 for t in asleep {
497 if !all.iter().any(|d| d.id == t.device) {
498 all.push(LinkDevice {
499 id: t.device,
500 name: t.name,
501 platform: t.platform,
502 linked: false,
503 state: None,
504 last_seen: Some(t.last_seen),
505 wakeable: Some(can_push),
506 owner: None,
507 acks: false,
508 });
509 }
510 }
511 all.extend(self.shared_devices(username, &entries, can_push));
512 for e in ours.iter().filter(|e| e.wants_devices) {
513 let devices = all.iter().filter(|d| d.id != e.device).cloned().collect();
514 let _ = e.tx.send(LinkCommand::Devices { devices }.into());
515 }
516 }
517
518 fn shared_devices(&self, grantee: &str, entries: &[Entry], can_push: bool) -> Vec<LinkDevice> {
522 let grants: Vec<Grant> = self
523 .grants
524 .lock()
525 .iter()
526 .filter(|g| g.grantee == grantee)
527 .cloned()
528 .collect();
529 let mut out = Vec::new();
530 for g in grants {
531 let linked = entries
532 .iter()
533 .find(|e| e.device == g.device && e.info.username == g.owner);
534 match linked {
535 Some(e) => out.push(LinkDevice {
536 id: e.device.clone(),
537 name: e.info.name.clone(),
538 platform: e.info.platform.clone(),
539 linked: true,
540 state: e.info.reports.then(|| LinkState {
541 position_ms: e.info.position_ms(),
542 ..e.info.state.clone()
543 }),
544 last_seen: None,
545 wakeable: Some(can_push),
546 owner: Some(g.owner.clone()),
547 acks: e.info.acks,
548 }),
549 None => {
550 if let Some(t) = outbox::push_targets(Some(&g.owner))
551 .into_iter()
552 .find(|t| t.device == g.device)
553 {
554 out.push(LinkDevice {
555 id: t.device,
556 name: t.name,
557 platform: t.platform,
558 linked: false,
559 state: None,
560 last_seen: Some(t.last_seen),
561 wakeable: Some(can_push),
562 owner: Some(g.owner.clone()),
563 acks: false,
564 });
565 }
566 }
567 }
568 }
569 out
570 }
571
572 pub fn share(
576 &self,
577 owner: &str,
578 device: &str,
579 grantee: &str,
580 allow: bool,
581 ) -> Result<(), String> {
582 if grantee == owner {
583 return Err("a device is already its own account's".into());
584 }
585 let grant = Grant {
586 device: device.to_string(),
587 owner: owner.to_string(),
588 grantee: grantee.to_string(),
589 };
590 {
591 let mut grants = self.grants.lock();
592 grants.retain(|g| *g != grant);
593 if allow {
594 grants.push(grant.clone());
595 }
596 }
597 if allow {
598 outbox::save_grant(&grant);
599 } else {
600 outbox::drop_grant(&grant);
601 }
602 log::info!(
603 "share: {owner}'s {device} {} {grantee}",
604 if allow {
605 "shared with"
606 } else {
607 "no longer shared with"
608 }
609 );
610 self.send_shares(owner, device, None);
611 self.announce_to(grantee);
612 Ok(())
613 }
614
615 pub fn send_shares(&self, owner: &str, device: &str, error: Option<String>) {
618 let mut grantees: Vec<String> = self
619 .grants
620 .lock()
621 .iter()
622 .filter(|g| g.owner == owner && g.device == device)
623 .map(|g| g.grantee.clone())
624 .collect();
625 grantees.sort();
626 if let Some(e) = self
627 .entries
628 .lock()
629 .iter()
630 .find(|e| e.info.username == owner && e.device == device && e.wants_devices)
631 {
632 let accounts = outbox::accounts()
633 .into_iter()
634 .filter(|a| a != owner)
635 .collect();
636 let _ = e.tx.send(Envelope::from(LinkCommand::Shares {
637 grantees,
638 error,
639 accounts,
640 }));
641 }
642 }
643
644 pub fn seen_at(&self, device: &str, username: &str, addr: std::net::IpAddr) {
646 self.addresses
647 .lock()
648 .insert(device.to_string(), (username.to_string(), addr));
649 outbox::save_address(device, username, addr);
650 }
651
652 fn wake_owner(&self, username: &str, from: &str, to: &str) -> String {
659 if let Some(owner) = self.shared_owner(username, to) {
660 return owner;
661 }
662 let addresses = self.addresses.lock();
663 let here = addresses
664 .get(from)
665 .filter(|(user, _)| user == username)
666 .map(|(_, addr)| *addr);
667 match (here, addresses.get(to)) {
668 (Some(here), Some((owner, there))) if *there == here => owner.clone(),
669 _ => username.to_string(),
670 }
671 }
672
673 fn grantee_of(&self, owner: &str, from: &str, to: &str) -> Option<String> {
676 let to_user = self
677 .entries
678 .lock()
679 .iter()
680 .find(|e| e.device == to && e.info.username != owner)
681 .map(|e| e.info.username.clone())?;
682 self.grants
683 .lock()
684 .iter()
685 .any(|g| g.owner == owner && g.device == from && g.grantee == to_user)
686 .then_some(to_user)
687 }
688
689 fn shared_owner(&self, username: &str, to: &str) -> Option<String> {
692 self.grants
693 .lock()
694 .iter()
695 .find(|g| g.grantee == username && g.device == to)
696 .map(|g| g.owner.clone())
697 }
698
699 pub fn wake(&self, username: &str, from: &str, to: &str, notify: bool) {
705 let from_name = self
706 .list(Some(username))
707 .into_iter()
708 .find(|c| c.device == from)
709 .map_or_else(|| "Another device".to_string(), |c| c.name);
710 let owner = self.wake_owner(username, from, to);
711 if owner != username {
712 log::info!("wake: {to} is {owner}'s, woken for {username}");
713 }
714 let username = &owner;
715 if self.list(Some(username)).iter().any(|c| c.device == to) {
716 log::info!("wake: {to} is already linked");
717 return;
718 }
719 let Some(pusher) = crate::push::pusher() else {
720 log::info!("wake: no push key, so {to} cannot be woken");
721 return;
722 };
723 let Some(target) = outbox::push_targets(Some(username))
724 .into_iter()
725 .find(|t| t.device == to)
726 else {
727 log::info!("wake: {to} has given no push token");
728 return;
729 };
730 let (push, how) = if notify {
731 (
732 crate::push::Push::Summon {
733 title: format!("{from_name} wants to play here"),
734 body: "Tap to open kōan and let it.".into(),
735 },
736 "notification",
737 )
738 } else {
739 (crate::push::Push::WakeNow, "wake push")
740 };
741 WOKEN.lock().insert(
742 (target.device.clone(), target.username.clone()),
743 (std::time::Instant::now(), how),
744 );
745 std::thread::spawn(move || {
746 let started = std::time::Instant::now();
747 deliver_push(pusher, &target, &push);
748 log::info!(
749 "wake: {how} for {} answered by APNs in {}ms",
750 target.name,
751 started.elapsed().as_millis()
752 );
753 });
754 }
755
756 pub fn forget(&self, username: &str, device: &str) -> Result<(), String> {
763 if let Some(owner) = self.shared_owner(username, device) {
766 return self.share(&owner, device, username, false);
767 }
768 if self.list(Some(username)).iter().any(|c| c.device == device) {
769 return Err(format!("{device} is linked; it would be back at once"));
770 }
771 outbox::forget_device(device, username);
772 log::info!("devices: {username} forgot {device}");
773 self.broadcast(
774 Some(username),
775 LinkCommand::Forgotten {
776 device: device.to_string(),
777 },
778 );
779 self.announce(username);
780 Ok(())
781 }
782
783 pub fn relay(
785 &self,
786 username: &str,
787 to: &str,
788 command: LinkCommand,
789 ) -> Result<ClientInfo, String> {
790 self.relay_from(username, None, to, command)
791 }
792
793 pub fn relay_from(
797 &self,
798 username: &str,
799 from: Option<&str>,
800 to: &str,
801 command: LinkCommand,
802 ) -> Result<ClientInfo, String> {
803 self.relay_acked(username, from, to, command, None)
804 }
805
806 pub fn relay_acked(
808 &self,
809 username: &str,
810 from: Option<&str>,
811 to: &str,
812 command: LinkCommand,
813 acking: Option<Acking>,
814 ) -> Result<ClientInfo, String> {
815 if !command.relayable() {
819 return Err("not a command".into());
820 }
821 if let Some(owner) = self.shared_owner(username, to) {
825 if !command.allowed_playback() {
826 return Err(format!("{to} is shared for playback only"));
827 }
828 let command = LinkCommand::Shared {
829 command: Box::new(command),
830 };
831 return self.send_with(Some(&owner), Some(to), command, acking);
832 }
833 if let Some(from) = from
837 && let Some(grantee) = self.grantee_of(username, from, to)
838 {
839 if !matches!(command, LinkCommand::Play { handoff: true, .. }) {
840 return Err(format!("{to} is another account's"));
841 }
842 let command = LinkCommand::Shared {
843 command: Box::new(command),
844 };
845 return self.send_with(Some(&grantee), Some(to), command, acking);
846 }
847 self.send_with(Some(username), Some(to), command, acking)
848 }
849
850 pub fn set_activity(
853 &self,
854 username: &str,
855 watcher: &str,
856 activity: Option<(String, String, bool)>,
857 ) {
858 let mut activities = self.activities.lock();
859 activities.retain(|a| !(a.username == username && a.watcher == watcher));
860 let Some((token, target, sandbox)) = activity else {
861 return;
862 };
863 activities.push(Activity {
864 username: username.to_string(),
865 watcher: watcher.to_string(),
866 target: target.clone(),
867 token,
868 sandbox,
869 sent: None,
870 });
871 drop(activities);
872 self.update_activities(username, &target);
873 }
874
875 fn update_activities(&self, username: &str, target: &str) {
878 let Some(pusher) = crate::push::pusher() else {
879 return;
880 };
881 let owner = self
884 .shared_owner(username, target)
885 .unwrap_or_else(|| username.to_string());
886 let Some(info) = self
887 .list(Some(&owner))
888 .into_iter()
889 .find(|c| c.device == target)
890 else {
891 return;
892 };
893 let watchers: Vec<String> = self
894 .grants
895 .lock()
896 .iter()
897 .filter(|g| g.owner == owner && g.device == target)
898 .map(|g| g.grantee.clone())
899 .chain(std::iter::once(owner.clone()))
900 .collect();
901 let state = crate::push::ActivityState::of(&info);
902 let mut due = Vec::new();
903 for a in self.activities.lock().iter_mut() {
904 if watchers.contains(&a.username)
905 && a.target == target
906 && a.sent.as_ref().is_none_or(|s| s.differs(&state))
907 {
908 a.sent = Some(state.clone());
909 due.push((
910 a.token.clone(),
911 a.sandbox,
912 a.watcher.clone(),
913 a.username.clone(),
914 ));
915 }
916 }
917 if due.is_empty() {
918 return;
919 }
920 std::thread::spawn(move || {
921 for (token, sandbox, watcher, username) in due {
922 let push = crate::push::Push::Activity(state.clone());
923 match pusher.send(&token, sandbox, &push) {
924 crate::push::Outcome::Sent => {}
925 crate::push::Outcome::Gone => {
926 log::info!("push: a Live Activity on {watcher} has ended");
927 registry().set_activity(&username, &watcher, None);
928 }
929 crate::push::Outcome::Failed(e) => {
930 log::warn!("push: Live Activity on {watcher}: {e}");
931 }
932 }
933 }
934 });
935 }
936
937 pub fn list(&self, username: Option<&str>) -> Vec<ClientInfo> {
939 let mut out: Vec<ClientInfo> = self
940 .entries
941 .lock()
942 .iter()
943 .filter(|e| username.is_none_or(|u| e.info.username == u))
944 .map(|e| e.info.clone())
945 .collect();
946 out.sort_by_key(|c| std::cmp::Reverse(c.connected_at));
947 out
948 }
949
950 pub fn send(
955 &self,
956 username: Option<&str>,
957 id: Option<&str>,
958 cmd: LinkCommand,
959 ) -> Result<ClientInfo, String> {
960 self.send_with(username, id, cmd, None)
961 }
962
963 pub fn send_with(
969 &self,
970 username: Option<&str>,
971 id: Option<&str>,
972 cmd: LinkCommand,
973 acking: Option<Acking>,
974 ) -> Result<ClientInfo, String> {
975 let clients = self.list(username);
976 let target = match id {
977 Some(id) => clients
978 .iter()
979 .find(|c| c.id == id || c.device == id || c.name.eq_ignore_ascii_case(id)),
980 None if clients.is_empty() => None,
981 None => Some(pick(&clients, chrono::Utc::now().timestamp())?),
982 };
983 let Some(target) = target else {
985 let reached = reach_absent(username, id, &cmd, acking.as_ref().map(|a| a.id))
986 .unwrap_or_else(|| {
987 Err(match id {
988 Some(id) => format!("no linked client {id}; see `clients`"),
989 None => {
990 "no koan app is linked to this server; open koan on the device".into()
991 }
992 })
993 });
994 if let (Ok(_), Some(acking)) = (&reached, acking) {
995 (acking.reply)(Some(AckOutcome::Queued));
996 }
997 return reached;
998 };
999 let target = target.clone();
1000 let entries = self.entries.lock();
1001 let entry = entries
1002 .iter()
1003 .find(|e| e.info.id == target.id)
1004 .ok_or("that client has just gone")?;
1005 let (account, device) = (entry.info.username.clone(), entry.device.clone());
1006 let (ack, unanswered) = match acking {
1010 Some(acking) if entry.info.acks => {
1011 self.answers.lock().insert(
1012 (account.clone(), device.clone(), acking.id),
1013 Waiting {
1014 reply: acking.reply,
1015 received: false,
1016 timer: None,
1017 },
1018 );
1019 (Some(acking.id), None)
1020 }
1021 other => (None, other),
1022 };
1023 let sent = entry.tx.send(Envelope {
1024 command: cmd.clone(),
1025 ack,
1026 });
1027 drop(entries);
1028 if sent.is_err() {
1029 if let Some(w) = ack.and_then(|ack| self.answers.lock().remove(&(account, device, ack)))
1030 {
1031 w.answer(Some(AckOutcome::Failed {
1032 error: "its link has just gone".into(),
1033 }));
1034 }
1035 return Err("that client has just gone".to_string());
1036 }
1037 if let Some(acking) = unanswered {
1038 (acking.reply)(None);
1039 }
1040 if let Some(ack) = ack {
1041 self.watch(account, device, ack, cmd);
1042 }
1043 Ok(target)
1044 }
1045
1046 fn watch(&self, username: String, device: String, ack: u64, cmd: LinkCommand) {
1051 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
1052 return;
1053 };
1054 let key = (username.clone(), device.clone(), ack);
1055 let timer = runtime.spawn(async move {
1056 tokio::time::sleep(FIRST_LOOK).await;
1057 let (looking, looking_in) = (device.clone(), username.clone());
1058 let taken = tokio::task::spawn_blocking(move || {
1059 registry().look(&looking_in, &looking, ack, &cmd)
1060 })
1061 .await
1062 .unwrap_or(false);
1063 if taken {
1064 tokio::time::sleep(LONGEST_ANSWER.saturating_sub(FIRST_LOOK)).await;
1065 registry().give_up(&username, &device, ack);
1066 }
1067 });
1068 match self.answers.lock().get_mut(&key) {
1069 Some(w) => w.timer = Some(timer.abort_handle()),
1070 None => timer.abort(),
1072 }
1073 }
1074
1075 pub fn answered(&self, username: &str, device: &str, ack: u64, outcome: AckOutcome) {
1078 let key = (username.to_string(), device.to_string(), ack);
1079 let waiting = self.answers.lock().remove(&key);
1080 if let Some(w) = waiting {
1081 w.answer(Some(outcome));
1082 }
1083 }
1084
1085 pub fn received(&self, username: &str, device: &str, ack: u64) {
1088 let key = (username.to_string(), device.to_string(), ack);
1089 if let Some(w) = self.answers.lock().get_mut(&key) {
1090 w.received = true;
1091 }
1092 }
1093
1094 fn look(&self, username: &str, device: &str, ack: u64, cmd: &LinkCommand) -> bool {
1102 let key = (username.to_string(), device.to_string(), ack);
1103 let mut answers = self.answers.lock();
1104 match answers.get(&key) {
1105 None => return false,
1106 Some(w) if w.received => return true,
1107 Some(_) => {}
1108 }
1109 let w = answers.remove(&key).expect("just seen");
1110 drop(answers);
1111 log::info!("link: {device} has not taken a command; reaching it as away");
1112 let outcome = match reach_absent(Some(username), Some(device), cmd, Some(ack)) {
1113 Some(Ok(_)) => AckOutcome::Queued,
1114 _ => AckOutcome::Failed {
1115 error: format!("{device} did not take it"),
1116 },
1117 };
1118 w.answer(Some(outcome));
1119 false
1120 }
1121
1122 fn give_up(&self, username: &str, device: &str, ack: u64) {
1125 let key = (username.to_string(), device.to_string(), ack);
1126 let waiting = self.answers.lock().remove(&key);
1127 if let Some(w) = waiting {
1128 w.answer(None);
1129 }
1130 }
1131}
1132
1133impl Registry {
1134 pub fn add_order(&self, order: Order) {
1135 outbox::save_order(&order);
1136 self.orders.lock().push(order);
1137 }
1138
1139 pub fn orders(&self, username: Option<&str>) -> Vec<Order> {
1140 self.orders
1141 .lock()
1142 .iter()
1143 .filter(|o| username.is_none() || o.username.as_deref() == username)
1144 .cloned()
1145 .collect()
1146 }
1147
1148 pub fn cancel_order(&self, username: Option<&str>, id: &str) -> bool {
1149 let mut orders = self.orders.lock();
1150 let before = orders.len();
1151 orders
1152 .retain(|o| !(o.id == id && (username.is_none() || o.username.as_deref() == username)));
1153 let gone = orders.len() != before;
1154 if gone {
1155 outbox::drop_order(id);
1156 }
1157 gone
1158 }
1159
1160 fn done(&self, id: &str) {
1161 self.orders.lock().retain(|o| o.id != id);
1162 outbox::drop_order(id);
1163 }
1164
1165 pub fn fulfil_orders(&self, find: impl Fn(&Order) -> Option<Vec<String>>) {
1168 let now = chrono::Utc::now().timestamp();
1169 let pending: Vec<Order> = {
1170 let mut orders = self.orders.lock();
1171 for o in orders.iter().filter(|o| now - o.created_at >= ORDER_TTL) {
1172 outbox::drop_order(&o.id);
1173 }
1174 orders.retain(|o| now - o.created_at < ORDER_TTL);
1175 orders.clone()
1176 };
1177 for order in pending.into_iter().filter(|o| o.playlist.is_none()) {
1178 let Some(ids) = find(&order).filter(|ids| !ids.is_empty()) else {
1179 continue;
1180 };
1181 let track_ids = ids;
1182 let cmd = if order.play_next {
1183 LinkCommand::PlayNext { track_ids }
1184 } else {
1185 LinkCommand::Enqueue {
1186 track_ids,
1187 play: false,
1188 }
1189 };
1190 match self.send(order.username.as_deref(), order.client.as_deref(), cmd) {
1191 Ok(c) => {
1192 log::info!(
1193 "link: {} — {} arrived; queued on {}",
1194 order.artist,
1195 order.album,
1196 c.name
1197 );
1198 self.done(&order.id);
1199 }
1200 Err(e) => log::info!("link: {} — {} arrived but {e}", order.artist, order.album),
1202 }
1203 }
1204 }
1205}
1206
1207pub fn fulfil_from(db_path: &std::path::Path) {
1209 let registry = registry();
1210 if registry.orders.lock().is_empty() {
1211 return;
1212 }
1213 let Ok(db) = koan_core::db::connection::Database::open_existing(db_path) else {
1214 return;
1215 };
1216 let for_playlists: Vec<Order> = registry
1219 .orders
1220 .lock()
1221 .iter()
1222 .filter(|o| o.playlist.is_some())
1223 .cloned()
1224 .collect();
1225 let mut edited = false;
1226 for order in for_playlists {
1227 let Some(playlist) = order.playlist else {
1228 continue;
1229 };
1230 let Some(ids) = order_tracks(&db.conn, &order) else {
1231 continue;
1232 };
1233 match koan_core::db::queries::add_tracks(&db.conn, playlist, &ids) {
1234 Ok(_) => {
1235 if !cfg!(test) {
1237 koan_core::playlists::push_to_remote(playlist);
1238 }
1239 log::info!(
1240 "link: {} — {} arrived; added {} tracks to playlist {playlist}",
1241 order.artist,
1242 order.album,
1243 ids.len()
1244 );
1245 registry.done(&order.id);
1246 edited = true;
1247 }
1248 Err(e) => log::warn!("link: could not add to playlist {playlist}: {e}"),
1249 }
1250 }
1251 if edited {
1252 changed();
1253 }
1254 registry.fulfil_orders(|order| {
1255 let rows = order_tracks(&db.conn, order)?;
1256 queries::uids_in_order(&db.conn, UidKind::Track, &rows).ok()
1257 });
1258}
1259
1260fn order_tracks(conn: &rusqlite::Connection, order: &Order) -> Option<Vec<i64>> {
1263 let tracks = album_tracks(conn, &order.artist, &order.album)?;
1264 if order.titles.is_empty() {
1265 return Some(tracks.into_iter().map(|(id, _)| id).collect());
1266 }
1267 let picked: Vec<i64> = order
1268 .titles
1269 .iter()
1270 .filter_map(|want| {
1271 let want = want.to_lowercase();
1272 tracks
1273 .iter()
1274 .find(|(_, t)| t.to_lowercase().contains(&want))
1275 .map(|(id, _)| *id)
1276 })
1277 .collect();
1278 (!picked.is_empty()).then_some(picked)
1279}
1280
1281pub fn album_tracks(
1284 conn: &rusqlite::Connection,
1285 artist: &str,
1286 album: &str,
1287) -> Option<Vec<(i64, String)>> {
1288 let like = |s: &str| format!("%{}%", s.replace(['%', '_'], ""));
1289 let album_id: i64 = conn
1290 .query_row(
1291 "SELECT al.id FROM albums al JOIN artists a ON a.id = al.artist_id
1292 WHERE a.name LIKE ?1 COLLATE NOCASE AND al.title LIKE ?2 COLLATE NOCASE
1293 ORDER BY al.id DESC LIMIT 1",
1294 [like(artist), like(album)],
1295 |r| r.get(0),
1296 )
1297 .ok()?;
1298 let mut stmt = conn
1299 .prepare("SELECT id, title FROM tracks WHERE album_id = ?1 ORDER BY disc, track_number, id")
1300 .ok()?;
1301 let tracks = stmt
1302 .query_map([album_id], |r| Ok((r.get(0)?, r.get(1)?)))
1303 .ok()?
1304 .filter_map(Result::ok)
1305 .collect();
1306 Some(tracks)
1307}
1308
1309impl Registry {
1310 pub fn broadcast(&self, username: Option<&str>, cmd: LinkCommand) -> Vec<String> {
1313 let ids: Vec<String> = self.list(username).into_iter().map(|c| c.id).collect();
1314 let entries = self.entries.lock();
1315 entries
1316 .iter()
1317 .filter(|e| ids.contains(&e.info.id) && e.tx.send(cmd.clone().into()).is_ok())
1318 .map(|e| e.info.name.clone())
1319 .collect()
1320 }
1321}
1322
1323impl Registry {
1324 pub fn deliver(&self, username: Option<&str>, cmd: LinkCommand) -> (Vec<String>, Vec<String>) {
1329 let (sent, queued) = self.link_or_queue(username, &cmd);
1330 wake(&queued.iter().map(Absent::key).collect::<Vec<_>>());
1331 (sent, queued.into_iter().map(|q| q.name).collect())
1332 }
1333
1334 fn link_or_queue(
1335 &self,
1336 username: Option<&str>,
1337 cmd: &LinkCommand,
1338 ) -> (Vec<String>, Vec<Absent>) {
1339 let sent = self.broadcast(username, cmd.clone());
1340 let queued = outbox::queue_for_absent(username, &self.live(), cmd);
1341 (sent, queued)
1342 }
1343
1344 fn live(&self) -> Vec<(String, String)> {
1346 self.entries
1347 .lock()
1348 .iter()
1349 .map(|e| (e.device.clone(), e.info.username.clone()))
1350 .collect()
1351 }
1352}
1353
1354impl Absent {
1355 fn key(&self) -> (String, String) {
1356 (self.device.clone(), self.username.clone())
1357 }
1358}
1359
1360fn wake(devices: &[(String, String)]) {
1363 let Some(pusher) = crate::push::pusher() else {
1364 return;
1365 };
1366 let targets: Vec<outbox::PushTarget> = outbox::push_targets(None)
1367 .into_iter()
1368 .filter(|t| {
1369 devices
1370 .iter()
1371 .any(|(device, username)| *device == t.device && *username == t.username)
1372 })
1373 .collect();
1374 if targets.is_empty() {
1375 return;
1376 }
1377 std::thread::spawn(move || {
1378 for t in targets {
1379 deliver_push(pusher, &t, &crate::push::Push::Wake);
1380 }
1381 });
1382}
1383
1384fn reach_absent(
1393 username: Option<&str>,
1394 id: Option<&str>,
1395 cmd: &LinkCommand,
1396 ack: Option<u64>,
1397) -> Option<Result<ClientInfo, String>> {
1398 if cmd.live_only() {
1399 return None;
1400 }
1401 let envelope = Envelope {
1402 command: cmd.clone(),
1403 ack,
1404 };
1405 let pusher = crate::push::pusher()?;
1406 let target = outbox::push_targets(username)
1409 .into_iter()
1410 .find(|t| id.is_none_or(|id| t.device == id || t.name.eq_ignore_ascii_case(id)))?;
1411 let info = ClientInfo {
1412 id: target.device.clone(),
1413 device: target.device.clone(),
1414 name: target.name.clone(),
1415 platform: target.platform.clone(),
1416 username: target.username.clone(),
1417 connected_at: 0,
1418 state: LinkState::default(),
1419 last_played_at: None,
1420 state_at: 0,
1421 reports: false,
1422 notified: true,
1423 acks: false,
1424 };
1425 let asked = match cmd {
1427 LinkCommand::Shared { command } => command.as_ref(),
1428 cmd => cmd,
1429 };
1430 let verb = match asked {
1431 LinkCommand::Play { .. } | LinkCommand::JumpTo { .. } | LinkCommand::Resume => Some("Play"),
1432 _ => None,
1433 };
1434 let push = match verb {
1435 Some(verb) => crate::push::Push::Notify {
1436 title: format!("{verb} on {}", target.name),
1437 body: outbox::describe(asked).unwrap_or_else(|| "From your koan server".into()),
1438 command: serde_json::to_value(&envelope).ok()?,
1439 image: cover_track(asked)
1440 .and_then(outbox::track_row)
1441 .and_then(|t| pusher.cover_link(t)),
1442 },
1443 None => {
1444 outbox::queue_for(&target.device, &target.username, &envelope);
1445 crate::push::Push::Wake
1446 }
1447 };
1448 std::thread::spawn(move || deliver_push(pusher, &target, &push));
1449 Some(Ok(info))
1450}
1451
1452fn cover_track(cmd: &LinkCommand) -> Option<&str> {
1455 match cmd {
1456 LinkCommand::Play {
1457 track_ids,
1458 start_at,
1459 ..
1460 } => track_ids
1461 .get(*start_at as usize)
1462 .or(track_ids.first())
1463 .map(String::as_str),
1464 LinkCommand::JumpTo { track_id } => Some(track_id),
1465 _ => None,
1466 }
1467}
1468
1469fn deliver_push(
1471 pusher: &crate::push::Pusher,
1472 target: &outbox::PushTarget,
1473 push: &crate::push::Push,
1474) {
1475 use crate::push::Outcome;
1476 match pusher.send(&target.token, target.sandbox, push) {
1477 Outcome::Sent => log::info!("push: sent to {}", target.name),
1478 Outcome::Gone => {
1479 log::info!(
1480 "push: {}'s token is no longer valid; forgotten",
1481 target.name
1482 );
1483 outbox::forget_push(&target.username, &target.device);
1484 }
1485 Outcome::Failed(e) => log::warn!("push: to {} failed: {e}", target.name),
1486 }
1487}
1488
1489type Woken = std::collections::HashMap<(String, String), (std::time::Instant, &'static str)>;
1493
1494static WOKEN: LazyLock<Mutex<Woken>> = LazyLock::new(Default::default);
1495
1496pub fn smart_activity(
1500 db: &koan_core::db::connection::Database,
1501 user: i64,
1502 fields: &[koan_core::smart::Field],
1503) {
1504 match koan_core::db::queries::smart::refresh_after_activity(&db.conn, user, fields) {
1505 Ok(moved) if !moved.is_empty() => changed(),
1506 Ok(_) => {}
1507 Err(e) => log::warn!("smart playlists not refreshed after activity: {e}"),
1508 }
1509}
1510
1511pub fn changed() {
1516 let (_, queued) = registry().link_or_queue(None, &LinkCommand::Sync { full: false });
1517 if queued.is_empty() || crate::push::pusher().is_none() {
1518 return;
1519 }
1520 let mut wakes = WAKES.lock();
1521 wakes.add(queued.iter().map(Absent::key), std::time::Instant::now());
1522 if !wakes.timer {
1523 wakes.timer = true;
1524 std::thread::spawn(send_wakes);
1525 }
1526}
1527
1528const QUIET: std::time::Duration = std::time::Duration::from_secs(30);
1532
1533const LONGEST_WAIT: std::time::Duration = std::time::Duration::from_secs(5 * 60);
1535
1536static WAKES: Mutex<Wakes> = Mutex::new(Wakes {
1537 pending: Vec::new(),
1538 timer: false,
1539});
1540
1541struct Wakes {
1544 pending: Vec<((String, String), std::time::Instant, std::time::Instant)>,
1546 timer: bool,
1548}
1549
1550impl Wakes {
1551 fn add(
1552 &mut self,
1553 devices: impl IntoIterator<Item = (String, String)>,
1554 now: std::time::Instant,
1555 ) {
1556 for key in devices {
1557 match self.pending.iter_mut().find(|(k, _, _)| *k == key) {
1558 Some((_, _, last)) => *last = now,
1559 None => self.pending.push((key, now, now)),
1560 }
1561 }
1562 }
1563
1564 fn due_at(first: std::time::Instant, last: std::time::Instant) -> std::time::Instant {
1565 (last + QUIET).min(first + LONGEST_WAIT)
1566 }
1567
1568 fn next(&self) -> Option<std::time::Instant> {
1570 self.pending
1571 .iter()
1572 .map(|(_, first, last)| Self::due_at(*first, *last))
1573 .min()
1574 }
1575
1576 fn take_due(&mut self, now: std::time::Instant) -> Vec<(String, String)> {
1578 let (due, waiting) = std::mem::take(&mut self.pending)
1579 .into_iter()
1580 .partition(|(_, first, last)| Self::due_at(*first, *last) <= now);
1581 self.pending = waiting;
1582 due.into_iter().map(|(key, _, _)| key).collect()
1583 }
1584}
1585
1586fn send_wakes() {
1589 loop {
1590 let (due, next) = {
1591 let mut wakes = WAKES.lock();
1592 let due = wakes.take_due(std::time::Instant::now());
1593 let next = wakes.next();
1594 if due.is_empty() && next.is_none() {
1595 wakes.timer = false;
1596 return;
1597 }
1598 (due, next)
1599 };
1600 let live = registry().live();
1601 let absent: Vec<(String, String)> = due.into_iter().filter(|d| !live.contains(d)).collect();
1602 if !absent.is_empty() {
1603 wake(&absent);
1604 }
1605 if let Some(next) = next {
1606 std::thread::sleep(next.saturating_duration_since(std::time::Instant::now()));
1607 }
1608 }
1609}
1610
1611pub fn changed_if_library_moved(conn: &rusqlite::Connection) {
1615 static LAST: parking_lot::Mutex<Option<(i64, i64, i64)>> = parking_lot::Mutex::new(None);
1616 let Ok(now) = conn.query_row(
1617 "SELECT (SELECT COUNT(*) FROM tracks), (SELECT COALESCE(MAX(id), 0) FROM tracks),
1618 (SELECT COUNT(*) FROM albums)",
1619 [],
1620 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
1621 ) else {
1622 return;
1623 };
1624 let before = LAST.lock().replace(now);
1625 if before.is_some_and(|b| b != now) {
1626 changed();
1627 }
1628}
1629
1630mod outbox {
1633 use koan_core::db::queries;
1634 use koan_core::remote::link::LinkCommand;
1635
1636 const KEEP_SECS: i64 = 30 * 24 * 60 * 60;
1641
1642 fn db() -> Option<koan_core::db::pool::Handle<'static>> {
1647 if cfg!(test) {
1648 return None;
1649 }
1650 koan_core::db::pool::shared().get().ok()
1651 }
1652
1653 pub fn load_orders() -> Vec<super::Order> {
1654 let Some(db) = db() else { return Vec::new() };
1655 db.conn
1656 .prepare("SELECT body FROM link_orders ORDER BY created_at")
1657 .and_then(|mut s| {
1658 s.query_map([], |r| r.get::<_, String>(0))?
1659 .collect::<Result<Vec<_>, _>>()
1660 })
1661 .unwrap_or_default()
1662 .into_iter()
1663 .filter_map(|b| serde_json::from_str(&b).ok())
1664 .collect()
1665 }
1666
1667 pub fn save_order(order: &super::Order) {
1668 let (Some(db), Ok(body)) = (db(), serde_json::to_string(order)) else {
1669 return;
1670 };
1671 let _ = db.conn.execute(
1672 "INSERT OR REPLACE INTO link_orders (id, body, created_at) VALUES (?1, ?2, ?3)",
1673 rusqlite::params![order.id, body, order.created_at],
1674 );
1675 }
1676
1677 pub fn drop_order(id: &str) {
1678 if let Some(db) = db() {
1679 let _ = db
1680 .conn
1681 .execute("DELETE FROM link_orders WHERE id = ?1", [id]);
1682 }
1683 }
1684
1685 pub fn take_and_remember(
1686 username: &str,
1687 device: &str,
1688 name: &str,
1689 platform: &str,
1690 live: &[(String, String)],
1691 ) -> Vec<koan_core::remote::acks::Envelope> {
1692 let Some(db) = db() else { return Vec::new() };
1693 let now = chrono::Utc::now().timestamp();
1694 let _ = db.conn.execute(
1695 "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, ?2, ?3, ?4, ?5)
1696 ON CONFLICT (device, username) DO UPDATE SET name = ?3, platform = ?4, last_seen = ?5",
1697 rusqlite::params![device, username, name, platform, now],
1698 );
1699 forget_stale(&db.conn, live, now);
1700 let waiting: Vec<(i64, String)> = db
1701 .conn
1702 .prepare("SELECT id, command FROM link_outbox WHERE device = ?1 AND username = ?2 ORDER BY id")
1703 .and_then(|mut s| {
1704 s.query_map([device, username], |r| Ok((r.get(0)?, r.get(1)?)))?
1705 .collect()
1706 })
1707 .unwrap_or_default();
1708 let _ = db.conn.execute(
1709 "DELETE FROM link_outbox WHERE device = ?1 AND username = ?2",
1710 [device, username],
1711 );
1712 if !waiting.is_empty() {
1713 log::info!("link: {} waiting commands for {name}", waiting.len());
1714 }
1715 waiting
1716 .into_iter()
1717 .filter_map(|(_, c)| serde_json::from_str(&c).ok())
1718 .collect()
1719 }
1720
1721 pub(super) fn forget_stale(conn: &rusqlite::Connection, live: &[(String, String)], now: i64) {
1724 touch_with(conn, live, now);
1725 let _ = conn.execute(
1726 "DELETE FROM link_outbox WHERE created_at < ?1",
1727 [now - KEEP_SECS],
1728 );
1729 let _ = conn.execute(
1730 "DELETE FROM link_push WHERE (device, username) IN
1731 (SELECT device, username FROM link_devices WHERE last_seen < ?1)",
1732 [now - KEEP_SECS],
1733 );
1734 let _ = conn.execute(
1735 "DELETE FROM link_devices WHERE last_seen < ?1",
1736 [now - KEEP_SECS],
1737 );
1738 }
1739
1740 pub fn touch(devices: &[(String, String)]) {
1742 if let Some(db) = db() {
1743 touch_with(&db.conn, devices, chrono::Utc::now().timestamp());
1744 }
1745 }
1746
1747 fn touch_with(conn: &rusqlite::Connection, devices: &[(String, String)], now: i64) {
1748 for (device, username) in devices {
1749 let _ = conn.execute(
1750 "UPDATE link_devices SET last_seen = ?1 WHERE device = ?2 AND username = ?3",
1751 rusqlite::params![now, device, username],
1752 );
1753 }
1754 }
1755
1756 pub struct Absent {
1758 pub device: String,
1759 pub username: String,
1760 pub name: String,
1761 }
1762
1763 pub fn queue_for_absent(
1765 username: Option<&str>,
1766 live: &[(String, String)],
1767 cmd: &LinkCommand,
1768 ) -> Vec<Absent> {
1769 let Some(db) = db() else { return Vec::new() };
1770 let known: Vec<(String, String, String)> = db
1771 .conn
1772 .prepare("SELECT device, username, name FROM link_devices")
1773 .and_then(|mut s| {
1774 s.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
1775 .collect()
1776 })
1777 .unwrap_or_default();
1778 let Ok(text) = serde_json::to_string(cmd) else {
1779 return Vec::new();
1780 };
1781 let absent: Vec<_> = known
1782 .into_iter()
1783 .filter(|(device, user, _)| {
1784 !username.is_some_and(|u| u != user)
1785 && !live.iter().any(|(d, u)| d == device && u == user)
1786 })
1787 .collect();
1788 if absent.is_empty() {
1789 return Vec::new();
1790 }
1791 let is_sync = matches!(cmd, LinkCommand::Sync { .. });
1792 let now = chrono::Utc::now().timestamp();
1793 koan_core::db::queries::atomically(&db.conn, || {
1796 let mut queued = Vec::new();
1797 for (device, user, name) in absent {
1798 if is_sync {
1799 let _ = db.conn.execute(
1801 "DELETE FROM link_outbox WHERE device = ?1 AND username = ?2 AND command LIKE '{\"type\":\"sync\"%'",
1802 [&device, &user],
1803 );
1804 }
1805 if db
1806 .conn
1807 .execute(
1808 "INSERT INTO link_outbox (device, username, command, created_at) VALUES (?1, ?2, ?3, ?4)",
1809 rusqlite::params![device, user, text, now],
1810 )
1811 .is_ok()
1812 {
1813 queued.push(Absent {
1814 device,
1815 username: user,
1816 name,
1817 });
1818 }
1819 }
1820 Ok::<_, rusqlite::Error>(queued)
1821 })
1822 .unwrap_or_default()
1823 }
1824
1825 #[derive(Clone)]
1827 pub struct PushTarget {
1828 pub device: String,
1829 pub username: String,
1830 pub name: String,
1831 pub platform: String,
1832 pub token: String,
1833 pub sandbox: bool,
1834 pub last_seen: i64,
1836 }
1837
1838 pub fn queue_for(device: &str, username: &str, envelope: &koan_core::remote::acks::Envelope) {
1842 let (Some(db), Ok(text)) = (db(), serde_json::to_string(envelope)) else {
1843 return;
1844 };
1845 let _ = db.conn.execute(
1846 "INSERT INTO link_outbox (device, username, command, created_at) VALUES (?1, ?2, ?3, ?4)",
1847 rusqlite::params![device, username, text, chrono::Utc::now().timestamp()],
1848 );
1849 }
1850
1851 pub fn accounts() -> Vec<String> {
1855 let Some(db) = db() else { return Vec::new() };
1856 koan_core::db::queries::auth::list_users(&db.conn)
1857 .map(|users| users.into_iter().map(|u| u.username).collect())
1858 .unwrap_or_default()
1859 }
1860
1861 pub fn load_addresses() -> std::collections::HashMap<String, (String, std::net::IpAddr)> {
1864 let Some(db) = db() else {
1865 return Default::default();
1866 };
1867 load_addresses_in(&db.conn)
1868 }
1869
1870 pub(super) fn load_addresses_in(
1871 conn: &rusqlite::Connection,
1872 ) -> std::collections::HashMap<String, (String, std::net::IpAddr)> {
1873 conn.prepare(
1874 "SELECT device, username, addr FROM link_devices WHERE addr IS NOT NULL ORDER BY last_seen",
1875 )
1876 .and_then(|mut s| {
1877 s.query_map([], |r| {
1878 Ok((
1879 r.get::<_, String>(0)?,
1880 r.get::<_, String>(1)?,
1881 r.get::<_, String>(2)?,
1882 ))
1883 })?
1884 .collect::<Result<Vec<_>, _>>()
1885 })
1886 .unwrap_or_default()
1887 .into_iter()
1888 .filter_map(|(device, username, addr)| Some((device, (username, addr.parse().ok()?))))
1889 .collect()
1890 }
1891
1892 pub fn save_address(device: &str, username: &str, addr: std::net::IpAddr) {
1893 if let Some(db) = db() {
1894 save_address_in(&db.conn, device, username, addr);
1895 }
1896 }
1897
1898 pub(super) fn save_address_in(
1899 conn: &rusqlite::Connection,
1900 device: &str,
1901 username: &str,
1902 addr: std::net::IpAddr,
1903 ) {
1904 let _ = conn.execute(
1905 "UPDATE link_devices SET addr = ?1 WHERE device = ?2 AND username = ?3",
1906 rusqlite::params![addr.to_string(), device, username],
1907 );
1908 }
1909
1910 pub fn load_grants() -> Vec<super::Grant> {
1911 let Some(db) = db() else { return Vec::new() };
1912 db.conn
1913 .prepare("SELECT device, owner, grantee FROM link_grants ORDER BY created_at")
1914 .and_then(|mut s| {
1915 s.query_map([], |r| {
1916 Ok(super::Grant {
1917 device: r.get(0)?,
1918 owner: r.get(1)?,
1919 grantee: r.get(2)?,
1920 })
1921 })?
1922 .collect()
1923 })
1924 .unwrap_or_default()
1925 }
1926
1927 pub fn save_grant(g: &super::Grant) {
1928 if let Some(db) = db() {
1929 let _ = db.conn.execute(
1930 "INSERT OR IGNORE INTO link_grants (device, owner, grantee, created_at) VALUES (?1, ?2, ?3, ?4)",
1931 rusqlite::params![g.device, g.owner, g.grantee, chrono::Utc::now().timestamp()],
1932 );
1933 }
1934 }
1935
1936 pub fn drop_grant(g: &super::Grant) {
1937 if let Some(db) = db() {
1938 let _ = db.conn.execute(
1939 "DELETE FROM link_grants WHERE device = ?1 AND owner = ?2 AND grantee = ?3",
1940 [&g.device, &g.owner, &g.grantee],
1941 );
1942 }
1943 }
1944
1945 pub fn save_push(username: &str, device: &str, token: &str, sandbox: bool) {
1946 let Some(db) = db() else { return };
1947 let _ = db.conn.execute(
1948 "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)
1949 ON CONFLICT (device, username) DO UPDATE SET token = ?3, sandbox = ?4, updated_at = ?5",
1950 rusqlite::params![device, username, token, sandbox, chrono::Utc::now().timestamp()],
1951 );
1952 }
1953
1954 pub fn forget_push(username: &str, device: &str) {
1955 if let Some(db) = db() {
1956 let _ = db.conn.execute(
1957 "DELETE FROM link_push WHERE device = ?1 AND username = ?2",
1958 [device, username],
1959 );
1960 }
1961 }
1962
1963 pub fn push_targets(username: Option<&str>) -> Vec<PushTarget> {
1965 let Some(db) = db() else { return Vec::new() };
1966 push_targets_in(&db.conn, username)
1967 }
1968
1969 pub(super) fn push_targets_in(
1970 conn: &rusqlite::Connection,
1971 username: Option<&str>,
1972 ) -> Vec<PushTarget> {
1973 conn
1974 .prepare(
1975 "SELECT p.device, p.username, d.name, d.platform, p.token, p.sandbox, d.last_seen
1976 FROM link_push p JOIN link_devices d ON d.device = p.device AND d.username = p.username
1977 WHERE ?1 IS NULL OR p.username = ?1
1978 ORDER BY d.last_seen DESC",
1979 )
1980 .and_then(|mut s| {
1981 s.query_map([username], |r| {
1982 Ok(PushTarget {
1983 device: r.get(0)?,
1984 username: r.get(1)?,
1985 name: r.get(2)?,
1986 platform: r.get(3)?,
1987 token: r.get(4)?,
1988 sandbox: r.get(5)?,
1989 last_seen: r.get(6)?,
1990 })
1991 })?
1992 .collect()
1993 })
1994 .unwrap_or_default()
1995 }
1996
1997 pub fn forget_device(device: &str, username: &str) {
2000 if let Some(db) = db() {
2001 forget_device_in(&db.conn, device, username);
2002 }
2003 }
2004
2005 pub(super) fn forget_device_in(conn: &rusqlite::Connection, device: &str, username: &str) {
2006 for table in ["link_push", "link_outbox", "link_devices"] {
2007 let _ = conn.execute(
2008 &format!("DELETE FROM {table} WHERE device = ?1 AND username = ?2"),
2009 [device, username],
2010 );
2011 }
2012 }
2013
2014 pub fn track_row(id: &str) -> Option<i64> {
2016 let db = db()?;
2017 queries::resolve_id(&db.conn, queries::UidKind::Track, id)
2018 .ok()
2019 .flatten()
2020 }
2021
2022 pub fn describe(cmd: &LinkCommand) -> Option<String> {
2025 let ids: Vec<&String> = match cmd {
2026 LinkCommand::Play { track_ids, .. }
2027 | LinkCommand::Enqueue { track_ids, .. }
2028 | LinkCommand::PlayNext { track_ids } => track_ids.iter().collect(),
2029 LinkCommand::JumpTo { track_id } => vec![track_id],
2030 _ => return None,
2031 };
2032 let db = db()?;
2033 let ids: Vec<i64> = ids
2034 .into_iter()
2035 .filter_map(|t| {
2036 queries::resolve_id(&db.conn, queries::UidKind::Track, t)
2037 .ok()
2038 .flatten()
2039 })
2040 .collect();
2041 let row = |id: i64| {
2042 db.conn
2043 .query_row(
2044 "SELECT t.title, COALESCE(a.name, ''), COALESCE(al.title, ''), t.album_id
2045 FROM tracks t LEFT JOIN artists a ON a.id = t.artist_id
2046 LEFT JOIN albums al ON al.id = t.album_id WHERE t.id = ?1",
2047 [id],
2048 |r| {
2049 Ok((
2050 r.get::<_, String>(0)?,
2051 r.get::<_, String>(1)?,
2052 r.get::<_, String>(2)?,
2053 r.get::<_, Option<i64>>(3)?,
2054 ))
2055 },
2056 )
2057 .ok()
2058 };
2059 let (title, artist, album, album_id) = row(*ids.first()?)?;
2060 let one_album = ids.len() > 1
2061 && album_id.is_some()
2062 && ids
2063 .iter()
2064 .all(|id| row(*id).is_some_and(|r| r.3 == album_id));
2065 Some(match (one_album, ids.len()) {
2066 (true, _) => format!("{album} — {artist}"),
2067 (false, 1) => format!("{title} — {artist}"),
2068 (false, n) => format!("{title} — {artist}, and {} more", n - 1),
2069 })
2070 }
2071}
2072
2073const RECENT: i64 = 6 * 60 * 60;
2076
2077fn pick(clients: &[ClientInfo], now: i64) -> Result<&ClientInfo, String> {
2078 if let Some(c) = clients.iter().find(|c| c.state.playing) {
2079 return Ok(c);
2080 }
2081 if let Some(c) = clients
2082 .iter()
2083 .filter(|c| c.last_played_at.is_some_and(|t| now - t < RECENT))
2084 .max_by_key(|c| c.last_played_at)
2085 {
2086 return Ok(c);
2087 }
2088 match clients {
2089 [] => Err("no koan app is linked to this server; open koan on the device".into()),
2090 [only] => Ok(only),
2091 several => Err(format!(
2092 "several koan apps are linked and none has played recently: {}. Ask which, then pass `client`",
2093 several
2094 .iter()
2095 .map(|c| format!("{} ({}, id {})", c.name, c.platform, c.device))
2096 .collect::<Vec<_>>()
2097 .join(", ")
2098 )),
2099 }
2100}
2101
2102#[cfg(test)]
2103mod tests {
2104
2105 #[test]
2110 fn a_quiet_link_that_took_the_command_is_kept_and_waited_for() {
2111 use koan_core::remote::acks::AckOutcome;
2112 let reg = Registry::default();
2113 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
2114 reg.register("q", "mac", "macos", "dev-quiet", tx, false, true);
2115 let answers = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2116 let acking = |id: u64| {
2117 let answers = answers.clone();
2118 Acking {
2119 id,
2120 reply: Box::new(move |outcome| answers.lock().unwrap().push((id, outcome))),
2121 }
2122 };
2123
2124 reg.relay_acked("q", None, "dev-quiet", LinkCommand::Pause, Some(acking(1)))
2126 .unwrap();
2127 reg.received("q", "dev-quiet", 1);
2128 assert!(reg.look("q", "dev-quiet", 1, &LinkCommand::Pause));
2129 assert!(answers.lock().unwrap().is_empty(), "still waiting");
2130 reg.answered("q", "dev-quiet", 1, AckOutcome::Done);
2131 assert_eq!(
2132 answers.lock().unwrap().last(),
2133 Some(&(1, Some(AckOutcome::Done)))
2134 );
2135
2136 reg.relay_acked("q", None, "dev-quiet", LinkCommand::Pause, Some(acking(2)))
2139 .unwrap();
2140 assert!(!reg.look("q", "dev-quiet", 2, &LinkCommand::Pause));
2141 assert!(matches!(
2142 answers.lock().unwrap().last(),
2143 Some((2, Some(AckOutcome::Failed { .. })))
2144 ));
2145 assert_eq!(reg.list(Some("q")).len(), 1, "the link is not dropped");
2146 }
2147
2148 #[tokio::test]
2151 async fn an_answer_stops_its_timer() {
2152 let timer = tokio::spawn(tokio::time::sleep(std::time::Duration::from_secs(3600)));
2153 let handle = timer.abort_handle();
2154 let waiting = Waiting {
2155 reply: Box::new(|_| {}),
2156 received: true,
2157 timer: Some(timer.abort_handle()),
2158 };
2159 waiting.answer(Some(koan_core::remote::acks::AckOutcome::Done));
2160 assert!(timer.await.unwrap_err().is_cancelled());
2161 assert!(handle.is_finished());
2162 }
2163
2164 #[test]
2165 fn a_command_relayed_with_an_id_is_answered_once() {
2166 use koan_core::remote::acks::AckOutcome;
2167 let reg = Registry::default();
2168 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2169 reg.register("a", "phone", "ios", "dev-answers", tx, false, true);
2170 let (old_tx, mut old_rx) = tokio::sync::mpsc::unbounded_channel();
2171 reg.register("a", "mac", "macos", "dev-old", old_tx, false, false);
2172 let answers = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2173 let acking = |id: u64| {
2174 let answers = answers.clone();
2175 Acking {
2176 id,
2177 reply: Box::new(move |outcome| answers.lock().unwrap().push((id, outcome))),
2178 }
2179 };
2180
2181 reg.relay_acked(
2183 "a",
2184 None,
2185 "dev-answers",
2186 LinkCommand::Pause,
2187 Some(acking(7)),
2188 )
2189 .unwrap();
2190 assert_eq!(rx.try_recv().unwrap().ack, Some(7));
2191 let (b_tx, _b_rx) = tokio::sync::mpsc::unbounded_channel();
2193 reg.register("b", "phone", "ios", "dev-answers", b_tx, false, true);
2194 reg.received("b", "dev-answers", 7);
2195 reg.answered("b", "dev-answers", 7, AckOutcome::Done);
2196 assert!(
2197 answers.lock().unwrap().is_empty(),
2198 "b cannot answer a's command"
2199 );
2200 reg.answered("a", "dev-answers", 7, AckOutcome::Done);
2201 reg.answered("a", "dev-answers", 7, AckOutcome::Done);
2202
2203 reg.relay_acked("a", None, "dev-old", LinkCommand::Pause, Some(acking(8)))
2206 .unwrap();
2207 assert_eq!(old_rx.try_recv().unwrap().ack, None);
2208
2209 assert_eq!(
2210 *answers.lock().unwrap(),
2211 [(7, Some(AckOutcome::Done)), (8, None)]
2212 );
2213 }
2214
2215 #[test]
2216 fn a_watch_is_never_queued_and_is_renewed_when_the_target_relinks() {
2217 let reg = Registry::default();
2218 let watches = |rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>| {
2219 std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command))
2220 .filter(|c| matches!(c, LinkCommand::WatchLevels { .. }))
2221 .collect::<Vec<_>>()
2222 };
2223 assert!(
2225 reg.relay("rl", "rl-phone", LinkCommand::WatchLevels { on: true })
2226 .is_err()
2227 );
2228 reg.watch_levels("rl", "rl-mac", "rl-phone", true);
2230 let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2231 reg.register("rl", "phone", "ios", "rl-phone", tx, false, false);
2232 assert_eq!(
2233 watches(&mut phone),
2234 vec![LinkCommand::WatchLevels { on: true }],
2235 "told once, on linking, because it is watched now"
2236 );
2237 let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2239 reg.register("rl", "phone", "ios", "rl-phone", tx, false, false);
2240 assert_eq!(
2241 watches(&mut phone),
2242 vec![LinkCommand::WatchLevels { on: true }]
2243 );
2244 }
2245
2246 #[test]
2247 fn levels_reach_a_watcher_only_while_it_watches() {
2248 use koan_core::remote::levels::Frame;
2249 let reg = Registry::default();
2250 let (tx_mac, mut mac) = tokio::sync::mpsc::unbounded_channel();
2251 let (tx_phone, mut phone) = tokio::sync::mpsc::unbounded_channel();
2252 let mac_id = reg.register("lv", "mac", "macos", "lv-mac", tx_mac, false, false);
2253 reg.register("lv", "phone", "ios", "lv-phone", tx_phone, false, false);
2254 let levels = |rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>| {
2255 std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command))
2256 .filter(|c| {
2257 matches!(
2258 c,
2259 LinkCommand::Levels { .. } | LinkCommand::WatchLevels { .. }
2260 )
2261 })
2262 .collect::<Vec<_>>()
2263 };
2264 let f = Frame(1_000, 1, 2, 3);
2265
2266 reg.levels("lv", "lv-phone", f);
2267 assert!(levels(&mut mac).is_empty(), "nobody watching");
2268
2269 reg.watch_levels("lv", "lv-mac", "lv-phone", true);
2270 assert_eq!(
2271 levels(&mut phone),
2272 vec![LinkCommand::WatchLevels { on: true }]
2273 );
2274 reg.levels("lv", "lv-phone", f);
2275 assert_eq!(
2276 levels(&mut mac),
2277 vec![LinkCommand::Levels {
2278 from: "lv-phone".into(),
2279 f
2280 }]
2281 );
2282
2283 reg.unregister(&mac_id);
2285 assert_eq!(
2286 levels(&mut phone),
2287 vec![LinkCommand::WatchLevels { on: false }]
2288 );
2289 reg.watch_levels("lv", "lv-mac", "lv-phone", true);
2290 reg.watch_levels("lv", "lv-mac", "lv-phone", false);
2291 assert_eq!(
2292 levels(&mut phone),
2293 vec![
2294 LinkCommand::WatchLevels { on: true },
2295 LinkCommand::WatchLevels { on: false }
2296 ]
2297 );
2298 }
2299
2300 #[test]
2301 fn a_playlist_order_adds_the_named_tracks_once_they_arrive() {
2302 let dir = tempfile::tempdir().unwrap();
2303 let path = dir.path().join("koan.db");
2304 let db = koan_core::db::connection::Database::open(&path).unwrap();
2305 let playlist = koan_core::db::queries::create_playlist(
2306 &db.conn,
2307 koan_core::db::queries::LOCAL_USER,
2308 "cyberpunk",
2309 None,
2310 )
2311 .unwrap();
2312 let order = Order {
2313 id: "o1".into(),
2314 username: None,
2315 client: None,
2316 artist: "Perturbator".into(),
2317 album: "Dangerous Days".into(),
2318 play_next: false,
2319 playlist: Some(playlist),
2320 titles: vec!["Future Club".into()],
2321 created_at: chrono::Utc::now().timestamp(),
2322 };
2323 registry().add_order(order);
2324
2325 fulfil_from(&path);
2327 assert!(registry().orders(None).iter().any(|o| o.id == "o1"));
2328
2329 db.conn
2330 .execute_batch(
2331 "INSERT INTO artists (id, name) VALUES (1, 'Perturbator');
2332 INSERT INTO albums (id, title, artist_id) VALUES (1, 'Dangerous Days', 1);
2333 INSERT INTO tracks (id, title, album_id, artist_id, track_number, path) VALUES
2334 (1, 'Welcome Back', 1, 1, 1, '/1.flac'), (2, 'Future Club', 1, 1, 2, '/2.flac');",
2335 )
2336 .unwrap();
2337 fulfil_from(&path);
2338 let held: Vec<i64> = db
2339 .conn
2340 .prepare("SELECT track_id FROM playlist_tracks WHERE playlist_id = ?1")
2341 .unwrap()
2342 .query_map([playlist], |r| r.get(0))
2343 .unwrap()
2344 .collect::<Result<_, _>>()
2345 .unwrap();
2346 assert_eq!(held, [2]);
2347 assert!(!registry().orders(None).iter().any(|o| o.id == "o1"));
2348 }
2349
2350 use super::*;
2351
2352 #[test]
2353 fn a_notification_cover_is_named_by_the_uid_the_command_carries() {
2354 let uid = "0190a5b2-7c3d-7e4f-8a1b-2c3d4e5f6a7b".to_string();
2356 let play = LinkCommand::Play {
2357 track_ids: vec!["x".into(), uid.clone()],
2358 start_at: 1,
2359 position_ms: 0,
2360 paused: false,
2361 handoff: false,
2362 mode: koan_core::player::state::QueueMode::Keep,
2363 };
2364 assert_eq!(cover_track(&play), Some(uid.as_str()));
2365 }
2366
2367 #[test]
2368 fn a_reconnect_replaces_the_device_and_commands_reach_it() {
2369 let reg = Registry::default();
2370 let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel();
2371 let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2372 let (tx3, _rx3) = tokio::sync::mpsc::unbounded_channel();
2373 reg.register("j", "phone", "ios", "dev-1", tx1, false, false);
2374 let id = reg.register("j", "phone", "ios", "dev-1", tx2, false, false);
2375 reg.register("someone", "laptop", "macos", "dev-2", tx3, false, false);
2376
2377 assert_eq!(reg.list(Some("j")).len(), 1);
2378 assert_eq!(reg.list(None).len(), 2);
2379
2380 let sent = reg.send(Some("j"), None, LinkCommand::Pause).unwrap();
2381 assert_eq!(sent.id, id);
2382 assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2383
2384 assert!(
2386 reg.send(Some("j"), Some("laptop"), LinkCommand::Pause)
2387 .is_err()
2388 );
2389
2390 reg.unregister(&id);
2391 assert!(reg.send(Some("j"), None, LinkCommand::Pause).is_err());
2392 }
2393
2394 #[test]
2395 fn disconnecting_an_account_closes_only_its_links() {
2396 let reg = Registry::default();
2397 let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel();
2398 let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2399 reg.register("j", "phone", "ios", "dev-1", tx1, false, false);
2400 reg.register("someone", "laptop", "macos", "dev-2", tx2, false, false);
2401
2402 reg.disconnect("j");
2403 assert!(reg.list(Some("j")).is_empty());
2404 assert!(matches!(
2406 rx1.try_recv(),
2407 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
2408 ));
2409 assert_eq!(reg.list(Some("someone")).len(), 1);
2410 assert!(matches!(
2411 rx2.try_recv(),
2412 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2413 ));
2414 }
2415
2416 #[test]
2417 fn a_burst_of_changes_wakes_each_device_once_when_it_goes_quiet() {
2418 let t0 = std::time::Instant::now();
2419 let s = std::time::Duration::from_secs;
2420 let phone = || ("dev-1".to_string(), "j".to_string());
2421 let ipad = || ("dev-2".to_string(), "j".to_string());
2422 let mut wakes = Wakes {
2423 pending: Vec::new(),
2424 timer: false,
2425 };
2426
2427 wakes.add([phone()], t0);
2428 wakes.add([phone(), ipad()], t0 + s(10));
2429 wakes.add([phone()], t0 + s(20));
2430 assert_eq!(wakes.pending.len(), 2);
2431
2432 assert!(wakes.take_due(t0 + s(39)).is_empty());
2434 assert_eq!(wakes.take_due(t0 + s(40)), [ipad()]);
2435 assert_eq!(wakes.next(), Some(t0 + s(50)));
2436 assert_eq!(wakes.take_due(t0 + s(50)), [phone()]);
2437 assert_eq!(wakes.next(), None);
2438 }
2439
2440 #[test]
2441 fn a_library_that_never_goes_quiet_still_wakes_devices() {
2442 let t0 = std::time::Instant::now();
2443 let phone = || ("dev-1".to_string(), "j".to_string());
2444 let mut wakes = Wakes {
2445 pending: Vec::new(),
2446 timer: false,
2447 };
2448 let mut sent = 0;
2449 for i in 0..40 {
2450 let now = t0 + std::time::Duration::from_secs(i * 10);
2451 sent += wakes.take_due(now).len();
2452 wakes.add([phone()], now);
2453 }
2454 assert_eq!(sent, 1);
2457 assert_eq!(wakes.pending.len(), 1);
2458 }
2459
2460 #[test]
2461 fn the_device_playing_is_the_one_meant() {
2462 let reg = Registry::default();
2463 let (tx1, _rx1) = tokio::sync::mpsc::unbounded_channel();
2464 let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2465 let mac = reg.register("j", "mac", "macos", "dev-1", tx1, false, false);
2466 let phone = reg.register("j", "phone", "ios", "dev-2", tx2, false, false);
2467
2468 let err = reg.send(Some("j"), None, LinkCommand::Pause).unwrap_err();
2470 assert!(err.contains("mac") && err.contains("phone"), "{err}");
2471
2472 reg.report(
2473 &phone,
2474 LinkState {
2475 playing: true,
2476 ..Default::default()
2477 },
2478 );
2479 assert_eq!(
2480 reg.send(Some("j"), None, LinkCommand::Pause).unwrap().id,
2481 phone
2482 );
2483 assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2484
2485 reg.report(&phone, LinkState::default());
2487 assert_eq!(
2488 reg.send(Some("j"), None, LinkCommand::Pause).unwrap().id,
2489 phone
2490 );
2491 let _ = mac;
2492 }
2493
2494 fn drain(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Envelope>) -> Vec<LinkCommand> {
2495 std::iter::from_fn(|| rx.try_recv().ok().map(|e| e.command)).collect()
2496 }
2497
2498 fn shared() -> (
2500 Registry,
2501 tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2502 tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2503 tokio::sync::mpsc::UnboundedReceiver<Envelope>,
2504 ) {
2505 let reg = Registry::default();
2506 let (phone_tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2507 let (mac_tx, mac) = tokio::sync::mpsc::unbounded_channel();
2508 let (k_tx, k) = tokio::sync::mpsc::unbounded_channel();
2509 let id = reg.register("j", "phone", "ios", "dev-phone", phone_tx, true, false);
2510 reg.register("j", "mac", "macos", "dev-mac", mac_tx, true, false);
2511 reg.register("k", "laptop", "macos", "dev-k", k_tx, true, false);
2512 reg.report(
2513 &id,
2514 LinkState {
2515 playing: true,
2516 title: Some("Roygbiv".into()),
2517 outputs: Some(Default::default()),
2518 ..Default::default()
2519 },
2520 );
2521 reg.share("j", "dev-phone", "k", true).unwrap();
2522 drain(&mut phone);
2523 (reg, phone, mac, k)
2524 }
2525
2526 #[test]
2527 fn a_grantee_sees_the_shared_device_and_nothing_else_of_the_owner() {
2528 let (_reg, _phone, _mac, mut k) = shared();
2529 let listed = drain(&mut k)
2530 .into_iter()
2531 .rev()
2532 .find_map(|c| match c {
2533 LinkCommand::Devices { devices } => Some(devices),
2534 _ => None,
2535 })
2536 .expect("told of it");
2537 assert_eq!(listed.len(), 1, "the phone, not the Mac");
2538 let phone = &listed[0];
2539 assert_eq!(phone.id, "dev-phone");
2540 assert_eq!(phone.owner.as_deref(), Some("j"));
2541 let state = phone.state.as_ref().expect("its state");
2542 assert_eq!(state.title.as_deref(), Some("Roygbiv"));
2543 assert!(
2544 state.outputs.is_some(),
2545 "outputs, to choose from: the output is in the playback set"
2546 );
2547 }
2548
2549 #[test]
2555 fn a_grantee_controls_playback_as_itself_and_nothing_of_the_owners_account() {
2556 let (reg, mut phone, mut mac, _k) = shared();
2557 let wrapped = |c: LinkCommand| LinkCommand::Shared {
2558 command: Box::new(c),
2559 };
2560 for cmd in [
2561 LinkCommand::Pause,
2562 LinkCommand::SetRendererVolume { volume: 40 },
2563 LinkCommand::SleepTimer {
2564 timer: Some(koan_core::player::state::SleepTimer::After { minutes: 30 }),
2565 },
2566 LinkCommand::HandOff { to: "dev-k".into() },
2567 ] {
2568 reg.relay("k", "dev-phone", cmd.clone()).unwrap();
2569 assert_eq!(drain(&mut phone), [wrapped(cmd)]);
2570 }
2571 for cmd in [
2572 LinkCommand::Sync { full: false },
2573 LinkCommand::Evict { track_ids: vec![] },
2574 LinkCommand::Shared {
2575 command: Box::new(LinkCommand::Pause),
2576 },
2577 ] {
2578 assert!(!cmd.allowed_playback());
2579 assert!(reg.relay("k", "dev-phone", cmd).is_err());
2580 }
2581 assert!(drain(&mut phone).is_empty());
2582 assert!(
2583 reg.relay("k", "dev-mac", LinkCommand::Pause).is_err(),
2584 "not shared, not reachable"
2585 );
2586 assert!(
2587 drain(&mut mac)
2588 .iter()
2589 .all(|c| matches!(c, LinkCommand::Devices { .. } | LinkCommand::Shares { .. })),
2590 "news, and no command"
2591 );
2592 assert!(reg.shared_owner("k", "dev-mac").is_none(), "nor wakeable");
2593 }
2594
2595 #[test]
2599 fn a_hand_off_runs_both_ways_across_a_grant_and_nothing_else_does() {
2600 let (reg, _phone, _mac, mut k) = shared();
2601 drain(&mut k);
2602 let play = LinkCommand::Play {
2603 track_ids: vec!["t".into()],
2604 start_at: 0,
2605 position_ms: 1000,
2606 paused: false,
2607 handoff: true,
2608 mode: koan_core::player::state::QueueMode::Keep,
2609 };
2610 reg.relay_from("j", Some("dev-phone"), "dev-k", play.clone())
2611 .unwrap();
2612 assert!(drain(&mut k).contains(&LinkCommand::Shared {
2613 command: Box::new(play.clone())
2614 }));
2615 assert!(
2616 reg.relay_from("j", Some("dev-phone"), "dev-k", LinkCommand::Pause)
2617 .is_err(),
2618 "the owner's phone does not command the grantee's devices"
2619 );
2620 assert!(
2621 reg.relay_from("j", Some("dev-mac"), "dev-k", play.clone())
2622 .is_err(),
2623 "only the shared device"
2624 );
2625 let not_a_hand_off = LinkCommand::Play {
2626 track_ids: vec!["t".into()],
2627 start_at: 0,
2628 position_ms: 0,
2629 paused: false,
2630 handoff: false,
2631 mode: koan_core::player::state::QueueMode::Keep,
2632 };
2633 assert!(
2634 reg.relay_from("j", Some("dev-phone"), "dev-k", not_a_hand_off)
2635 .is_err(),
2636 "a hand-off, not any play"
2637 );
2638 }
2639
2640 #[test]
2641 fn revoking_ends_control_at_once() {
2642 let (reg, mut phone, _mac, mut k) = shared();
2643 reg.share("j", "dev-phone", "k", false).unwrap();
2644 assert!(reg.relay("k", "dev-phone", LinkCommand::Pause).is_err());
2645 assert!(
2646 drain(&mut phone)
2647 .iter()
2648 .all(|c| !matches!(c, LinkCommand::Pause))
2649 );
2650 assert!(reg.shared_owner("k", "dev-phone").is_none());
2651 let listed = drain(&mut k)
2652 .into_iter()
2653 .rev()
2654 .find_map(|c| match c {
2655 LinkCommand::Devices { devices } => Some(devices),
2656 _ => None,
2657 })
2658 .expect("told it is gone");
2659 assert!(listed.is_empty());
2660 }
2661
2662 #[test]
2663 fn a_device_hears_whom_it_is_shared_with_every_time_it_links() {
2664 let reg = Registry::default();
2665 let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2666 reg.register("j", "phone", "ios", "dev-phone", tx, true, false);
2667 let first = drain(&mut phone);
2668 assert!(
2669 first.contains(&LinkCommand::Shares {
2670 grantees: vec![],
2671 error: None,
2672 accounts: vec![],
2673 }),
2674 "an empty list replaces one from another server"
2675 );
2676 reg.send_shares(
2677 "j",
2678 "dev-phone",
2679 Some("There is no account called x".into()),
2680 );
2681 assert!(drain(&mut phone).iter().any(
2682 |c| matches!(c, LinkCommand::Shares { error: Some(e), .. } if e.contains("no account"))
2683 ));
2684 }
2685
2686 #[test]
2687 fn the_owner_is_told_who_it_shares_with() {
2688 let reg = Registry::default();
2689 let (tx, mut phone) = tokio::sync::mpsc::unbounded_channel();
2690 reg.register("j", "phone", "ios", "dev-phone", tx, true, false);
2691 reg.share("j", "dev-phone", "k", true).unwrap();
2692 reg.share("j", "dev-phone", "m", true).unwrap();
2693 let last = drain(&mut phone).into_iter().rev().find_map(|c| match c {
2694 LinkCommand::Shares { grantees, .. } => Some(grantees),
2695 _ => None,
2696 });
2697 assert_eq!(last, Some(vec!["k".to_string(), "m".to_string()]));
2698 assert!(
2699 reg.share("j", "dev-phone", "j", true).is_err(),
2700 "not with itself"
2701 );
2702 }
2703
2704 #[test]
2708 fn a_device_is_woken_for_another_account_on_its_network_or_by_grant() {
2709 let reg = Registry::default();
2710 let home: std::net::IpAddr = "203.0.113.7".parse().unwrap();
2711 let away: std::net::IpAddr = "198.51.100.2".parse().unwrap();
2712 reg.seen_at("dev-ipad", "sarita", home);
2713 reg.seen_at("dev-phone", "admin", home);
2714 assert_eq!(
2715 reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2716 "sarita",
2717 "same address: the iPad's own token"
2718 );
2719 reg.seen_at("dev-phone", "admin", away);
2720 assert_eq!(
2721 reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2722 "admin",
2723 "elsewhere, with no grant: only admin's own, which it is not"
2724 );
2725 reg.share("sarita", "dev-ipad", "admin", true).unwrap();
2726 assert_eq!(
2727 reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2728 "sarita",
2729 "shared: from anywhere"
2730 );
2731 reg.seen_at("dev-other", "mallory", home);
2733 assert_eq!(reg.wake_owner("admin", "dev-other", "dev-tv"), "admin");
2734 }
2735
2736 #[test]
2739 fn where_a_device_last_linked_from_survives_a_restart() {
2740 let conn = rusqlite::Connection::open_in_memory().unwrap();
2741 koan_core::db::schema::create_tables(&conn).unwrap();
2742 for (device, user, seen) in [("dev-ipad", "sarita", 1), ("dev-phone", "admin", 2)] {
2743 conn.execute(
2744 "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, ?2, ?1, 'ios', ?3)",
2745 rusqlite::params![device, user, seen],
2746 )
2747 .unwrap();
2748 }
2749 let home: std::net::IpAddr = "203.0.113.7".parse().unwrap();
2750 outbox::save_address_in(&conn, "dev-ipad", "sarita", home);
2751 outbox::save_address_in(&conn, "dev-phone", "admin", home);
2752
2753 let reg = Registry::default();
2755 *reg.addresses.lock() = outbox::load_addresses_in(&conn);
2756 assert_eq!(
2757 reg.wake_owner("admin", "dev-phone", "dev-ipad"),
2758 "sarita",
2759 "still woken through its own account after the restart"
2760 );
2761 }
2762
2763 #[test]
2767 fn a_forgotten_device_is_dropped_by_the_accounts_devices() {
2768 let reg = Registry::default();
2769 let (mac_tx, mut mac) = tokio::sync::mpsc::unbounded_channel();
2770 let (other_tx, mut other) = tokio::sync::mpsc::unbounded_channel();
2771 reg.register("j", "mac", "macos", "dev-mac", mac_tx, true, false);
2772 reg.register("k", "laptop", "macos", "dev-k", other_tx, true, false);
2773 while mac.try_recv().is_ok() {}
2774 while other.try_recv().is_ok() {}
2775
2776 reg.forget("j", "dev-phone").unwrap();
2777 let heard: Vec<LinkCommand> =
2778 std::iter::from_fn(|| mac.try_recv().ok().map(|e| e.command)).collect();
2779 assert!(heard.contains(&LinkCommand::Forgotten {
2780 device: "dev-phone".into()
2781 }));
2782 assert!(
2783 std::iter::from_fn(|| other.try_recv().ok().map(|e| e.command))
2784 .all(|c| !matches!(c, LinkCommand::Forgotten { .. })),
2785 "another account hears nothing of it"
2786 );
2787 assert!(
2788 reg.forget("j", "dev-mac").is_err(),
2789 "linked: it would be back"
2790 );
2791 assert!(
2792 reg.relay(
2793 "j",
2794 "dev-mac",
2795 LinkCommand::Forgotten { device: "x".into() }
2796 )
2797 .is_err(),
2798 "news from the server, not a command a device may send"
2799 );
2800 }
2801
2802 #[test]
2805 fn forgetting_a_shared_device_declines_the_share() {
2806 let (reg, mut phone, _mac, mut k) = shared();
2807 drain(&mut k);
2808 reg.forget("k", "dev-phone").unwrap();
2809 assert!(reg.shared_owner("k", "dev-phone").is_none());
2810 assert!(
2811 drain(&mut phone)
2812 .iter()
2813 .any(|c| matches!(c, LinkCommand::Shares { grantees, .. } if grantees.is_empty()))
2814 );
2815 let listed = drain(&mut k).into_iter().rev().find_map(|c| match c {
2816 LinkCommand::Devices { devices } => Some(devices),
2817 _ => None,
2818 });
2819 assert_eq!(listed.map(|d| d.len()), Some(0));
2820 }
2821
2822 #[test]
2825 fn forgetting_a_device_stops_pushes_to_it_and_only_for_its_account() {
2826 let conn = rusqlite::Connection::open_in_memory().unwrap();
2827 koan_core::db::schema::create_tables(&conn).unwrap();
2828 for user in ["j", "k"] {
2829 conn.execute(
2830 "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES ('dev-phone', ?1, 'phone', 'ios', 1)",
2831 [user],
2832 )
2833 .unwrap();
2834 conn.execute(
2835 "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES ('dev-phone', ?1, 't', 0, 1)",
2836 [user],
2837 )
2838 .unwrap();
2839 }
2840 outbox::forget_device_in(&conn, "dev-phone", "k");
2841 assert_eq!(
2842 outbox::push_targets_in(&conn, Some("j")).len(),
2843 1,
2844 "k forgetting its own leaves j's alone"
2845 );
2846 outbox::forget_device_in(&conn, "dev-phone", "j");
2847 assert!(outbox::push_targets_in(&conn, Some("j")).is_empty());
2848 assert!(outbox::push_targets_in(&conn, None).is_empty());
2849 }
2850
2851 #[test]
2852 fn a_device_hears_its_peers_and_commands_reach_them_by_device_id() {
2853 let reg = Registry::default();
2854 let (tx1, mut rx1) = tokio::sync::mpsc::unbounded_channel();
2855 let (tx2, mut rx2) = tokio::sync::mpsc::unbounded_channel();
2856 let (tx3, mut rx3) = tokio::sync::mpsc::unbounded_channel();
2857 reg.register("j", "mac", "macos", "dev-mac", tx1, true, false);
2858 let phone = reg.register("j", "phone", "ios", "dev-phone", tx2, true, false);
2859 reg.register("someone", "laptop", "macos", "dev-other", tx3, true, false);
2860
2861 reg.report(
2862 &phone,
2863 LinkState {
2864 playing: true,
2865 title: Some("Roygbiv".into()),
2866 ..Default::default()
2867 },
2868 );
2869 let devices = drain(&mut rx1)
2870 .into_iter()
2871 .rev()
2872 .find_map(|c| match c {
2873 LinkCommand::Devices { devices } => Some(devices),
2874 _ => None,
2875 })
2876 .expect("the Mac is told of the phone");
2877 assert_eq!(devices.len(), 1, "not itself, not another account's");
2878 assert_eq!(devices[0].id, "dev-phone");
2879 assert_eq!(
2880 devices[0].state.as_ref().and_then(|s| s.title.as_deref()),
2881 Some("Roygbiv")
2882 );
2883 while rx3.try_recv().is_ok() {}
2884 assert!(rx3.try_recv().is_err());
2885
2886 while rx2.try_recv().is_ok() {}
2887 reg.relay("j", "dev-phone", LinkCommand::Pause).unwrap();
2888 assert_eq!(rx2.try_recv().unwrap().command, LinkCommand::Pause);
2889 assert!(
2890 reg.relay("someone", "dev-phone", LinkCommand::Pause)
2891 .is_err(),
2892 "another account cannot reach it"
2893 );
2894 assert!(
2895 reg.relay("j", "dev-phone", LinkCommand::Devices { devices: vec![] })
2896 .is_err()
2897 );
2898 }
2899
2900 #[test]
2901 fn a_device_linked_for_a_month_is_not_forgotten() {
2902 let conn = rusqlite::Connection::open_in_memory().unwrap();
2903 koan_core::db::schema::create_tables(&conn).unwrap();
2904 let now = 100 * 24 * 60 * 60;
2905 let long_ago = now - 40 * 24 * 60 * 60;
2906 for device in ["mac", "old-phone"] {
2907 conn.execute(
2908 "INSERT INTO link_devices (device, username, name, platform, last_seen) VALUES (?1, 'j', ?1, 'ios', ?2)",
2909 rusqlite::params![device, long_ago],
2910 )
2911 .unwrap();
2912 conn.execute(
2913 "INSERT INTO link_push (device, username, token, sandbox, updated_at) VALUES (?1, 'j', 't', 0, ?2)",
2914 rusqlite::params![device, long_ago],
2915 )
2916 .unwrap();
2917 }
2918 outbox::forget_stale(&conn, &[("mac".into(), "j".into())], now);
2919 let left = |table: &str| -> Vec<String> {
2920 conn.prepare(&format!("SELECT device FROM {table} ORDER BY device"))
2921 .unwrap()
2922 .query_map([], |r| r.get(0))
2923 .unwrap()
2924 .collect::<Result<_, _>>()
2925 .unwrap()
2926 };
2927 assert_eq!(left("link_devices"), ["mac"]);
2928 assert_eq!(left("link_push"), ["mac"]);
2929 }
2930}