1use std::collections::{BTreeMap, BTreeSet};
18
19use crate::ids::{ConnectionId, SessionId};
20use crate::view::UserFlags;
21
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub struct Silence {
35 muted: BTreeSet<SessionId>,
36 deafened: BTreeSet<SessionId>,
37}
38
39impl Silence {
40 pub fn record(&mut self, session: SessionId, flags: UserFlags) {
50 if flags.self_mute || flags.mute || flags.suppress {
51 self.muted.insert(session);
52 }
53 if flags.self_deaf || flags.deaf {
54 self.deafened.insert(session);
55 }
56 }
57
58 #[must_use]
60 pub fn may_speak(&self, session: SessionId) -> bool {
61 !self.muted.contains(&session)
62 }
63
64 #[must_use]
66 pub fn may_hear(&self, session: SessionId) -> bool {
67 !self.deafened.contains(&session)
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct DomainId(pub u64);
74
75#[derive(Debug, Clone, PartialEq, Eq, Default)]
81pub struct AudioRelation {
82 domains: BTreeMap<DomainId, BTreeSet<ConnectionId>>,
83 listeners: BTreeSet<(ConnectionId, DomainId)>,
84 edges: BTreeSet<(ConnectionId, ConnectionId)>,
85}
86
87impl AudioRelation {
88 pub fn domain(&mut self, domain: DomainId, members: &[ConnectionId]) {
93 self.domains
94 .entry(domain)
95 .or_default()
96 .extend(members.iter().copied());
97 }
98
99 pub fn listen(&mut self, listener: ConnectionId, domain: DomainId) {
102 self.listeners.insert((listener, domain));
103 }
104
105 pub fn edge(&mut self, sender: ConnectionId, receiver: ConnectionId) {
107 self.edges.insert((sender, receiver));
108 }
109
110 pub fn domains(&self) -> impl Iterator<Item = (DomainId, &BTreeSet<ConnectionId>)> {
112 self.domains.iter().map(|(id, members)| (*id, members))
113 }
114
115 pub fn listeners(&self) -> impl Iterator<Item = (ConnectionId, &BTreeSet<ConnectionId>)> {
120 self.listeners
121 .iter()
122 .filter_map(|(listener, domain)| Some((*listener, self.domains.get(domain)?)))
123 }
124
125 pub fn listen_declarations(&self) -> impl Iterator<Item = (ConnectionId, DomainId)> + '_ {
128 self.listeners.iter().copied()
129 }
130
131 pub fn explicit_edges(&self) -> impl Iterator<Item = (ConnectionId, ConnectionId)> + '_ {
133 self.edges.iter().copied()
134 }
135
136 #[must_use]
149 pub fn resolve(&self) -> BTreeSet<(ConnectionId, ConnectionId)> {
150 let mut resolved = self.edges.clone();
151
152 for members in self.domains.values() {
153 for sender in members {
154 for receiver in members {
155 if sender != receiver {
156 resolved.insert((*sender, *receiver));
157 }
158 }
159 }
160 }
161
162 for (listener, members) in self.listeners() {
163 for sender in members {
164 if *sender != listener {
165 resolved.insert((*sender, listener));
166 }
167 }
168 }
169
170 resolved
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Default)]
184pub struct AudioRouting {
185 receivers: BTreeMap<SessionId, Vec<SessionId>>,
186 since: BTreeMap<SessionId, u64>,
187 silence: Silence,
190}
191
192impl AudioRouting {
193 #[must_use]
195 pub fn receivers(&self, sender: SessionId) -> &[SessionId] {
196 self.receivers.get(&sender).map_or(&[], Vec::as_slice)
197 }
198
199 #[must_use]
208 pub fn since(&self, session: SessionId) -> Option<u64> {
209 self.since.get(&session).copied()
210 }
211
212 #[must_use]
214 pub fn may_hear(&self, sender: SessionId, receiver: SessionId) -> bool {
215 self.receivers(sender).contains(&receiver)
216 }
217
218 #[must_use]
225 pub fn may_speak(&self, sender: SessionId) -> bool {
226 self.silence.may_speak(sender)
227 }
228
229 pub fn senders(&self) -> impl Iterator<Item = SessionId> + '_ {
231 self.receivers.keys().copied()
232 }
233}
234
235pub fn compile(
260 relation: &AudioRelation,
261 session_of: &BTreeMap<ConnectionId, SessionId>,
262 since: &BTreeMap<SessionId, u64>,
263 silence: &Silence,
264) -> AudioRouting {
265 let mut receivers: BTreeMap<SessionId, Vec<SessionId>> = BTreeMap::new();
266 let resolve = |connections: &BTreeSet<ConnectionId>| -> Vec<SessionId> {
267 connections
268 .iter()
269 .filter_map(|connection| session_of.get(connection).copied())
270 .collect()
271 };
272
273 let mut listening_on: BTreeMap<DomainId, Vec<SessionId>> = BTreeMap::new();
274 for (listener, domain) in relation.listen_declarations() {
275 if let Some(session) = session_of.get(&listener).filter(|s| silence.may_hear(**s)) {
276 listening_on.entry(domain).or_default().push(*session);
277 }
278 }
279
280 for (domain, members) in relation.domains() {
281 let sessions = resolve(members);
282 let audience: Vec<SessionId> = sessions
286 .iter()
287 .copied()
288 .filter(|session| silence.may_hear(*session))
289 .collect();
290 let empty: Vec<SessionId> = Vec::new();
291 let listening = listening_on.get(&domain).unwrap_or(&empty);
292
293 for sender in sessions.iter().filter(|s| silence.may_speak(**s)) {
294 let list = receivers.entry(*sender).or_default();
295 list.extend(audience.iter().filter(|receiver| *receiver != sender));
299 list.extend(listening.iter().filter(|listener| *listener != sender));
300 }
301 }
302
303 for (sender, receiver) in relation.explicit_edges() {
304 let (Some(sender), Some(receiver)) = (session_of.get(&sender), session_of.get(&receiver))
305 else {
306 continue;
307 };
308 if !silence.may_speak(*sender) || !silence.may_hear(*receiver) {
309 continue;
310 }
311 receivers.entry(*sender).or_default().push(*receiver);
312 }
313
314 for list in receivers.values_mut() {
315 list.sort_unstable();
318 list.dedup();
319 }
320 receivers.retain(|_, list| !list.is_empty());
321
322 AudioRouting {
323 receivers,
324 since: since.clone(),
325 silence: silence.clone(),
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 #![allow(clippy::expect_used)]
332
333 use super::*;
334
335 fn sessions(pairs: &[(u64, u32)]) -> BTreeMap<ConnectionId, SessionId> {
336 pairs
337 .iter()
338 .map(|(connection, session)| (ConnectionId(*connection), SessionId(*session)))
339 .collect()
340 }
341
342 #[test]
343 fn a_domain_is_symmetric_and_excludes_self() {
344 let mut relation = AudioRelation::default();
345 relation.domain(
346 DomainId(1),
347 &[ConnectionId(1), ConnectionId(2), ConnectionId(3)],
348 );
349
350 let edges = relation.resolve();
351 assert!(edges.contains(&(ConnectionId(1), ConnectionId(2))));
352 assert!(edges.contains(&(ConnectionId(2), ConnectionId(1))));
353 assert!(
354 !edges.contains(&(ConnectionId(1), ConnectionId(1))),
355 "echoing a speaker back to itself is the doubled-voice bug"
356 );
357 assert_eq!(edges.len(), 6, "three members, every ordered pair but self");
358 }
359
360 #[test]
361 fn a_listener_hears_without_being_heard() {
362 let mut relation = AudioRelation::default();
363 relation.domain(DomainId(1), &[ConnectionId(1), ConnectionId(2)]);
364 relation.listen(ConnectionId(9), DomainId(1));
365
366 let edges = relation.resolve();
367 assert!(edges.contains(&(ConnectionId(1), ConnectionId(9))));
368 assert!(edges.contains(&(ConnectionId(2), ConnectionId(9))));
369 assert!(
370 !edges.contains(&(ConnectionId(9), ConnectionId(1))),
371 "a spectator must stay silent"
372 );
373 }
374
375 #[test]
376 fn listening_to_an_undeclared_domain_grants_nothing() {
377 let mut relation = AudioRelation::default();
378 relation.listen(ConnectionId(9), DomainId(404));
379
380 assert!(
381 relation.resolve().is_empty(),
382 "a typo must cost silence, never a leak"
383 );
384 }
385
386 #[test]
387 fn compiling_drops_connections_with_no_rendered_user() {
388 let mut relation = AudioRelation::default();
389 relation.domain(DomainId(1), &[ConnectionId(1), ConnectionId(2)]);
390
391 let routing = compile(
394 &relation,
395 &sessions(&[(1, 100)]),
396 &BTreeMap::new(),
397 &Silence::default(),
398 );
399 assert!(routing.receivers(SessionId(100)).is_empty());
400 }
401
402 #[test]
403 fn a_receiver_reachable_twice_is_listed_once() {
404 let mut relation = AudioRelation::default();
405 relation.domain(DomainId(1), &[ConnectionId(1), ConnectionId(2)]);
406 relation.edge(ConnectionId(1), ConnectionId(2));
407
408 let routing = compile(
409 &relation,
410 &sessions(&[(1, 100), (2, 200)]),
411 &BTreeMap::new(),
412 &Silence::default(),
413 );
414 assert_eq!(routing.receivers(SessionId(100)), &[SessionId(200)]);
415 }
416
417 fn silenced(flags: &[(u32, UserFlags)]) -> Silence {
419 let mut silence = Silence::default();
420 for (session, flags) in flags {
421 silence.record(SessionId(*session), *flags);
422 }
423 silence
424 }
425
426 fn muted() -> UserFlags {
427 UserFlags {
428 self_mute: true,
429 ..UserFlags::default()
430 }
431 }
432
433 fn deafened() -> UserFlags {
434 UserFlags {
435 self_deaf: true,
436 ..UserFlags::default()
437 }
438 }
439
440 #[test]
441 fn a_muted_speaker_has_no_receivers_at_all() {
442 let mut relation = AudioRelation::default();
443 relation.domain(
444 DomainId(1),
445 &[ConnectionId(1), ConnectionId(2), ConnectionId(3)],
446 );
447 relation.edge(ConnectionId(1), ConnectionId(4));
448 relation.listen(ConnectionId(4), DomainId(1));
449
450 let session_of = sessions(&[(1, 10), (2, 20), (3, 30), (4, 40)]);
451 let routing = compile(
452 &relation,
453 &session_of,
454 &BTreeMap::new(),
455 &silenced(&[(10, muted())]),
456 );
457
458 assert!(
459 routing.receivers(SessionId(10)).is_empty(),
460 "a muted microphone must have no line to anyone, by any primitive"
461 );
462 assert!(!routing.may_speak(SessionId(10)));
463 assert_eq!(
464 routing.receivers(SessionId(20)),
465 &[SessionId(10), SessionId(30), SessionId(40)],
466 "muting silences a microphone, not an ear: the muted one is still a \
467 receiver, and the others keep every line they had"
468 );
469 }
470
471 #[test]
472 fn a_deafened_receiver_appears_in_nobody_s_list() {
473 let mut relation = AudioRelation::default();
474 relation.domain(DomainId(1), &[ConnectionId(1), ConnectionId(2)]);
475 relation.edge(ConnectionId(3), ConnectionId(2));
476 relation.listen(ConnectionId(2), DomainId(1));
477
478 let session_of = sessions(&[(1, 10), (2, 20), (3, 30)]);
479 let routing = compile(
480 &relation,
481 &session_of,
482 &BTreeMap::new(),
483 &silenced(&[(20, deafened())]),
484 );
485
486 for sender in [SessionId(10), SessionId(30)] {
487 assert!(
488 !routing.receivers(sender).contains(&SessionId(20)),
489 "a deafened session must not be a receiver of {sender:?}"
490 );
491 }
492 assert_eq!(
493 routing.receivers(SessionId(20)),
494 &[SessionId(10)],
495 "deafening silences the ear, not the microphone"
496 );
497 assert!(routing.may_speak(SessionId(20)));
498 }
499
500 #[test]
501 fn the_server_flags_silence_exactly_as_their_self_counterparts_do() {
502 let server_muted = UserFlags {
506 mute: true,
507 ..UserFlags::default()
508 };
509 let suppressed = UserFlags {
510 suppress: true,
511 ..UserFlags::default()
512 };
513 let server_deafened = UserFlags {
514 deaf: true,
515 ..UserFlags::default()
516 };
517
518 let silence = silenced(&[(10, server_muted), (20, suppressed), (30, server_deafened)]);
519 assert!(!silence.may_speak(SessionId(10)));
520 assert!(!silence.may_speak(SessionId(20)));
521 assert!(!silence.may_hear(SessionId(30)));
522 assert!(
523 silence.may_speak(SessionId(30)),
524 "server-deafening is not server-muting"
525 );
526 }
527
528 #[test]
529 fn compile_agrees_with_the_relations_definition() {
530 let mut relation = AudioRelation::default();
535 relation.domain(
536 DomainId(1),
537 &[ConnectionId(1), ConnectionId(2), ConnectionId(3)],
538 );
539 relation.domain(DomainId(2), &[ConnectionId(3), ConnectionId(4)]);
540 relation.listen(ConnectionId(5), DomainId(1));
541 relation.listen(ConnectionId(5), DomainId(2));
542 relation.listen(ConnectionId(3), DomainId(2));
543 relation.listen(ConnectionId(6), DomainId(404));
544 relation.edge(ConnectionId(4), ConnectionId(1));
545 relation.edge(ConnectionId(1), ConnectionId(2));
546
547 let session_of = sessions(&[(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)]);
548 let compiled = compile(
549 &relation,
550 &session_of,
551 &BTreeMap::new(),
552 &Silence::default(),
553 );
554
555 let mut expected: BTreeMap<SessionId, Vec<SessionId>> = BTreeMap::new();
556 for (sender, receiver) in relation.resolve() {
557 let (Some(sender), Some(receiver)) =
558 (session_of.get(&sender), session_of.get(&receiver))
559 else {
560 continue;
561 };
562 expected.entry(*sender).or_default().push(*receiver);
563 }
564 for list in expected.values_mut() {
565 list.sort_unstable();
566 list.dedup();
567 }
568
569 for (sender, receivers) in &expected {
570 assert_eq!(
571 compiled.receivers(*sender),
572 receivers.as_slice(),
573 "compile disagrees with resolve for sender {sender:?}"
574 );
575 }
576 assert_eq!(
577 compiled.senders().collect::<Vec<SessionId>>(),
578 expected.keys().copied().collect::<Vec<SessionId>>(),
579 "compile and resolve must agree on which senders exist at all"
580 );
581 }
582
583 #[test]
584 fn since_is_carried_per_participant() {
585 let since = BTreeMap::from([(SessionId(100), 7), (SessionId(200), 9)]);
586 let routing = compile(
587 &AudioRelation::default(),
588 &BTreeMap::new(),
589 &since,
590 &Silence::default(),
591 );
592
593 assert_eq!(routing.since(SessionId(100)), Some(7));
594 assert_eq!(routing.since(SessionId(200)), Some(9));
595 assert_eq!(routing.since(SessionId(300)), None);
596 }
597}