1#[cfg(feature = "voip-runtime")]
5use std::mem::size_of;
6#[cfg(feature = "voip-runtime")]
7use std::sync::Arc;
8#[cfg(feature = "voip-runtime")]
9use std::time::Duration;
10
11#[cfg(feature = "voip-runtime")]
12use log::warn;
13use wacore::stanza::call::{TerminateParams, build_reject, build_terminate};
14#[cfg(feature = "voip-runtime")]
15use wacore::stanza::group_call::{
16 build_active_group_accept, build_active_group_preaccept, build_call_link_create,
17 build_call_link_join_with_capability, build_call_link_query, build_raise_hand,
18 build_screen_share, build_waiting_room_admit, build_waiting_room_deny,
19 build_waiting_room_heartbeat, build_waiting_room_toggle, parse_call_link_create_ack,
20 parse_call_link_join_ack, parse_call_link_join_call_id, parse_call_link_query_ack,
21 parse_waiting_room_admit_ack, parse_waiting_room_deny_ack, parse_waiting_room_toggle_ack,
22};
23use wacore::types::call::IncomingCall;
24#[cfg(feature = "voip-runtime")]
25use wacore::types::call::{CallAction, VideoState};
26#[cfg(feature = "voip-runtime")]
27use wacore::types::group_call::{
28 CallLink, CallLinkJoin, CallLinkMedia, CallLinkPreview, GroupCallUpdate, ScreenShare,
29 ScreenShareState, WaitingRoom,
30};
31#[cfg(feature = "voip-runtime")]
32use wacore::voip::{AudioFormat, CallEvent, CallPhase, CallSession, VideoControl};
33use wacore_binary::Jid;
34#[cfg(feature = "voip-runtime")]
35use wacore_binary::Node;
36#[cfg(feature = "voip-runtime")]
37use wacore_binary::Server;
38#[cfg(feature = "voip-runtime")]
39use zeroize::Zeroizing;
40
41#[cfg(feature = "voip-runtime")]
42use super::ResponseWaiter;
43use super::{Client, ClientError};
44
45#[cfg(feature = "voip-runtime")]
46const CALL_SERVICE_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
47#[cfg(feature = "voip-runtime")]
48const WAITING_ROOM_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10);
49#[cfg(feature = "voip-runtime")]
50const WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES: u8 = 3;
51#[cfg(feature = "voip-runtime")]
52const MAX_PENDING_CALL_LINK_TRANSITIONS: usize = 32;
53#[cfg(feature = "voip-runtime")]
54const MAX_PENDING_CALL_LINK_TRANSITION_BYTES: usize = 1024 * 1024;
55#[cfg(feature = "voip-runtime")]
56const MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS: usize = 32;
57
58pub struct Voip<'a> {
61 client: &'a Client,
62}
63
64#[cfg(feature = "voip-runtime")]
65struct CallLinkRegistrationGuard {
66 client: std::sync::Weak<Client>,
67 registry: Arc<wacore::voip::CallRegistry>,
68 call_id: String,
69 call_creator: Jid,
70 generation: u64,
71 armed: bool,
72}
73
74#[cfg(feature = "voip-runtime")]
75pub(crate) struct CallLinkJoinRegistration {
76 pub(crate) join: CallLinkJoin,
77 pub(crate) generation: u64,
78}
79
80#[cfg(feature = "voip-runtime")]
81#[derive(Clone, Copy)]
82enum WaitingRoomUserAction {
83 Admit,
84 Deny,
85}
86
87#[cfg(feature = "voip-runtime")]
88enum PendingCallLinkTransition {
89 Group(Box<GroupCallUpdate>),
90 WaitingRoom(WaitingRoom),
91 RawEpoch {
92 call_creator: Jid,
93 sender: Jid,
94 transaction_id: u32,
95 raw_epoch: Zeroizing<Vec<u8>>,
96 },
97 Terminated {
98 call_creator: Jid,
99 sender: Jid,
100 },
101 Saturated,
102}
103
104#[cfg(feature = "voip-runtime")]
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub(crate) enum PendingCallLinkBuffer {
107 NotPending,
108 Buffered,
109 Saturated,
110}
111
112#[cfg(feature = "voip-runtime")]
113impl PendingCallLinkBuffer {
114 pub(crate) fn suppresses_dispatch(self) -> bool {
115 self != Self::NotPending
116 }
117}
118
119#[cfg(feature = "voip-runtime")]
120impl PendingCallLinkTransition {
121 fn group_heap_bytes(update: &GroupCallUpdate) -> usize {
122 use wacore::stats::HeapSize;
123
124 size_of::<GroupCallUpdate>() + update.heap_bytes()
125 }
126
127 fn waiting_room_heap_bytes(room: &WaitingRoom) -> usize {
128 use wacore::stats::HeapSize;
129
130 room.heap_bytes()
131 }
132
133 fn heap_bytes(&self) -> usize {
134 use wacore::stats::HeapSize;
135
136 match self {
137 Self::Group(update) => Self::group_heap_bytes(update),
138 Self::WaitingRoom(room) => Self::waiting_room_heap_bytes(room),
139 Self::RawEpoch {
140 call_creator,
141 sender,
142 raw_epoch,
143 ..
144 } => call_creator
145 .heap_bytes()
146 .saturating_add(sender.heap_bytes())
147 .saturating_add(raw_epoch.capacity()),
148 Self::Terminated {
149 call_creator,
150 sender,
151 } => call_creator
152 .heap_bytes()
153 .saturating_add(sender.heap_bytes()),
154 Self::Saturated => 0,
155 }
156 }
157}
158
159#[cfg(feature = "voip-runtime")]
160#[derive(Default)]
161pub(super) struct PendingCallLinkJoins {
162 active: usize,
163 bound_call_id: Option<String>,
164 transitions: std::collections::HashMap<String, Vec<PendingCallLinkTransition>>,
165 saturation_fingerprints: Vec<u64>,
166 saturation_hash_builder: std::collections::hash_map::RandomState,
167 untracked_saturation: bool,
168}
169
170#[cfg(feature = "voip-runtime")]
171impl PendingCallLinkJoins {
172 fn accepts(&self, call_id: &str) -> bool {
173 self.bound_call_id
174 .as_deref()
175 .is_none_or(|bound| bound == call_id)
176 }
177
178 fn can_buffer_transition(&self, call_id: &str, payload_bytes: usize) -> bool {
179 use wacore::stats::HeapSize;
180
181 let entries = self.transitions.values().map(Vec::len).sum::<usize>();
182 if entries >= MAX_PENDING_CALL_LINK_TRANSITIONS {
183 return false;
184 }
185 let new_key_bytes = if self.transitions.contains_key(call_id) {
186 0
187 } else {
188 size_of::<String>() + call_id.heap_bytes()
189 };
190 let structural_reserve = MAX_PENDING_CALL_LINK_TRANSITIONS
191 .saturating_mul(size_of::<PendingCallLinkTransition>());
192 self.memory_stats()
193 .bytes
194 .saturating_add(payload_bytes.try_into().unwrap_or(u64::MAX))
195 .saturating_add(new_key_bytes.try_into().unwrap_or(u64::MAX))
196 .saturating_add(structural_reserve.try_into().unwrap_or(u64::MAX))
197 <= MAX_PENDING_CALL_LINK_TRANSITION_BYTES as u64
198 }
199
200 fn bind_call_id(&mut self, call_id: &str) {
201 let fingerprint = self.call_id_fingerprint(call_id);
202 self.bound_call_id = Some(call_id.to_string());
203 self.transitions.retain(|retained, _| retained == call_id);
204 self.saturation_fingerprints
205 .retain(|retained| *retained == fingerprint);
206 }
207
208 fn prepare_bound_retry(&mut self, call_id: &str) -> bool {
209 if !self.untracked_saturation || self.bound_call_id.as_deref() != Some(call_id) {
210 return false;
211 }
212 self.transitions.clear();
218 self.saturation_fingerprints.clear();
219 self.untracked_saturation = false;
220 true
221 }
222
223 fn is_saturated(&self, call_id: &str) -> bool {
224 self.untracked_saturation
225 || self
226 .saturation_fingerprints
227 .contains(&self.call_id_fingerprint(call_id))
228 || self.transitions.get(call_id).is_some_and(|transitions| {
229 transitions
230 .iter()
231 .any(|transition| matches!(transition, PendingCallLinkTransition::Saturated))
232 })
233 }
234
235 fn call_id_fingerprint(&self, call_id: &str) -> u64 {
236 use std::hash::BuildHasher;
237
238 self.saturation_hash_builder.hash_one(call_id)
239 }
240
241 fn mark_saturated(&mut self, call_id: &str) {
242 self.transitions.remove(call_id);
246 if self.can_buffer_transition(call_id, 0) {
247 self.transitions.insert(
248 call_id.to_string(),
249 vec![PendingCallLinkTransition::Saturated],
250 );
251 return;
252 }
253 let fingerprint = self.call_id_fingerprint(call_id);
254 if self.saturation_fingerprints.contains(&fingerprint) {
255 return;
256 }
257 if self.saturation_fingerprints.len() < MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
258 self.saturation_fingerprints.push(fingerprint);
262 } else {
263 self.untracked_saturation = true;
267 }
268 }
269
270 pub(super) fn memory_stats(&self) -> wacore::stats::CollectionStats {
271 use wacore::stats::HeapSize;
272
273 let transition_bytes = self
274 .transitions
275 .iter()
276 .map(|(call_id, transitions)| {
277 size_of::<String>()
278 + call_id.heap_bytes()
279 + transitions.capacity() * size_of::<PendingCallLinkTransition>()
280 + transitions
281 .iter()
282 .map(PendingCallLinkTransition::heap_bytes)
283 .sum::<usize>()
284 })
285 .sum::<usize>();
286 let bytes = transition_bytes
287 .saturating_add(self.saturation_fingerprints.capacity() * size_of::<u64>());
288 wacore::stats::CollectionStats::new(
289 self.transitions
290 .values()
291 .map(Vec::len)
292 .sum::<usize>()
293 .saturating_add(self.saturation_fingerprints.len())
294 .saturating_add(usize::from(self.untracked_saturation))
295 .try_into()
296 .unwrap_or(u64::MAX),
297 bytes.try_into().unwrap_or(u64::MAX),
298 )
299 }
300}
301
302#[cfg(feature = "voip-runtime")]
303struct PendingCallLinkJoinGuard {
304 state: Arc<std::sync::Mutex<PendingCallLinkJoins>>,
305}
306
307#[cfg(feature = "voip-runtime")]
308impl Drop for PendingCallLinkJoinGuard {
309 fn drop(&mut self) {
310 let mut state = self
311 .state
312 .lock()
313 .unwrap_or_else(|poisoned| poisoned.into_inner());
314 state.active = state.active.saturating_sub(1);
315 if state.active == 0 {
316 state.bound_call_id = None;
317 state.transitions.clear();
318 state.saturation_fingerprints.clear();
319 state.untracked_saturation = false;
320 }
321 }
322}
323
324#[cfg(feature = "voip-runtime")]
325impl CallLinkRegistrationGuard {
326 fn new(
327 client: &Client,
328 registry: Arc<wacore::voip::CallRegistry>,
329 call_id: &str,
330 call_creator: Jid,
331 generation: u64,
332 ) -> Self {
333 Self {
334 client: client.self_weak.get().cloned().unwrap_or_default(),
335 registry,
336 call_id: call_id.to_string(),
337 call_creator,
338 generation,
339 armed: true,
340 }
341 }
342
343 fn disarm(&mut self) {
344 self.armed = false;
345 }
346}
347
348#[cfg(feature = "voip-runtime")]
349impl Drop for CallLinkRegistrationGuard {
350 fn drop(&mut self) {
351 if !self.armed {
352 return;
353 }
354 let Some(client) = self.client.upgrade() else {
355 self.registry
356 .remove_if_current(&self.call_id, self.generation);
357 return;
358 };
359 let registry = self.registry.clone();
360 let call_id = self.call_id.clone();
361 let call_creator = self.call_creator.clone();
362 let generation = self.generation;
363 let runtime = client.runtime.clone();
364 runtime
365 .spawn(Box::pin(async move {
366 let _transition = client.lock_answer_transition(&call_id).await;
370 let Some(phase) = registry.remove_if_current_with_phase(&call_id, generation)
371 else {
372 return;
373 };
374 if phase == CallPhase::WaitingRoom {
375 return;
376 }
377 let target = Jid::new(&call_id, Server::Call);
378 crate::voip::facade::send_answer_terminate(
379 &client,
380 &call_id,
381 &target,
382 &call_creator,
383 )
384 .await;
385 }))
386 .detach();
387 }
388}
389
390impl Client {
391 pub fn voip(&self) -> Voip<'_> {
394 Voip { client: self }
395 }
396
397 #[cfg(feature = "voip-runtime")]
400 pub(crate) fn call_registry(&self) -> Arc<wacore::voip::CallRegistry> {
401 self.call_registry.clone()
402 }
403
404 #[cfg(feature = "voip-runtime")]
405 fn begin_call_link_join(&self) -> PendingCallLinkJoinGuard {
406 let mut state = self
407 .pending_call_link_joins
408 .lock()
409 .unwrap_or_else(|poisoned| poisoned.into_inner());
410 if state.active == 0 {
411 state.bound_call_id = None;
412 state.transitions.clear();
413 state.saturation_fingerprints.clear();
414 state.untracked_saturation = false;
415 }
416 state.active = state.active.saturating_add(1);
417 drop(state);
418 PendingCallLinkJoinGuard {
419 state: self.pending_call_link_joins.clone(),
420 }
421 }
422
423 #[cfg(feature = "voip-runtime")]
424 pub(crate) fn pending_call_link_control_candidate(
425 &self,
426 call_id: &str,
427 call_creator: &Jid,
428 sender: &Jid,
429 ) -> bool {
430 let state = self
431 .pending_call_link_joins
432 .lock()
433 .unwrap_or_else(|poisoned| poisoned.into_inner());
434 state.active != 0
435 && !call_id.is_empty()
436 && state.accepts(call_id)
437 && sender.to_non_ad() == call_creator.to_non_ad()
438 && self.call_registry.generation_of(call_id).is_none()
439 }
440
441 #[cfg(feature = "voip-runtime")]
445 pub(crate) fn bind_pending_call_link_join_ack(&self, response: &wacore_binary::NodeRef<'_>) {
446 let Ok(call_id) = parse_call_link_join_call_id(response) else {
447 return;
448 };
449 let mut state = self
450 .pending_call_link_joins
451 .lock()
452 .unwrap_or_else(|poisoned| poisoned.into_inner());
453 if state.active != 0 {
454 state.bind_call_id(&call_id);
455 }
456 }
457
458 #[cfg(feature = "voip-runtime")]
459 fn prepare_pending_call_link_join_retry(&self, call_id: &str) -> bool {
460 let mut state = self
461 .pending_call_link_joins
462 .lock()
463 .unwrap_or_else(|poisoned| poisoned.into_inner());
464 state.prepare_bound_retry(call_id)
465 }
466
467 #[cfg(feature = "voip-runtime")]
471 pub(crate) fn buffer_pending_call_link_update(
472 &self,
473 update: &GroupCallUpdate,
474 sender: &Jid,
475 ) -> PendingCallLinkBuffer {
476 let mut state = self
477 .pending_call_link_joins
478 .lock()
479 .unwrap_or_else(|poisoned| poisoned.into_inner());
480 if state.active == 0
481 || update.call_id.is_empty()
482 || !state.accepts(&update.call_id)
483 || sender.to_non_ad() != update.call_creator.to_non_ad()
484 || self.call_registry.generation_of(&update.call_id).is_some()
485 {
486 return PendingCallLinkBuffer::NotPending;
487 }
488 if state.is_saturated(&update.call_id) {
489 return PendingCallLinkBuffer::Saturated;
490 }
491 if state
492 .transitions
493 .get(&update.call_id)
494 .into_iter()
495 .flatten()
496 .rev()
497 .find_map(|transition| match transition {
498 PendingCallLinkTransition::Group(update) => Some(update.transaction_id),
499 PendingCallLinkTransition::WaitingRoom(_)
500 | PendingCallLinkTransition::RawEpoch { .. }
501 | PendingCallLinkTransition::Terminated { .. }
502 | PendingCallLinkTransition::Saturated => None,
503 })
504 .is_some_and(|transaction_id| transaction_id >= update.transaction_id)
505 {
506 return PendingCallLinkBuffer::Buffered;
507 }
508 if !state.can_buffer_transition(
509 &update.call_id,
510 PendingCallLinkTransition::group_heap_bytes(update),
511 ) {
512 state.mark_saturated(&update.call_id);
513 return PendingCallLinkBuffer::Saturated;
514 }
515 state
516 .transitions
517 .entry(update.call_id.clone())
518 .or_default()
519 .push(PendingCallLinkTransition::Group(Box::new(update.clone())));
520 PendingCallLinkBuffer::Buffered
521 }
522
523 #[cfg(feature = "voip-runtime")]
525 pub(crate) fn buffer_pending_call_link_epoch(
526 &self,
527 call_id: &str,
528 call_creator: &Jid,
529 sender: &Jid,
530 transaction_id: u32,
531 raw_epoch: &[u8],
532 ) -> PendingCallLinkBuffer {
533 let mut state = self
534 .pending_call_link_joins
535 .lock()
536 .unwrap_or_else(|poisoned| poisoned.into_inner());
537 if state.active == 0
538 || call_id.is_empty()
539 || !state.accepts(call_id)
540 || sender.to_non_ad() != call_creator.to_non_ad()
541 || self.call_registry.generation_of(call_id).is_some()
542 {
543 return PendingCallLinkBuffer::NotPending;
544 }
545 if state.is_saturated(call_id) {
546 return PendingCallLinkBuffer::Saturated;
547 }
548 if state
549 .transitions
550 .get(call_id)
551 .into_iter()
552 .flatten()
553 .rev()
554 .find_map(|transition| match transition {
555 PendingCallLinkTransition::RawEpoch {
556 call_creator: retained_creator,
557 sender: retained_sender,
558 transaction_id,
559 ..
560 } if retained_creator == call_creator && retained_sender == sender => {
561 Some(*transaction_id)
562 }
563 _ => None,
564 })
565 .is_some_and(|retained| retained >= transaction_id)
566 {
567 return PendingCallLinkBuffer::Buffered;
568 }
569 if !state.can_buffer_transition(call_id, raw_epoch.len()) {
570 state.mark_saturated(call_id);
571 return PendingCallLinkBuffer::Saturated;
572 }
573 state
574 .transitions
575 .entry(call_id.to_string())
576 .or_default()
577 .push(PendingCallLinkTransition::RawEpoch {
578 call_creator: call_creator.clone(),
579 sender: sender.clone(),
580 transaction_id,
581 raw_epoch: Zeroizing::new(raw_epoch.to_vec()),
582 });
583 PendingCallLinkBuffer::Buffered
584 }
585
586 #[cfg(feature = "voip-runtime")]
588 pub(crate) fn buffer_pending_call_link_terminate(
589 &self,
590 call_id: &str,
591 call_creator: &Jid,
592 sender: &Jid,
593 ) -> PendingCallLinkBuffer {
594 let mut state = self
595 .pending_call_link_joins
596 .lock()
597 .unwrap_or_else(|poisoned| poisoned.into_inner());
598 if state.active == 0
599 || call_id.is_empty()
600 || !state.accepts(call_id)
601 || sender.to_non_ad() != call_creator.to_non_ad()
602 || self.call_registry.generation_of(call_id).is_some()
603 {
604 return PendingCallLinkBuffer::NotPending;
605 }
606 if state.is_saturated(call_id) {
607 return PendingCallLinkBuffer::Saturated;
608 }
609 if state
610 .transitions
611 .get(call_id)
612 .into_iter()
613 .flatten()
614 .any(|transition| {
615 matches!(
616 transition,
617 PendingCallLinkTransition::Terminated {
618 call_creator: retained_creator,
619 sender: retained_sender,
620 } if retained_creator == call_creator && retained_sender == sender
621 )
622 })
623 {
624 return PendingCallLinkBuffer::Buffered;
625 }
626 if !state.can_buffer_transition(call_id, 0) {
627 state.mark_saturated(call_id);
628 return PendingCallLinkBuffer::Saturated;
629 }
630 state
631 .transitions
632 .entry(call_id.to_string())
633 .or_default()
634 .push(PendingCallLinkTransition::Terminated {
635 call_creator: call_creator.clone(),
636 sender: sender.clone(),
637 });
638 PendingCallLinkBuffer::Buffered
639 }
640
641 #[cfg(feature = "voip-runtime")]
643 pub(crate) async fn retain_or_apply_pending_call_link_terminate(
644 &self,
645 call_id: &str,
646 call_creator: &Jid,
647 sender: &Jid,
648 ) -> bool {
649 let _answer_transition = self.lock_answer_transition(call_id).await;
650 let buffered = self.buffer_pending_call_link_terminate(call_id, call_creator, sender);
651 if buffered.suppresses_dispatch() {
652 return true;
653 }
654 let Some(generation) = self.call_registry.generation_of(call_id) else {
655 return false;
656 };
657 if !self.call_registry.group_creator_authorized_if_current(
658 call_id,
659 generation,
660 call_creator,
661 sender,
662 ) {
663 return false;
664 }
665 self.call_registry.remove_if_current(call_id, generation)
666 }
667
668 #[cfg(feature = "voip-runtime")]
671 pub(crate) fn buffer_pending_call_link_waiting_room(
672 &self,
673 room: &WaitingRoom,
674 sender: &Jid,
675 ) -> PendingCallLinkBuffer {
676 let mut state = self
677 .pending_call_link_joins
678 .lock()
679 .unwrap_or_else(|poisoned| poisoned.into_inner());
680 if self.call_registry.generation_of(&room.call_id).is_some()
681 || state.active == 0
682 || room.call_id.is_empty()
683 || !state.accepts(&room.call_id)
684 || room.link_token.is_empty()
685 || sender.to_non_ad() != room.call_creator.to_non_ad()
686 {
687 return PendingCallLinkBuffer::NotPending;
688 }
689 if state.is_saturated(&room.call_id) {
690 return PendingCallLinkBuffer::Saturated;
691 }
692 if !state.can_buffer_transition(
693 &room.call_id,
694 PendingCallLinkTransition::waiting_room_heap_bytes(room),
695 ) {
696 state.mark_saturated(&room.call_id);
697 return PendingCallLinkBuffer::Saturated;
698 }
699 state
700 .transitions
701 .entry(room.call_id.clone())
702 .or_default()
703 .push(PendingCallLinkTransition::WaitingRoom(room.clone()));
704 PendingCallLinkBuffer::Buffered
705 }
706
707 #[cfg(feature = "voip-runtime")]
708 async fn register_call_link_session(
709 &self,
710 session: CallSession,
711 waiting_room: Option<WaitingRoom>,
712 expected_media: CallLinkMedia,
713 expected_token: &str,
714 ) -> Result<u64, wacore::voip::GroupStateApply> {
715 let call_id = session.call_id.clone();
716 let call_creator = session.call_creator.clone();
717 let mut rekey_pending = session
718 .group
719 .as_ref()
720 .is_some_and(|update| update.rekey_requested);
721 let _answer_transition = self.lock_answer_transition(&call_id).await;
725 let mut state = self
726 .pending_call_link_joins
727 .lock()
728 .unwrap_or_else(|poisoned| poisoned.into_inner());
729 state.bind_call_id(&call_id);
730 let saturated = state.is_saturated(&call_id);
731 let staged = state.transitions.remove(&call_id).unwrap_or_default();
732 if saturated {
733 return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
734 }
735 let generation = self.call_registry.insert_call_link_checked(session)?;
736 if let Some(room) = waiting_room {
737 let applied = self
738 .call_registry
739 .apply_waiting_room_if_current(room, generation);
740 if applied != wacore::voip::GroupStateApply::Applied {
741 self.call_registry.remove_if_current(&call_id, generation);
742 return Err(applied);
743 }
744 }
745 for transition in staged {
746 match transition {
747 PendingCallLinkTransition::Group(update)
748 if update.call_creator == call_creator
749 && update.media == expected_media.as_str() =>
750 {
751 let mut update = *update;
752 update.rekey_requested |= rekey_pending;
753 let staged_rekey = update.rekey_requested;
754 match self.apply_pending_call_link_update(update, generation) {
755 wacore::voip::GroupStateApply::Applied => {
756 rekey_pending = staged_rekey;
757 }
758 wacore::voip::GroupStateApply::Stale => {}
759 rejected => {
760 self.call_registry.remove_if_current(&call_id, generation);
761 return Err(rejected);
762 }
763 }
764 }
765 PendingCallLinkTransition::WaitingRoom(room)
766 if room.call_creator == call_creator
767 && room.media == expected_media
768 && room.link_token == expected_token =>
769 {
770 let applied = self
771 .call_registry
772 .apply_waiting_room_if_current(room, generation);
773 if !matches!(
774 applied,
775 wacore::voip::GroupStateApply::Applied
776 | wacore::voip::GroupStateApply::Stale
777 ) {
778 self.call_registry.remove_if_current(&call_id, generation);
779 return Err(applied);
780 }
781 }
782 PendingCallLinkTransition::RawEpoch {
783 call_creator: staged_creator,
784 sender,
785 transaction_id,
786 raw_epoch,
787 } => {
788 if !self.call_registry.group_sender_authorized_if_current(
789 &call_id,
790 generation,
791 &staged_creator,
792 &sender,
793 ) {
794 continue;
795 }
796 if !self.call_registry.send_group_epoch_if_current(
797 &call_id,
798 generation,
799 transaction_id,
800 raw_epoch.to_vec(),
801 ) {
802 self.call_registry.remove_if_current(&call_id, generation);
803 return Err(wacore::voip::GroupStateApply::UnknownCall);
804 }
805 }
806 PendingCallLinkTransition::Terminated {
807 call_creator: staged_creator,
808 sender,
809 } => {
810 if !self.call_registry.group_creator_authorized_if_current(
811 &call_id,
812 generation,
813 &staged_creator,
814 &sender,
815 ) {
816 continue;
817 }
818 self.call_registry.remove_if_current(&call_id, generation);
819 return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
820 }
821 PendingCallLinkTransition::Saturated => {
822 self.call_registry.remove_if_current(&call_id, generation);
823 return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
824 }
825 _ => {}
826 }
827 }
828 Ok(generation)
829 }
830
831 #[cfg(feature = "voip-runtime")]
832 fn apply_pending_call_link_update(
833 &self,
834 update: GroupCallUpdate,
835 generation: u64,
836 ) -> wacore::voip::GroupStateApply {
837 self.call_registry
838 .apply_group_update_if_current(update, generation)
839 }
840
841 #[cfg(feature = "voip-runtime")]
845 pub(crate) fn answer_transition_lock(&self, call_id: &str) -> Arc<async_lock::Mutex<()>> {
846 use std::hash::{Hash, Hasher};
847
848 let mut hasher = std::collections::hash_map::DefaultHasher::new();
849 call_id.hash(&mut hasher);
850 let lane = hasher.finish() as usize % self.answer_transition_locks.len();
851 self.answer_transition_locks[lane].clone()
852 }
853
854 #[cfg(feature = "voip-runtime")]
855 pub(crate) async fn lock_answer_transition(
856 &self,
857 call_id: &str,
858 ) -> async_lock::MutexGuardArc<()> {
859 self.answer_transition_lock(call_id).lock_arc().await
860 }
861}
862
863#[derive(Debug, thiserror::Error)]
866#[non_exhaustive]
867pub enum CallError {
868 #[error("{0}")]
869 Send(#[from] ClientError),
870 #[error("call_id cannot be empty")]
871 EmptyCallId,
872 #[cfg(feature = "voip-runtime")]
874 #[error("not an incoming call offer")]
875 NotAnOffer,
876 #[cfg(feature = "voip-runtime")]
878 #[error("accept() requires audio(...) or encoded_audio(...) before start()")]
879 MissingAudio,
880 #[cfg(feature = "voip-runtime")]
882 #[error("incoming offer does not advertise the selected audio rate {0}")]
883 AudioFormatNotOffered(u32),
884 #[cfg(feature = "voip-runtime")]
886 #[error("incoming offer did not advertise video; use start_video() after answering")]
887 VideoNotOffered,
888 #[cfg(feature = "voip-runtime")]
890 #[error("call ended during answer setup")]
891 CallEndedDuringSetup,
892 #[cfg(feature = "voip-runtime")]
894 #[error("callKey decrypt failed: {0}")]
895 Decrypt(String),
896 #[cfg(feature = "voip-runtime")]
898 #[error("call setup failed: {0}")]
899 Setup(String),
900 #[cfg(feature = "voip-runtime")]
902 #[error("relay connect failed: {0}")]
903 Connect(String),
904 #[cfg(feature = "voip-runtime")]
906 #[error("media offer error: {0}")]
907 Media(&'static str),
908 #[cfg(feature = "voip-runtime")]
910 #[error("video upgrade request is no longer current")]
911 VideoUpgradeExpired,
912 #[cfg(feature = "voip-runtime")]
914 #[error("peer has no resolvable devices")]
915 NoDevices,
916 #[cfg(feature = "voip-runtime")]
920 #[error("offer pkmsg requires <device-identity> (account is None)")]
921 MissingDeviceIdentity,
922 #[cfg(feature = "voip-runtime")]
924 #[error("call service response failed: {0}")]
925 Response(String),
926 #[cfg(feature = "voip-runtime")]
928 #[error("call service request timed out")]
929 ResponseTimeout,
930}
931
932impl Voip<'_> {
933 pub async fn reject(&self, incoming: &IncomingCall) -> Result<(), CallError> {
935 self.reject_call_inner(
936 incoming.action.call_id(),
937 &incoming.from,
938 incoming.action.call_creator(),
939 incoming.ringing_generation(),
940 )
941 .await
942 }
943
944 pub async fn reject_call(
950 &self,
951 call_id: &str,
952 peer: &Jid,
953 call_creator: &Jid,
954 ) -> Result<(), CallError> {
955 self.reject_call_inner(call_id, peer, call_creator, None)
956 .await
957 }
958
959 async fn reject_call_inner(
960 &self,
961 call_id: &str,
962 peer: &Jid,
963 call_creator: &Jid,
964 _ringing_generation: Option<u64>,
965 ) -> Result<(), CallError> {
966 if call_id.is_empty() {
967 return Err(CallError::EmptyCallId);
968 }
969 let id = self.client.generate_request_id();
970 let stanza = build_reject(call_id, peer, call_creator, &id);
971 #[cfg(feature = "voip-runtime")]
975 {
976 let registry = self.client.call_registry();
977 if let Some(generation) = _ringing_generation {
978 if !registry.reject_ringing_if_current(call_id, generation) {
979 return Err(CallError::CallEndedDuringSetup);
980 }
981 } else {
982 let generation = registry.ringing_group_generation(call_id, call_creator);
983 registry.take_ringing(call_id);
984 if let Some(generation) = generation {
985 registry.remove_if_current(call_id, generation);
986 }
987 }
988 }
989 self.client.send_node(stanza).await?;
990 Ok(())
991 }
992
993 #[cfg(feature = "voip-runtime")]
999 pub fn accept<'b>(&'b self, incoming: &'b IncomingCall) -> crate::voip::AcceptCall<'b> {
1000 crate::voip::facade::AcceptCall::new(self.client, incoming)
1001 }
1002
1003 #[cfg(feature = "voip-runtime")]
1010 pub fn call<'b>(&'b self, peer: &'b Jid) -> crate::voip::OutgoingCall<'b> {
1011 crate::voip::facade::OutgoingCall::new(self.client, peer)
1012 }
1013
1014 #[cfg(feature = "voip-runtime")]
1016 pub fn group_call<'b>(&'b self, targets: &'b [Jid]) -> crate::voip::OutgoingGroupCall<'b> {
1017 crate::voip::facade::OutgoingGroupCall::new(self.client, targets)
1018 }
1019
1020 #[cfg(feature = "voip-runtime")]
1023 pub fn group_call_by_id<'b>(&'b self, group_jid: &'b Jid) -> crate::voip::GroupBoundCall<'b> {
1024 crate::voip::facade::GroupBoundCall::new(self.client, group_jid)
1025 }
1026
1027 #[cfg(feature = "voip-runtime")]
1029 pub fn call_link<'b>(
1030 &'b self,
1031 token_or_url: &'b str,
1032 media: CallLinkMedia,
1033 ) -> crate::voip::CallLinkCall<'b> {
1034 crate::voip::facade::CallLinkCall::new(self.client, token_or_url, media)
1035 }
1036
1037 #[cfg(feature = "voip-runtime")]
1039 pub async fn preaccept_group_invite(&self, incoming: &IncomingCall) -> Result<(), CallError> {
1040 let CallAction::Offer {
1041 call_id,
1042 call_creator,
1043 is_video,
1044 ..
1045 } = &incoming.action
1046 else {
1047 return Err(CallError::NotAnOffer);
1048 };
1049 if incoming.group.is_none() {
1050 return Err(CallError::Media("offer is not an active group invitation"));
1051 }
1052 let registry = self.client.call_registry();
1053 let Some(retained_generation) = incoming.ringing_generation() else {
1054 return Err(CallError::CallEndedDuringSetup);
1055 };
1056 let generation = registry
1057 .ringing_group_generation(call_id, call_creator)
1058 .ok_or(CallError::CallEndedDuringSetup)?;
1059 if generation != retained_generation {
1060 return Err(CallError::CallEndedDuringSetup);
1061 }
1062 let node = build_active_group_preaccept(
1063 call_id,
1064 call_creator,
1065 &self.client.generate_request_id(),
1066 *is_video,
1067 )
1068 .map_err(|error| CallError::Response(error.to_string()))?;
1069 self.client.send_node(node).await?;
1070 if registry.ringing_group_generation(call_id, call_creator) != Some(generation) {
1071 return Err(CallError::CallEndedDuringSetup);
1072 }
1073 Ok(())
1074 }
1075
1076 #[cfg(feature = "voip-runtime")]
1081 pub async fn accept_group_invite(&self, incoming: &IncomingCall) -> Result<(), CallError> {
1082 let CallAction::Offer {
1083 call_id,
1084 call_creator,
1085 is_video,
1086 ..
1087 } = &incoming.action
1088 else {
1089 return Err(CallError::NotAnOffer);
1090 };
1091 if incoming.group.is_none() {
1092 return Err(CallError::Media("offer is not an active group invitation"));
1093 }
1094 let registry = self.client.call_registry();
1095 let Some(retained_generation) = incoming.ringing_generation() else {
1096 return Err(CallError::CallEndedDuringSetup);
1097 };
1098 let generation = registry
1099 .ringing_group_generation(call_id, call_creator)
1100 .ok_or(CallError::CallEndedDuringSetup)?;
1101 if generation != retained_generation {
1102 return Err(CallError::CallEndedDuringSetup);
1103 }
1104 let node = build_active_group_accept(
1105 call_id,
1106 call_creator,
1107 &self.client.generate_request_id(),
1108 *is_video,
1109 )
1110 .map_err(|error| CallError::Response(error.to_string()))?;
1111 self.client.send_node(node).await?;
1112 if registry.ringing_group_generation(call_id, call_creator) != Some(generation) {
1113 return Err(CallError::CallEndedDuringSetup);
1114 }
1115 Ok(())
1116 }
1117
1118 #[cfg(feature = "voip-runtime")]
1120 pub async fn create_call_link(&self, media: CallLinkMedia) -> Result<CallLink, CallError> {
1121 let request_id = self.client.generate_request_id();
1122 let request = build_call_link_create(media, &request_id)
1123 .map_err(|error| CallError::Response(error.to_string()))?;
1124 let link = execute_call_service_request(
1125 self.client,
1126 &request_id,
1127 request,
1128 parse_call_link_create_ack,
1129 )
1130 .await?;
1131 if link.media != media {
1132 return Err(CallError::Response(
1133 "call-link creation changed the requested media mode".to_string(),
1134 ));
1135 }
1136 Ok(link)
1137 }
1138
1139 #[cfg(feature = "voip-runtime")]
1141 pub async fn preview_call_link(
1142 &self,
1143 token_or_url: &str,
1144 media: CallLinkMedia,
1145 ) -> Result<CallLinkPreview, CallError> {
1146 let token = normalize_call_link_token(token_or_url, media)?;
1147 let request_id = self.client.generate_request_id();
1148 let request = build_call_link_query(&token, media, &request_id)
1149 .map_err(|error| CallError::Response(error.to_string()))?;
1150 let preview = execute_call_service_request(
1151 self.client,
1152 &request_id,
1153 request,
1154 parse_call_link_query_ack,
1155 )
1156 .await?;
1157 if preview.token != token || preview.media != media {
1158 return Err(CallError::Response(
1159 "call-link preview changed the requested link identity".to_string(),
1160 ));
1161 }
1162 Ok(preview)
1163 }
1164
1165 #[cfg(feature = "voip-runtime")]
1168 pub async fn join_call_link(
1169 &self,
1170 token_or_url: &str,
1171 media: CallLinkMedia,
1172 ) -> Result<CallLinkJoin, CallError> {
1173 self.join_call_link_with_audio(token_or_url, media, AudioFormat::MLOW_16KHZ_60MS)
1174 .await
1175 }
1176
1177 #[cfg(feature = "voip-runtime")]
1178 pub(crate) async fn join_call_link_with_audio(
1179 &self,
1180 token_or_url: &str,
1181 media: CallLinkMedia,
1182 audio_format: AudioFormat,
1183 ) -> Result<CallLinkJoin, CallError> {
1184 Ok(self
1185 .join_call_link_registration_with_audio(token_or_url, media, audio_format)
1186 .await?
1187 .join)
1188 }
1189
1190 #[cfg(feature = "voip-runtime")]
1191 pub(crate) async fn join_call_link_registration_with_audio(
1192 &self,
1193 token_or_url: &str,
1194 media: CallLinkMedia,
1195 audio_format: AudioFormat,
1196 ) -> Result<CallLinkJoinRegistration, CallError> {
1197 let pending_join_lane = self.client.pending_call_link_join_lane.lock().await;
1202 let own_lid = self.client.lid().ok_or(CallError::Media("no own LID"))?;
1203 let token = normalize_call_link_token(token_or_url, media)?;
1204 let capability =
1205 crate::voip::facade::offer_capability(media == CallLinkMedia::Video, audio_format);
1206 let pending_join = self.client.begin_call_link_join();
1207 let mut join =
1208 execute_call_link_join_request(self.client, &token, media, capability).await?;
1209 if join.media != media {
1210 return Err(CallError::Response(
1211 "call-link response changed the requested media mode".to_string(),
1212 ));
1213 }
1214 if join.call_id.is_empty() {
1215 return Err(CallError::EmptyCallId);
1216 }
1217 if self
1218 .client
1219 .prepare_pending_call_link_join_retry(&join.call_id)
1220 {
1221 let first_call_id = join.call_id.clone();
1222 let first_call_creator = join.call_creator.clone();
1223 let refreshed =
1224 execute_call_link_join_request(self.client, &token, media, capability).await?;
1225 if refreshed.media != media
1226 || refreshed.call_id != first_call_id
1227 || refreshed.call_creator != first_call_creator
1228 {
1229 return Err(CallError::Response(
1230 "call-link identity changed while refreshing admission state".to_string(),
1231 ));
1232 }
1233 join = refreshed;
1234 }
1235
1236 let mut session = CallSession::new_outgoing(
1237 &join.call_id,
1238 Jid::new(&join.call_id, Server::Call),
1239 join.call_creator.clone(),
1240 );
1241 session.audio_format = Some(audio_format);
1242 session.is_video = media == CallLinkMedia::Video;
1243 session.group = join.group.clone();
1244 let _ = session.transition_to(CallPhase::Calling);
1245 let _ = session.transition_to(if join.in_waiting_room {
1246 CallPhase::WaitingRoom
1247 } else {
1248 CallPhase::Connecting
1249 });
1250 let registry = self.client.call_registry();
1251 let generation = self
1252 .client
1253 .register_call_link_session(session, join.waiting_room.clone(), media, &token)
1254 .await
1255 .map_err(|_| {
1256 CallError::Response("call-link admission snapshot was rejected".to_string())
1257 })?;
1258 drop(pending_join);
1261 drop(pending_join_lane);
1262 let mut registration = CallLinkRegistrationGuard::new(
1263 self.client,
1264 registry.clone(),
1265 &join.call_id,
1266 join.call_creator.clone(),
1267 generation,
1268 );
1269
1270 if join.in_waiting_room && join.waiting_room.is_none() {
1271 return Err(CallError::Response(
1272 "call-link join omitted its waiting-room state".to_string(),
1273 ));
1274 }
1275
1276 registry.set_group_invite_self_device(
1277 &join.call_id,
1278 generation,
1279 wacore::types::group_call::GroupCallDevice::new(own_lid).with_capability(1, capability),
1280 );
1281 let rekey_required = join
1282 .group
1283 .as_ref()
1284 .is_some_and(|update| update.rekey_requested);
1285 let mut still_waiting = self
1286 .synchronize_call_link_admission(&mut join, generation, rekey_required)
1287 .await?;
1288 if still_waiting {
1289 let heartbeat = self
1290 .waiting_room_heartbeat(&join.call_id, &join.call_creator)
1291 .await;
1292 still_waiting = self
1296 .synchronize_call_link_admission(&mut join, generation, rekey_required)
1297 .await?;
1298 if still_waiting {
1299 heartbeat?;
1300 }
1301 }
1302 if still_waiting {
1303 self.start_waiting_room_heartbeat(
1304 join.call_id.clone(),
1305 join.call_creator.clone(),
1306 generation,
1307 );
1308 }
1309
1310 registration.disarm();
1311 Ok(CallLinkJoinRegistration { join, generation })
1312 }
1313
1314 #[cfg(feature = "voip-runtime")]
1315 async fn synchronize_call_link_admission(
1316 &self,
1317 join: &mut CallLinkJoin,
1318 generation: u64,
1319 rekey_required: bool,
1320 ) -> Result<bool, CallError> {
1321 let registry = self.client.call_registry();
1322 let transition_lock = registry
1323 .group_transition_lock(&join.call_id, generation)
1324 .ok_or(CallError::CallEndedDuringSetup)?;
1325 let _transition_guard = transition_lock.lock().await;
1326 let state = registry
1327 .group_state_if_current(&join.call_id, generation)
1328 .ok_or(CallError::CallEndedDuringSetup)?;
1329 if let Some(room) = state.waiting_room().cloned() {
1330 join.waiting_room_enabled = room.enabled;
1331 join.is_admin = room.is_admin;
1332 join.waiting_room = Some(room);
1333 }
1334 let phase = registry
1335 .phase_if_current(&join.call_id, generation)
1336 .ok_or(CallError::CallEndedDuringSetup)?;
1337 if phase == CallPhase::WaitingRoom {
1338 join.in_waiting_room = true;
1339 return Ok(true);
1340 }
1341
1342 let update = state.snapshot().cloned().ok_or(CallError::Media(
1343 "admitted call link has no authoritative group snapshot",
1344 ))?;
1345 join.in_waiting_room = false;
1346 join.group = Some(update.clone());
1347 let retained_epoch =
1348 registry.pending_group_epoch_transaction_if_current(&join.call_id, generation);
1349 if (rekey_required || update.rekey_requested)
1350 && retained_epoch.is_none_or(|transaction| transaction < update.transaction_id)
1351 {
1352 crate::voip::facade::fanout_group_epoch(self.client, &update)
1355 .await?
1356 .commit(|epoch| {
1357 registry
1358 .send_group_epoch_if_current(
1359 &join.call_id,
1360 generation,
1361 update.transaction_id,
1362 epoch.to_vec(),
1363 )
1364 .then_some(())
1365 .ok_or(CallError::Media(
1366 "call-link group epoch could not be retained",
1367 ))
1368 })?;
1369 }
1370 Ok(false)
1371 }
1372
1373 #[cfg(feature = "voip-runtime")]
1375 pub async fn set_approval_required(
1376 &self,
1377 call_id: &str,
1378 call_creator: &Jid,
1379 enabled: bool,
1380 ) -> Result<(), CallError> {
1381 let generation = self
1382 .client
1383 .call_registry()
1384 .generation_of(call_id)
1385 .ok_or(CallError::Media("call is no longer active"))?;
1386 self.set_approval_required_for_generation(call_id, call_creator, generation, enabled)
1387 .await
1388 }
1389
1390 #[cfg(feature = "voip-runtime")]
1391 pub(crate) async fn set_approval_required_for_generation(
1392 &self,
1393 call_id: &str,
1394 call_creator: &Jid,
1395 generation: u64,
1396 enabled: bool,
1397 ) -> Result<(), CallError> {
1398 let registry = self.client.call_registry();
1399 let transition_lock = registry
1400 .group_transition_lock(call_id, generation)
1401 .ok_or(CallError::Media("call is no longer active"))?;
1402 let _transition_guard = transition_lock.lock().await;
1403 self.ensure_waiting_room_admin_if_current(call_id, generation)?;
1404 let request_id = self.client.generate_request_id();
1405 execute_call_service_request(
1406 self.client,
1407 &request_id,
1408 build_waiting_room_toggle(call_id, call_creator, enabled, &request_id)
1409 .map_err(|error| CallError::Response(error.to_string()))?,
1410 parse_waiting_room_toggle_ack,
1411 )
1412 .await?;
1413 if registry.set_waiting_room_enabled_if_current(call_id, generation, enabled) {
1414 Ok(())
1415 } else {
1416 Err(CallError::Media(
1417 "call was replaced while applying group control",
1418 ))
1419 }
1420 }
1421
1422 #[cfg(feature = "voip-runtime")]
1424 pub async fn waiting_room_heartbeat(
1425 &self,
1426 call_id: &str,
1427 call_creator: &Jid,
1428 ) -> Result<(), CallError> {
1429 self.send_group_control(
1430 call_id,
1431 build_waiting_room_heartbeat(call_id, call_creator, &self.client.generate_request_id())
1432 .map_err(|error| CallError::Response(error.to_string()))?,
1433 )
1434 .await
1435 }
1436
1437 #[cfg(feature = "voip-runtime")]
1439 pub async fn admit_waiting_user(
1440 &self,
1441 call_id: &str,
1442 call_creator: &Jid,
1443 user: &Jid,
1444 ) -> Result<(), CallError> {
1445 let generation = self
1446 .client
1447 .call_registry()
1448 .generation_of(call_id)
1449 .ok_or(CallError::Media("call is no longer active"))?;
1450 self.admit_waiting_user_for_generation(call_id, call_creator, generation, user)
1451 .await
1452 }
1453
1454 #[cfg(feature = "voip-runtime")]
1455 pub(crate) async fn admit_waiting_user_for_generation(
1456 &self,
1457 call_id: &str,
1458 call_creator: &Jid,
1459 generation: u64,
1460 user: &Jid,
1461 ) -> Result<(), CallError> {
1462 self.waiting_room_user_action_for_generation(
1463 call_id,
1464 call_creator,
1465 generation,
1466 user,
1467 WaitingRoomUserAction::Admit,
1468 )
1469 .await
1470 }
1471
1472 #[cfg(feature = "voip-runtime")]
1474 pub async fn deny_waiting_user(
1475 &self,
1476 call_id: &str,
1477 call_creator: &Jid,
1478 user: &Jid,
1479 ) -> Result<(), CallError> {
1480 let generation = self
1481 .client
1482 .call_registry()
1483 .generation_of(call_id)
1484 .ok_or(CallError::Media("call is no longer active"))?;
1485 self.deny_waiting_user_for_generation(call_id, call_creator, generation, user)
1486 .await
1487 }
1488
1489 #[cfg(feature = "voip-runtime")]
1490 pub(crate) async fn deny_waiting_user_for_generation(
1491 &self,
1492 call_id: &str,
1493 call_creator: &Jid,
1494 generation: u64,
1495 user: &Jid,
1496 ) -> Result<(), CallError> {
1497 self.waiting_room_user_action_for_generation(
1498 call_id,
1499 call_creator,
1500 generation,
1501 user,
1502 WaitingRoomUserAction::Deny,
1503 )
1504 .await
1505 }
1506
1507 #[cfg(feature = "voip-runtime")]
1508 async fn waiting_room_user_action_for_generation(
1509 &self,
1510 call_id: &str,
1511 call_creator: &Jid,
1512 generation: u64,
1513 user: &Jid,
1514 action: WaitingRoomUserAction,
1515 ) -> Result<(), CallError> {
1516 self.ensure_waiting_room_admin_if_current(call_id, generation)?;
1517 let request_id = self.client.generate_request_id();
1518 let (request, parse) = match action {
1519 WaitingRoomUserAction::Admit => (
1520 build_waiting_room_admit(call_id, call_creator, user, &request_id),
1521 parse_waiting_room_admit_ack
1522 as fn(&wacore_binary::NodeRef<'_>) -> anyhow::Result<()>,
1523 ),
1524 WaitingRoomUserAction::Deny => (
1525 build_waiting_room_deny(call_id, call_creator, user, &request_id),
1526 parse_waiting_room_deny_ack
1527 as fn(&wacore_binary::NodeRef<'_>) -> anyhow::Result<()>,
1528 ),
1529 };
1530 execute_call_service_request(
1531 self.client,
1532 &request_id,
1533 request.map_err(|error| CallError::Response(error.to_string()))?,
1534 parse,
1535 )
1536 .await?;
1537 if self.client.call_registry().is_current(call_id, generation) {
1538 Ok(())
1539 } else {
1540 Err(CallError::Media(
1541 "call was replaced while applying group control",
1542 ))
1543 }
1544 }
1545
1546 #[cfg(feature = "voip-runtime")]
1548 pub async fn set_hand_raised(
1549 &self,
1550 call_id: &str,
1551 call_creator: &Jid,
1552 raised: bool,
1553 ) -> Result<(), CallError> {
1554 let generation = self
1555 .client
1556 .call_registry()
1557 .generation_of(call_id)
1558 .ok_or(CallError::Media("call is no longer active"))?;
1559 self.set_hand_raised_for_generation(call_id, call_creator, generation, raised)
1560 .await
1561 }
1562
1563 #[cfg(feature = "voip-runtime")]
1564 pub(crate) async fn set_hand_raised_for_generation(
1565 &self,
1566 call_id: &str,
1567 call_creator: &Jid,
1568 generation: u64,
1569 raised: bool,
1570 ) -> Result<(), CallError> {
1571 let registry = self.client.call_registry();
1572 let transition_lock = registry
1573 .group_transition_lock(call_id, generation)
1574 .ok_or(CallError::Media("call is no longer active"))?;
1575 let _transition_guard = transition_lock.lock().await;
1576 if !registry.group_creator_matches_if_current(call_id, generation, call_creator) {
1577 return Err(CallError::Media(
1578 "call creator does not match the active group call",
1579 ));
1580 }
1581 let participant = self
1582 .client
1583 .lid()
1584 .ok_or(CallError::Media("no own LID"))?
1585 .to_non_ad();
1586 let target = Jid::new(call_id, Server::Call);
1587 self.send_group_control(
1588 call_id,
1589 build_raise_hand(
1590 call_id,
1591 &target,
1592 call_creator,
1593 &self.client.generate_request_id(),
1594 raised,
1595 )
1596 .map_err(|error| CallError::Response(error.to_string()))?,
1597 )
1598 .await?;
1599 if registry.set_raised_hand_if_current(call_id, generation, &participant, raised) {
1600 registry.send_call_event_if_current(
1601 call_id,
1602 generation,
1603 CallEvent::HandRaised {
1604 participant,
1605 raised,
1606 },
1607 );
1608 Ok(())
1609 } else {
1610 Err(CallError::Media(
1611 "call was replaced while applying group control",
1612 ))
1613 }
1614 }
1615
1616 #[cfg(feature = "voip-runtime")]
1618 pub async fn set_screen_share(
1619 &self,
1620 call_id: &str,
1621 call_creator: &Jid,
1622 state: ScreenShareState,
1623 screen_share_id: Option<u32>,
1624 ) -> Result<(), CallError> {
1625 let generation = self
1626 .client
1627 .call_registry()
1628 .generation_of(call_id)
1629 .ok_or(CallError::Media("call is no longer active"))?;
1630 self.set_screen_share_for_generation(
1631 call_id,
1632 call_creator,
1633 generation,
1634 state,
1635 screen_share_id,
1636 )
1637 .await
1638 }
1639
1640 #[cfg(feature = "voip-runtime")]
1641 pub(crate) async fn set_screen_share_for_generation(
1642 &self,
1643 call_id: &str,
1644 call_creator: &Jid,
1645 generation: u64,
1646 state: ScreenShareState,
1647 screen_share_id: Option<u32>,
1648 ) -> Result<(), CallError> {
1649 let registry = self.client.call_registry();
1650 let transition_lock = registry
1651 .group_transition_lock(call_id, generation)
1652 .ok_or(CallError::Media("call is no longer active"))?;
1653 let _transition_guard = transition_lock.lock().await;
1654 if !registry.group_creator_matches_if_current(call_id, generation, call_creator) {
1655 return Err(CallError::Media(
1656 "call creator does not match the active group call",
1657 ));
1658 }
1659 let group = registry
1660 .group_state_if_current(call_id, generation)
1661 .ok_or(CallError::Media("call is not an active group call"))?;
1662 if state == ScreenShareState::Started
1663 && (group
1664 .snapshot()
1665 .is_none_or(|snapshot| snapshot.media != "video")
1666 || !matches!(
1667 registry.video_states(call_id, generation),
1668 Some((VideoState::Enabled, _))
1669 ))
1670 {
1671 return Err(CallError::Media(
1672 "screen sharing requires an active local video plane",
1673 ));
1674 }
1675 let participant = self
1676 .client
1677 .lid()
1678 .ok_or(CallError::Media("no own LID"))?
1679 .to_non_ad();
1680 let target = Jid::new(call_id, Server::Call);
1681 self.send_group_control(
1682 call_id,
1683 build_screen_share(
1684 call_id,
1685 &target,
1686 call_creator,
1687 &self.client.generate_request_id(),
1688 state,
1689 screen_share_id,
1690 )
1691 .map_err(|error| CallError::Response(error.to_string()))?,
1692 )
1693 .await?;
1694 let screen_share = ScreenShare::new(state, screen_share_id);
1695 if registry.set_screen_share_if_current(
1696 call_id,
1697 generation,
1698 &participant,
1699 screen_share.clone(),
1700 ) {
1701 registry.send_call_event_if_current(
1702 call_id,
1703 generation,
1704 CallEvent::ScreenShareChanged {
1705 participant,
1706 screen_share,
1707 },
1708 );
1709 } else {
1710 return Err(CallError::Media(
1711 "call was replaced while applying group control",
1712 ));
1713 }
1714 registry.send_video_ctl(call_id, generation, VideoControl::RequireKeyframe);
1717 Ok(())
1718 }
1719
1720 #[cfg(feature = "voip-runtime")]
1721 async fn send_group_control(&self, call_id: &str, node: Node) -> Result<(), CallError> {
1722 if call_id.is_empty() {
1723 return Err(CallError::EmptyCallId);
1724 }
1725 self.client.send_node(node).await?;
1726 Ok(())
1727 }
1728
1729 #[cfg(feature = "voip-runtime")]
1730 fn ensure_waiting_room_admin_if_current(
1731 &self,
1732 call_id: &str,
1733 generation: u64,
1734 ) -> Result<(), CallError> {
1735 let room = self
1736 .client
1737 .call_registry()
1738 .group_state_if_current(call_id, generation)
1739 .and_then(|state| state.waiting_room().cloned())
1740 .ok_or(CallError::Media("call has no waiting-room state"))?;
1741 if !room.is_admin {
1742 return Err(CallError::Media(
1743 "waiting-room control requires an administrator",
1744 ));
1745 }
1746 Ok(())
1747 }
1748
1749 #[cfg(feature = "voip-runtime")]
1750 fn start_waiting_room_heartbeat(&self, call_id: String, call_creator: Jid, generation: u64) {
1751 let weak_client = self.client.self_weak.get().cloned().unwrap_or_default();
1752 let runtime = self.client.runtime.clone();
1753 let sleeper = runtime.clone();
1754 let heartbeat_call_id = call_id.clone();
1755 let task = runtime.spawn(Box::pin(async move {
1756 let mut consecutive_failures = 0;
1757 loop {
1758 sleeper.sleep(WAITING_ROOM_HEARTBEAT_INTERVAL).await;
1759 let Some(client) = weak_client.upgrade() else {
1760 break;
1761 };
1762 if client
1763 .call_registry()
1764 .phase_if_current(&heartbeat_call_id, generation)
1765 != Some(CallPhase::WaitingRoom)
1766 {
1767 break;
1768 }
1769 let request_id = client.generate_request_id();
1770 let heartbeat = match build_waiting_room_heartbeat(
1771 &heartbeat_call_id,
1772 &call_creator,
1773 &request_id,
1774 ) {
1775 Ok(heartbeat) => heartbeat,
1776 Err(error) => {
1777 warn!(
1778 "voip: invalid waiting-room heartbeat for call {}: {error}",
1779 heartbeat_call_id
1780 );
1781 break;
1782 }
1783 };
1784 if let Err(error) = client
1785 .send_node(heartbeat)
1786 .await
1787 {
1788 consecutive_failures += 1;
1789 warn!(
1790 "voip: waiting-room heartbeat failed for call {} ({consecutive_failures}/{WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES}): {error}",
1791 heartbeat_call_id,
1792 );
1793 if consecutive_failures >= WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES {
1794 client.call_registry().send_call_event_if_current(
1795 &heartbeat_call_id,
1796 generation,
1797 CallEvent::WaitingRoomHeartbeatFailed,
1798 );
1799 client
1800 .call_registry()
1801 .remove_if_current(&heartbeat_call_id, generation);
1802 break;
1803 }
1804 continue;
1805 }
1806 consecutive_failures = 0;
1807 }
1808 }));
1809 self.client
1810 .call_registry()
1811 .set_waiting_room_task(&call_id, generation, task);
1812 }
1813
1814 pub async fn terminate(
1816 &self,
1817 call_id: &str,
1818 peer: &Jid,
1819 call_creator: &Jid,
1820 ) -> Result<(), CallError> {
1821 if call_id.is_empty() {
1822 return Err(CallError::EmptyCallId);
1823 }
1824 let id = self.client.generate_request_id();
1825 let stanza = build_terminate(&TerminateParams {
1826 call_id,
1827 to: peer,
1828 id: Some(&id),
1829 call_creator,
1830 reason: None,
1831 });
1832 let sent = self.client.send_node(stanza).await;
1833 #[cfg(feature = "voip-runtime")]
1838 crate::voip::facade::terminate_call(self.client, call_id);
1839 sent?;
1840 Ok(())
1841 }
1842}
1843
1844#[cfg(feature = "voip-runtime")]
1845fn normalize_call_link_token(
1846 token_or_url: &str,
1847 expected_media: CallLinkMedia,
1848) -> Result<String, CallError> {
1849 let value = token_or_url.trim();
1850 if value.is_empty() {
1851 return Err(CallError::Response(
1852 "call-link token is required".to_string(),
1853 ));
1854 }
1855 const PREFIX: &str = "https://call.whatsapp.com/";
1856 if let Some(path) = value.strip_prefix(PREFIX) {
1857 let path = path.split_once(['?', '#']).map_or(path, |(path, _)| path);
1858 let mut parts = path.split('/');
1859 let media = parts.next();
1860 let Some(token) = parts.next().filter(|token| !token.is_empty()) else {
1861 return Err(CallError::Response(
1862 "invalid call-link URL or media mode".to_string(),
1863 ));
1864 };
1865 if parts.next().is_some() || media != Some(expected_media.as_str()) {
1866 return Err(CallError::Response(
1867 "invalid call-link URL or media mode".to_string(),
1868 ));
1869 }
1870 return Ok(token.to_string());
1871 }
1872 if value.contains("://") || value.contains('/') {
1873 return Err(CallError::Response("invalid call-link token".to_string()));
1874 }
1875 Ok(value.to_string())
1876}
1877
1878#[cfg(feature = "voip-runtime")]
1879#[inline(never)]
1880async fn execute_call_link_join_request(
1881 client: &Client,
1882 token: &str,
1883 media: CallLinkMedia,
1884 capability: &[u8],
1885) -> Result<CallLinkJoin, CallError> {
1886 let request_id = client.generate_request_id();
1887 let request = build_call_link_join_with_capability(token, media, &request_id, capability)
1888 .map_err(|error| CallError::Response(error.to_string()))?;
1889 execute_call_service_request(client, &request_id, request, |response| {
1890 parse_call_link_join_ack(response, token)
1891 })
1892 .await
1893}
1894
1895#[cfg(feature = "voip-runtime")]
1896async fn execute_call_service_request<T>(
1897 client: &Client,
1898 request_id: &str,
1899 request: Node,
1900 parse: impl FnOnce(&wacore_binary::NodeRef<'_>) -> anyhow::Result<T>,
1901) -> Result<T, CallError> {
1902 let (tx, response) = futures::channel::oneshot::channel();
1903 let cleanup_generation = client
1904 .response_waiters_guard()
1905 .try_insert_guarded(request_id.to_string(), ResponseWaiter::Iq(tx))
1906 .ok_or_else(|| CallError::Response("duplicate call-service request id".to_string()))?;
1907 let _waiter_guard = crate::request::ResponseWaiterGuard::new(
1908 client.response_waiters.clone(),
1909 request_id.to_string(),
1910 cleanup_generation,
1911 );
1912 client.send_node(request).await?;
1913 let response =
1914 match wacore::runtime::timeout(&*client.runtime, CALL_SERVICE_REQUEST_TIMEOUT, response)
1915 .await
1916 {
1917 Ok(Ok(response)) => response,
1918 Ok(Err(_)) => return Err(CallError::Response("response channel closed".to_string())),
1919 Err(_) => return Err(CallError::ResponseTimeout),
1920 };
1921 parse(response.get()).map_err(|error| CallError::Response(error.to_string()))
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926 #[cfg(feature = "voip-runtime")]
1927 use super::PendingCallLinkBuffer;
1928 use std::sync::Arc;
1929 use std::sync::atomic::{AtomicUsize, Ordering};
1930 #[cfg(feature = "voip-runtime")]
1931 use std::time::Duration;
1932
1933 use async_trait::async_trait;
1934 use bytes::Bytes;
1935 use wacore::handshake::NoiseCipher;
1936 use wacore::types::call::{CallAction, IncomingCall};
1937 #[cfg(feature = "voip-runtime")]
1938 use wacore::types::group_call::{
1939 CallLinkMedia, GroupCallDevice, GroupCallParticipant, GroupCallRelay,
1940 GroupCallRelayEndpoint, GroupCallUpdate, ScreenShareState, WaitingRoom,
1941 };
1942 #[cfg(feature = "voip-runtime")]
1943 use wacore::voip::{
1944 AudioFormat, CallEvent, CallPhase, CallSession, VideoControl, video_control_channel,
1945 };
1946 #[cfg(feature = "voip-runtime")]
1947 use wacore_binary::builder::NodeBuilder;
1948 use wacore_binary::{Jid, Server};
1949
1950 #[cfg(feature = "voip-runtime")]
1951 use super::{
1952 MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS, MAX_PENDING_CALL_LINK_TRANSITION_BYTES,
1953 MAX_PENDING_CALL_LINK_TRANSITIONS, WaitingRoomUserAction,
1954 };
1955 use crate::client::Client;
1956 #[cfg(feature = "voip-runtime")]
1957 use crate::client::{CallError, ResponseWaiter};
1958
1959 #[cfg(feature = "voip-runtime")]
1960 #[test]
1961 fn call_link_urls_strip_query_and_fragment_without_relaxing_validation() {
1962 assert_eq!(
1963 super::normalize_call_link_token(
1964 "https://call.whatsapp.com/video/TEST-TOKEN?utm_source=test#join",
1965 CallLinkMedia::Video,
1966 )
1967 .unwrap(),
1968 "TEST-TOKEN"
1969 );
1970 assert!(
1971 super::normalize_call_link_token(
1972 "https://call.whatsapp.com/audio/TEST-TOKEN?x=1",
1973 CallLinkMedia::Video,
1974 )
1975 .is_err()
1976 );
1977 assert!(
1978 super::normalize_call_link_token(
1979 "https://call.whatsapp.com/video/?x=1",
1980 CallLinkMedia::Video,
1981 )
1982 .is_err()
1983 );
1984 }
1985
1986 struct CountingTransport {
1987 count: Arc<AtomicUsize>,
1988 }
1989
1990 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1991 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1992 impl crate::transport::Transport for CountingTransport {
1993 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
1994 self.count.fetch_add(1, Ordering::SeqCst);
1995 Ok(())
1996 }
1997 async fn disconnect(&self) {}
1998 }
1999
2000 async fn make_client_with_count() -> (Arc<Client>, Arc<AtomicUsize>) {
2001 let client = crate::test_utils::create_test_client().await;
2002
2003 let count = Arc::new(AtomicUsize::new(0));
2004 let socket_transport: Arc<dyn crate::transport::Transport> = Arc::new(CountingTransport {
2005 count: count.clone(),
2006 });
2007 let key = [0u8; 32];
2008 let noise_socket = crate::socket::NoiseSocket::new(
2009 Arc::new(crate::runtime_impl::TokioRuntime),
2010 socket_transport,
2011 NoiseCipher::new(&key).expect("valid key"),
2012 NoiseCipher::new(&key).expect("valid key"),
2013 );
2014 *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
2015 (client, count)
2016 }
2017
2018 #[cfg(feature = "voip-runtime")]
2019 struct FailingTransport;
2020
2021 #[cfg(feature = "voip-runtime")]
2022 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
2023 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
2024 impl crate::transport::Transport for FailingTransport {
2025 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
2026 Err(anyhow::anyhow!("transport down"))
2027 }
2028 async fn disconnect(&self) {}
2029 }
2030
2031 #[cfg(feature = "voip-runtime")]
2032 async fn make_client_failing() -> Arc<Client> {
2033 let client = crate::test_utils::create_test_client().await;
2034 let socket_transport: Arc<dyn crate::transport::Transport> = Arc::new(FailingTransport);
2035 let key = [0u8; 32];
2036 let noise_socket = crate::socket::NoiseSocket::new(
2037 Arc::new(crate::runtime_impl::TokioRuntime),
2038 socket_transport,
2039 NoiseCipher::new(&key).expect("valid key"),
2040 NoiseCipher::new(&key).expect("valid key"),
2041 );
2042 *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
2043 client
2044 }
2045
2046 fn caller() -> Jid {
2047 Jid::new("111111111111111", Server::Lid)
2048 }
2049
2050 fn call_creator() -> Jid {
2051 Jid::new("222222222222222", Server::Lid)
2052 }
2053
2054 fn incoming_reject() -> IncomingCall {
2055 IncomingCall::new_for_test(
2056 caller(),
2057 "STANZA-ID-0001".into(),
2058 wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
2059 CallAction::Offer {
2060 call_id: "CALL-ID-0001".into(),
2061 call_creator: caller(),
2062 caller_pn: None,
2063 caller_country_code: None,
2064 device_class: None,
2065 joinable: false,
2066 is_video: false,
2067 audio: Vec::new(),
2068 group_jid: None,
2069 },
2070 )
2071 }
2072
2073 #[tokio::test]
2074 async fn reject_sends_stanza() {
2075 let (client, count) = make_client_with_count().await;
2076 client
2077 .voip()
2078 .reject(&incoming_reject())
2079 .await
2080 .expect("reject should send");
2081 assert_eq!(count.load(Ordering::SeqCst), 1);
2082 }
2083
2084 #[tokio::test]
2085 async fn reject_call_sends_stanza_without_event_context() {
2086 let (client, count) = make_client_with_count().await;
2087 let waiter = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2088 let peer = caller();
2089 let creator = call_creator();
2090 client
2091 .voip()
2092 .reject_call("CALL-ID-0001", &peer, &creator)
2093 .await
2094 .expect("reject should send");
2095 assert_eq!(count.load(Ordering::SeqCst), 1);
2096
2097 let sent = waiter.await.expect("reject stanza should be observable");
2098 let call = sent.as_node_ref();
2099 assert_eq!(
2100 call.attrs().optional_string("to").as_deref(),
2101 Some(peer.to_string().as_str())
2102 );
2103 let reject = &call.children().expect("call action")[0];
2104 assert_eq!(reject.tag, "reject");
2105 assert_eq!(
2106 reject.attrs().optional_string("call-id").as_deref(),
2107 Some("CALL-ID-0001")
2108 );
2109 assert_eq!(
2110 reject.attrs().optional_string("call-creator").as_deref(),
2111 Some(creator.to_string().as_str())
2112 );
2113 assert_eq!(
2114 reject.attrs().optional_string("count").as_deref(),
2115 Some("0")
2116 );
2117 }
2118
2119 #[cfg(feature = "voip-runtime")]
2120 #[tokio::test]
2121 async fn rejecting_an_incoming_group_offer_removes_its_ringing_generation() {
2122 let (client, _count) = make_client_with_count().await;
2123 let creator = caller();
2124 let call_id = "INCOMING-GROUP-CALL";
2125 let mut session = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2126 session.group = Some(
2127 GroupCallUpdate::builder()
2128 .call_id(call_id.to_string())
2129 .call_creator(creator.clone())
2130 .transaction_id(1)
2131 .media("audio".to_string())
2132 .connected_limit(32)
2133 .joinable(true)
2134 .av_upgradable(true)
2135 .rekey_requested(false)
2136 .participants(Vec::new())
2137 .build(),
2138 );
2139 let generation = client
2140 .call_registry()
2141 .insert_ringing_group_if_inactive(session)
2142 .expect("valid group snapshot")
2143 .expect("ringing generation");
2144
2145 client
2146 .voip()
2147 .reject_call(call_id, &creator, &creator)
2148 .await
2149 .expect("reject");
2150
2151 assert_ne!(
2152 client.call_registry().generation_of(call_id),
2153 Some(generation),
2154 "reject must reap the exact eagerly registered group offer"
2155 );
2156 }
2157
2158 #[cfg(feature = "voip-runtime")]
2159 #[tokio::test]
2160 async fn rejecting_a_stale_group_offer_event_preserves_the_replacement_generation() {
2161 let (client, count) = make_client_with_count().await;
2162 let creator = caller();
2163 let call_id = "REPLACED-INCOMING-GROUP-CALL";
2164 let update = GroupCallUpdate::builder()
2165 .call_id(call_id.to_string())
2166 .call_creator(creator.clone())
2167 .transaction_id(1)
2168 .media("audio".to_string())
2169 .connected_limit(32)
2170 .joinable(true)
2171 .av_upgradable(true)
2172 .rekey_requested(false)
2173 .participants(Vec::new())
2174 .build();
2175 let mut session = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2176 session.group = Some(update.clone());
2177 let stale = client
2178 .call_registry()
2179 .insert_ringing_group_if_inactive(session)
2180 .expect("valid group snapshot")
2181 .expect("ringing generation");
2182 let mut incoming = IncomingCall::new_for_test(
2183 creator.clone(),
2184 "STALE-GROUP-OFFER".to_string(),
2185 wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
2186 CallAction::Offer {
2187 call_id: call_id.to_string(),
2188 call_creator: creator.clone(),
2189 caller_pn: None,
2190 caller_country_code: None,
2191 device_class: None,
2192 joinable: true,
2193 is_video: false,
2194 audio: Vec::new(),
2195 group_jid: None,
2196 },
2197 );
2198 incoming.group = Some(Box::new(update.clone()));
2199 incoming.set_ringing_generation(stale);
2200
2201 let mut replacement = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2202 replacement.group = Some(update);
2203 let replacement = client.call_registry().insert_ringing_group(replacement);
2204
2205 assert!(matches!(
2206 client.voip().reject(&incoming).await,
2207 Err(CallError::CallEndedDuringSetup)
2208 ));
2209 assert_eq!(
2210 count.load(Ordering::SeqCst),
2211 0,
2212 "a stale application event must not reject the replacement on the wire"
2213 );
2214
2215 assert_eq!(
2216 client.call_registry().generation_of(call_id),
2217 Some(replacement),
2218 "a retained application event must not reap a newer same-id generation"
2219 );
2220 assert_eq!(
2221 client
2222 .call_registry()
2223 .ringing_group_generation(call_id, &creator),
2224 Some(replacement),
2225 "the newer offer must remain available for the application to answer or reject"
2226 );
2227 client
2228 .call_registry()
2229 .remove_if_current(call_id, replacement);
2230 client.call_registry().take_ringing(call_id);
2231 }
2232
2233 #[tokio::test]
2234 async fn terminate_sends_stanza() {
2235 let (client, count) = make_client_with_count().await;
2236 client
2237 .voip()
2238 .terminate("CALL-ID-0001", &caller(), &caller())
2239 .await
2240 .expect("terminate should send");
2241 assert_eq!(count.load(Ordering::SeqCst), 1);
2242 }
2243
2244 #[cfg(feature = "voip-runtime")]
2245 #[tokio::test]
2246 async fn terminate_aborts_the_local_call() {
2247 use wacore::voip::CallSession;
2248 let (client, _count) = make_client_with_count().await;
2249 let reg = client.call_registry();
2250 reg.insert(CallSession::new_outgoing(
2251 "CALL-ID-0001",
2252 caller(),
2253 caller(),
2254 ));
2255 assert_eq!(reg.active_count(), 1);
2256 client
2257 .voip()
2258 .terminate("CALL-ID-0001", &caller(), &caller())
2259 .await
2260 .expect("terminate should send");
2261 assert_eq!(
2262 reg.active_count(),
2263 0,
2264 "terminate must tear the local call down, not just signal the peer"
2265 );
2266 }
2267
2268 #[cfg(feature = "voip-runtime")]
2269 #[tokio::test]
2270 async fn terminate_tears_down_local_even_when_send_fails() {
2271 use wacore::voip::CallSession;
2272 let client = make_client_failing().await;
2273 let reg = client.call_registry();
2274 reg.insert(CallSession::new_outgoing(
2275 "CALL-ID-0001",
2276 caller(),
2277 caller(),
2278 ));
2279 assert_eq!(reg.active_count(), 1);
2280 let res = client
2281 .voip()
2282 .terminate("CALL-ID-0001", &caller(), &caller())
2283 .await;
2284 assert!(
2285 res.is_err(),
2286 "a failed signaling send must surface the error"
2287 );
2288 assert_eq!(
2289 reg.active_count(),
2290 0,
2291 "a failed signaling send must still tear the local media task down"
2292 );
2293 }
2294
2295 #[tokio::test]
2296 async fn reject_empty_call_id_errors() {
2297 let (client, _count) = make_client_with_count().await;
2298 let mut call = incoming_reject();
2299 call.action = CallAction::Reject {
2300 call_id: String::new(),
2301 call_creator: caller(),
2302 reason: None,
2303 };
2304 assert!(client.voip().reject(&call).await.is_err());
2305 }
2306
2307 #[cfg(feature = "voip-runtime")]
2308 #[tokio::test]
2309 async fn local_group_controls_commit_state_events_and_screen_keyframe_gate() {
2310 let (client, transport) = crate::test_utils::create_iq_test_client().await;
2311 let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2312 client
2313 .persistence_manager()
2314 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2315 own_device,
2316 )))
2317 .await;
2318 let participant = Jid::new("111111111111111", Server::Lid);
2319 let creator = participant.clone();
2320 let call_id = "TEST-GROUP-CONTROLS";
2321 let registry = client.call_registry();
2322 let mut session =
2323 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
2324 session.group = Some(
2325 GroupCallUpdate::builder()
2326 .call_id(call_id.to_string())
2327 .call_creator(creator.clone())
2328 .transaction_id(1)
2329 .media("video".to_string())
2330 .connected_limit(32)
2331 .joinable(true)
2332 .av_upgradable(true)
2333 .rekey_requested(false)
2334 .participants(vec![GroupCallParticipant::new(
2335 participant.clone(),
2336 vec![GroupCallDevice::new(participant.clone().with_device(1))],
2337 )])
2338 .build(),
2339 );
2340 let generation = registry.insert(session);
2341 let (event_tx, event_rx) = async_channel::bounded(4);
2342 let (video_tx, video_rx) = video_control_channel();
2343 registry.set_video_channels(call_id, generation, event_tx, video_tx, Box::new(|| {}));
2344
2345 client
2346 .voip()
2347 .set_hand_raised(call_id, &creator, true)
2348 .await
2349 .expect("raise hand");
2350 assert!(
2351 registry
2352 .group_state(call_id)
2353 .expect("group state")
2354 .raised_hands()
2355 .contains(&participant)
2356 );
2357 assert!(matches!(
2358 event_rx.try_recv(),
2359 Ok(CallEvent::HandRaised {
2360 participant: event_participant,
2361 raised: true,
2362 }) if event_participant == participant
2363 ));
2364
2365 assert!(
2366 client
2367 .voip()
2368 .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2369 .await
2370 .is_err(),
2371 "a call without a local video plane must not advertise an unsendable screen share"
2372 );
2373 assert_eq!(
2374 transport.sent_count(),
2375 1,
2376 "the rejected screen-share transition must stay off the wire"
2377 );
2378 assert!(registry.set_is_video(call_id, generation, true));
2379
2380 client
2381 .voip()
2382 .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2383 .await
2384 .expect("start screen share");
2385 let share = registry
2386 .group_state(call_id)
2387 .expect("group state")
2388 .screen_shares()
2389 .get(&participant)
2390 .cloned()
2391 .expect("local screen share");
2392 assert_eq!(share.state, ScreenShareState::Started);
2393 assert_eq!(share.version, 2);
2394 assert_eq!(share.screen_share_id, Some(7));
2395 assert!(matches!(
2396 event_rx.try_recv(),
2397 Ok(CallEvent::ScreenShareChanged {
2398 participant: event_participant,
2399 screen_share,
2400 }) if event_participant == participant && screen_share == share
2401 ));
2402 assert_eq!(
2403 video_rx.try_recv(),
2404 Ok(VideoControl::RequireKeyframe),
2405 "starting a replacement screen source must re-arm the H.264 recovery gate"
2406 );
2407
2408 client
2409 .voip()
2410 .set_screen_share(call_id, &creator, ScreenShareState::Stopped, None)
2411 .await
2412 .expect("stop screen share");
2413 assert!(
2414 registry
2415 .group_state(call_id)
2416 .expect("group state")
2417 .screen_shares()
2418 .is_empty()
2419 );
2420 assert!(matches!(
2421 event_rx.try_recv(),
2422 Ok(CallEvent::ScreenShareChanged {
2423 participant: event_participant,
2424 screen_share,
2425 }) if event_participant == participant
2426 && screen_share.state == ScreenShareState::Stopped
2427 ));
2428 assert_eq!(
2429 video_rx.try_recv(),
2430 Ok(VideoControl::RequireKeyframe),
2431 "returning to the camera must re-arm the H.264 recovery gate"
2432 );
2433 assert_eq!(transport.sent_count(), 3);
2434
2435 let mut audio_only = registry
2436 .group_state_if_current(call_id, generation)
2437 .and_then(|state| state.snapshot().cloned())
2438 .expect("authoritative roster");
2439 audio_only.transaction_id = 2;
2440 audio_only.media = "audio".to_string();
2441 assert_eq!(
2442 registry.apply_group_update_if_current(audio_only, generation),
2443 wacore::voip::GroupStateApply::Applied
2444 );
2445 assert!(
2446 client
2447 .voip()
2448 .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(8))
2449 .await
2450 .is_err(),
2451 "an authoritative audio downgrade must disable screen sharing even if local video was negotiated"
2452 );
2453 assert_eq!(
2454 transport.sent_count(),
2455 3,
2456 "the rejected post-downgrade transition must stay off the wire"
2457 );
2458
2459 let replacement_creator = Jid::new("222222222222222", Server::Lid);
2460 let mut replacement = CallSession::new_outgoing(
2461 call_id,
2462 Jid::new(call_id, Server::Call),
2463 replacement_creator.clone(),
2464 );
2465 replacement.group = Some(
2466 GroupCallUpdate::builder()
2467 .call_id(call_id.to_string())
2468 .call_creator(replacement_creator)
2469 .transaction_id(1)
2470 .media("video".to_string())
2471 .connected_limit(32)
2472 .joinable(true)
2473 .av_upgradable(true)
2474 .rekey_requested(false)
2475 .participants(vec![GroupCallParticipant::new(
2476 participant,
2477 vec![GroupCallDevice::new(
2478 Jid::new("111111111111111", Server::Lid).with_device(1),
2479 )],
2480 )])
2481 .build(),
2482 );
2483 let replacement_generation = registry.insert(replacement);
2484 assert!(
2485 client
2486 .voip()
2487 .set_hand_raised(call_id, &creator, true)
2488 .await
2489 .is_err(),
2490 "stale creator metadata cannot mutate a replacement generation"
2491 );
2492 assert_eq!(
2493 transport.sent_count(),
2494 3,
2495 "a stale group identity must be rejected before signaling"
2496 );
2497 registry.remove_if_current(call_id, replacement_generation);
2498 }
2499
2500 #[cfg(feature = "voip-runtime")]
2501 #[tokio::test]
2502 async fn local_group_controls_wait_for_the_authoritative_transition_lane() {
2503 let (client, transport) = crate::test_utils::create_iq_test_client().await;
2504 let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2505 client
2506 .persistence_manager()
2507 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2508 own_device,
2509 )))
2510 .await;
2511 let participant = Jid::new("111111111111111", Server::Lid);
2512 let creator = participant.clone();
2513 let call_id = "TEST-GROUP-CONTROL-SERIALIZATION";
2514 let registry = client.call_registry();
2515 let mut session =
2516 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
2517 session.group = Some(
2518 GroupCallUpdate::builder()
2519 .call_id(call_id.to_string())
2520 .call_creator(creator.clone())
2521 .transaction_id(1)
2522 .media("video".to_string())
2523 .connected_limit(32)
2524 .joinable(true)
2525 .av_upgradable(true)
2526 .rekey_requested(false)
2527 .participants(vec![GroupCallParticipant::new(
2528 participant,
2529 vec![GroupCallDevice::new(
2530 Jid::new("111111111111111", Server::Lid).with_device(1),
2531 )],
2532 )])
2533 .build(),
2534 );
2535 let generation = registry.insert(session);
2536 assert!(registry.set_is_video(call_id, generation, true));
2537 let transition_lock = registry
2538 .group_transition_lock(call_id, generation)
2539 .expect("group transition lane");
2540
2541 let guard = transition_lock.lock().await;
2542 let hand_client = client.clone();
2543 let hand_creator = creator.clone();
2544 let hand = tokio::spawn(async move {
2545 hand_client
2546 .voip()
2547 .set_hand_raised_for_generation(call_id, &hand_creator, generation, true)
2548 .await
2549 });
2550 tokio::task::yield_now().await;
2551 assert_eq!(
2552 transport.sent_count(),
2553 0,
2554 "raise-hand signaling must wait for an authoritative transition"
2555 );
2556 drop(guard);
2557 hand.await
2558 .expect("raise-hand task")
2559 .expect("raise-hand transition");
2560
2561 let guard = transition_lock.lock().await;
2562 let screen_client = client.clone();
2563 let screen = tokio::spawn(async move {
2564 screen_client
2565 .voip()
2566 .set_screen_share_for_generation(
2567 call_id,
2568 &creator,
2569 generation,
2570 ScreenShareState::Started,
2571 Some(7),
2572 )
2573 .await
2574 });
2575 tokio::task::yield_now().await;
2576 assert_eq!(
2577 transport.sent_count(),
2578 1,
2579 "screen-share signaling must wait for an authoritative transition"
2580 );
2581 drop(guard);
2582 screen
2583 .await
2584 .expect("screen-share task")
2585 .expect("screen-share transition");
2586 assert_eq!(transport.sent_count(), 2);
2587 registry.remove_if_current(call_id, generation);
2588 }
2589
2590 #[cfg(feature = "voip-runtime")]
2591 #[tokio::test]
2592 async fn direct_calls_reject_group_controls_before_sending() {
2593 let (client, transport) = crate::test_utils::create_iq_test_client().await;
2594 let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2595 client
2596 .persistence_manager()
2597 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2598 own_device,
2599 )))
2600 .await;
2601 let call_id = "TEST-DIRECT-CONTROLS";
2602 let creator = Jid::new("111111111111111", Server::Lid);
2603 let generation = client.call_registry().insert(CallSession::new_outgoing(
2604 call_id,
2605 Jid::new("222222222222222", Server::Lid),
2606 creator.clone(),
2607 ));
2608
2609 assert!(
2610 client
2611 .voip()
2612 .set_hand_raised(call_id, &creator, true)
2613 .await
2614 .is_err()
2615 );
2616 assert!(
2617 client
2618 .voip()
2619 .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2620 .await
2621 .is_err()
2622 );
2623 assert_eq!(
2624 transport.sent_count(),
2625 0,
2626 "group-only controls must not be emitted for a direct call"
2627 );
2628 assert!(client.call_registry().group_state(call_id).is_none());
2629 client
2630 .call_registry()
2631 .remove_if_current(call_id, generation);
2632 }
2633
2634 #[cfg(feature = "voip-runtime")]
2635 #[tokio::test(start_paused = true)]
2636 async fn call_link_requests_round_trip_through_bounded_response_waiters() {
2637 async fn wait_for_frames(
2638 transport: &crate::transport::mock::CapturingMockTransport,
2639 expected: usize,
2640 ) {
2641 for _ in 0..10_000 {
2642 if transport.sent_count() >= expected {
2643 return;
2644 }
2645 tokio::task::yield_now().await;
2646 }
2647 panic!("timed out waiting for {expected} captured call frames");
2648 }
2649
2650 let (client, transport) = crate::test_utils::create_iq_test_client().await;
2651 let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
2652 client
2653 .persistence_manager()
2654 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2655 own_lid.clone(),
2656 )))
2657 .await;
2658 let creator = Jid::new("333333333333333", Server::Lid);
2659
2660 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2661 let create_client = client.clone();
2662 let create = tokio::spawn(async move {
2663 create_client
2664 .voip()
2665 .create_call_link(CallLinkMedia::Video)
2666 .await
2667 });
2668 let request = sent.await.expect("link_create request");
2669 let request_id = request
2670 .as_node_ref()
2671 .attrs()
2672 .optional_string("id")
2673 .expect("request id")
2674 .into_owned();
2675 crate::test_utils::answer_iq(
2676 &client,
2677 &request_id,
2678 &NodeBuilder::new("ack")
2679 .attr("class", "call")
2680 .attr("type", "link_create")
2681 .attr("id", request_id.as_str())
2682 .children([NodeBuilder::new("link_create")
2683 .attr("token", "TEST-CALL-LINK")
2684 .attr("media", "video")
2685 .build()])
2686 .build(),
2687 )
2688 .await;
2689 let link = create.await.expect("create task").expect("create response");
2690 assert_eq!(link.token, "TEST-CALL-LINK");
2691 assert_eq!(link.media, CallLinkMedia::Video);
2692
2693 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2694 let preview_client = client.clone();
2695 let preview = tokio::spawn(async move {
2696 preview_client
2697 .voip()
2698 .preview_call_link("TEST-CALL-LINK", CallLinkMedia::Video)
2699 .await
2700 });
2701 let request = sent.await.expect("link_query request");
2702 let request_id = request
2703 .as_node_ref()
2704 .attrs()
2705 .optional_string("id")
2706 .expect("request id")
2707 .into_owned();
2708 crate::test_utils::answer_iq(
2709 &client,
2710 &request_id,
2711 &NodeBuilder::new("ack")
2712 .attr("class", "call")
2713 .attr("type", "link_query")
2714 .attr("id", request_id.as_str())
2715 .children([NodeBuilder::new("link_query")
2716 .attr("token", "TEST-CALL-LINK")
2717 .attr("media", "video")
2718 .attr("link_creator", creator.clone())
2719 .children([NodeBuilder::new("waiting_room")
2720 .attr("enabled", "1")
2721 .attr("is_admin", "0")
2722 .build()])
2723 .build()])
2724 .build(),
2725 )
2726 .await;
2727 let preview = preview
2728 .await
2729 .expect("preview task")
2730 .expect("preview response");
2731 assert_eq!(preview.creator, creator);
2732 assert!(preview.waiting_room_enabled);
2733 assert!(!preview.is_admin);
2734
2735 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2736 let join_client = client.clone();
2737 let join = tokio::spawn(async move {
2738 join_client
2739 .voip()
2740 .join_call_link_with_audio(
2741 "TEST-CALL-LINK",
2742 CallLinkMedia::Video,
2743 AudioFormat::OPUS_16KHZ_60MS,
2744 )
2745 .await
2746 });
2747 let request = sent.await.expect("link_join request");
2748 let request_ref = request.as_node_ref();
2749 let action = &request_ref.children().expect("join action children")[0];
2750 assert_eq!(
2751 action
2752 .get_optional_child("capability")
2753 .expect("join capability")
2754 .content_bytes(),
2755 Some(wacore::stanza::call::CAPABILITY_STANDARD_OPUS_VIDEO_OFFER.as_slice())
2756 );
2757 let request_id = request
2758 .as_node_ref()
2759 .attrs()
2760 .optional_string("id")
2761 .expect("request id")
2762 .into_owned();
2763 crate::test_utils::answer_iq(
2764 &client,
2765 &request_id,
2766 &NodeBuilder::new("ack")
2767 .attr("class", "call")
2768 .attr("type", "link_join")
2769 .attr("id", request_id.as_str())
2770 .children([NodeBuilder::new("waiting_room")
2771 .attr("call-id", "TEST-CALL-ID")
2772 .attr("call-creator", creator.clone())
2773 .attr("link-token", "TEST-CALL-LINK")
2774 .attr("media", "video")
2775 .attr("enabled", "1")
2776 .attr("is_admin", "0")
2777 .attr("transaction-id", "7")
2778 .children([NodeBuilder::new("user")
2779 .attr("jid", Jid::new("444444444444444", Server::Lid))
2780 .attr("state", "pending")
2781 .build()])
2782 .build()])
2783 .build(),
2784 )
2785 .await;
2786 let join = join.await.expect("join task").expect("join response");
2787 assert!(join.in_waiting_room);
2788 assert!(join.waiting_room_enabled);
2789 assert_eq!(join.call_id, "TEST-CALL-ID");
2790 assert!(join.group.is_none());
2791 assert_eq!(
2792 client.call_registry().phase("TEST-CALL-ID"),
2793 Some(CallPhase::WaitingRoom)
2794 );
2795 let room = client
2796 .call_registry()
2797 .group_state("TEST-CALL-ID")
2798 .and_then(|state| state.waiting_room().cloned())
2799 .expect("waiting-room state retained");
2800 assert_eq!(room.transaction_id, Some(7));
2801 assert_eq!(room.users.len(), 1);
2802
2803 wait_for_frames(&transport, 4).await;
2804 let immediate = crate::test_utils::decode_sent_iq(&transport, 3).await;
2805 let heartbeat = &immediate.get().children().expect("heartbeat action")[0];
2806 assert_eq!(heartbeat.tag, "heartbeat");
2807 assert_eq!(
2808 heartbeat.attrs().optional_string("type").as_deref(),
2809 Some("waiting_room")
2810 );
2811
2812 tokio::time::advance(Duration::from_secs(10)).await;
2813 wait_for_frames(&transport, 5).await;
2814 let scheduled = crate::test_utils::decode_sent_iq(&transport, 4).await;
2815 assert_eq!(
2816 scheduled.get().children().expect("heartbeat action")[0].tag,
2817 "heartbeat"
2818 );
2819
2820 let admitted = NodeBuilder::new("group_update")
2821 .attr("call-id", "TEST-CALL-ID")
2822 .attr("call-creator", creator)
2823 .children([NodeBuilder::new("group_info")
2824 .attr("transaction-id", "8")
2825 .attr("connected-limit", "32")
2826 .attr("media", "video")
2827 .children([NodeBuilder::new("user")
2828 .attr("jid", own_lid.to_non_ad())
2829 .attr("state", "connected")
2830 .children([NodeBuilder::new("device").attr("jid", own_lid).build()])
2831 .build()])
2832 .build()])
2833 .build();
2834 let update = wacore::stanza::group_call::parse_group_update(&admitted.as_node_ref())
2835 .expect("admitted group snapshot");
2836 assert_eq!(
2837 client.call_registry().apply_group_update(update),
2838 wacore::voip::GroupStateApply::Applied
2839 );
2840 assert_eq!(
2841 client.call_registry().phase("TEST-CALL-ID"),
2842 Some(CallPhase::Connecting)
2843 );
2844 let heartbeat_count = transport.sent_count();
2845 tokio::time::advance(Duration::from_secs(20)).await;
2846 tokio::task::yield_now().await;
2847 assert_eq!(
2848 transport.sent_count(),
2849 heartbeat_count,
2850 "admission must cancel the repeating heartbeat"
2851 );
2852 let generation = client
2853 .call_registry()
2854 .generation_of("TEST-CALL-ID")
2855 .expect("registered call-link generation");
2856 client
2857 .call_registry()
2858 .remove_if_current("TEST-CALL-ID", generation);
2859 }
2860
2861 #[cfg(feature = "voip-runtime")]
2862 #[tokio::test]
2863 async fn call_link_preview_rejects_a_changed_token_or_media() {
2864 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2865 let creator = Jid::new("333333333333333", Server::Lid);
2866 for (response_token, response_media) in
2867 [("OTHER-CALL-LINK", "video"), ("TEST-CALL-LINK", "audio")]
2868 {
2869 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2870 let preview_client = client.clone();
2871 let preview = tokio::spawn(async move {
2872 preview_client
2873 .voip()
2874 .preview_call_link("TEST-CALL-LINK", CallLinkMedia::Video)
2875 .await
2876 });
2877 let request = sent.await.expect("link_query request");
2878 let request_id = request
2879 .as_node_ref()
2880 .attrs()
2881 .optional_string("id")
2882 .expect("request id")
2883 .into_owned();
2884 crate::test_utils::answer_iq(
2885 &client,
2886 &request_id,
2887 &NodeBuilder::new("ack")
2888 .attr("class", "call")
2889 .attr("type", "link_query")
2890 .attr("id", request_id.as_str())
2891 .children([NodeBuilder::new("link_query")
2892 .attr("token", response_token)
2893 .attr("media", response_media)
2894 .attr("link_creator", creator.clone())
2895 .build()])
2896 .build(),
2897 )
2898 .await;
2899 assert!(matches!(
2900 preview.await.expect("preview task"),
2901 Err(CallError::Response(message))
2902 if message == "call-link preview changed the requested link identity"
2903 ));
2904 }
2905 }
2906
2907 #[cfg(feature = "voip-runtime")]
2908 #[tokio::test]
2909 async fn call_link_creation_rejects_a_changed_media_mode() {
2910 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2911 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2912 let create_client = client.clone();
2913 let create = tokio::spawn(async move {
2914 create_client
2915 .voip()
2916 .create_call_link(CallLinkMedia::Video)
2917 .await
2918 });
2919 let request = sent.await.expect("link_create request");
2920 let request_id = request
2921 .as_node_ref()
2922 .attrs()
2923 .optional_string("id")
2924 .expect("request id")
2925 .into_owned();
2926 crate::test_utils::answer_iq(
2927 &client,
2928 &request_id,
2929 &NodeBuilder::new("ack")
2930 .attr("class", "call")
2931 .attr("type", "link_create")
2932 .attr("id", request_id.as_str())
2933 .children([NodeBuilder::new("link_create")
2934 .attr("token", "TEST-CALL-LINK")
2935 .attr("media", "audio")
2936 .build()])
2937 .build(),
2938 )
2939 .await;
2940 assert!(matches!(
2941 create.await.expect("create task"),
2942 Err(CallError::Response(message))
2943 if message == "call-link creation changed the requested media mode"
2944 ));
2945 }
2946
2947 #[cfg(feature = "voip-runtime")]
2948 #[tokio::test]
2949 async fn approval_ack_cannot_commit_to_a_replacement_generation() {
2950 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2951 let call_id = "TEST-APPROVAL-GENERATION";
2952 let creator = Jid::new("333333333333333", Server::Lid);
2953 let registry = client.call_registry();
2954 let first = registry
2955 .insert_call_link_checked(CallSession::new_outgoing(
2956 call_id,
2957 Jid::new(call_id, Server::Call),
2958 creator.clone(),
2959 ))
2960 .expect("valid call-link session");
2961 assert_eq!(
2962 registry.apply_waiting_room(
2963 WaitingRoom::builder()
2964 .call_id(call_id.to_string())
2965 .call_creator(creator.clone())
2966 .link_token("TEST-CALL-LINK".to_string())
2967 .media(CallLinkMedia::Audio)
2968 .enabled(false)
2969 .is_admin(true)
2970 .transaction_id(1)
2971 .users(Vec::new())
2972 .build(),
2973 ),
2974 wacore::voip::GroupStateApply::Applied
2975 );
2976
2977 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2978 let request_client = client.clone();
2979 let request_creator = creator.clone();
2980 let toggle = tokio::spawn(async move {
2981 request_client
2982 .voip()
2983 .set_approval_required(call_id, &request_creator, true)
2984 .await
2985 });
2986 let request = sent.await.expect("waiting-room toggle request");
2987 let request_id = request
2988 .as_node_ref()
2989 .attrs()
2990 .optional_string("id")
2991 .expect("request id")
2992 .into_owned();
2993
2994 let replacement = registry.insert(CallSession::new_outgoing(
2995 call_id,
2996 Jid::new(call_id, Server::Call),
2997 creator,
2998 ));
2999 assert_ne!(replacement, first);
3000 crate::test_utils::answer_iq(
3001 &client,
3002 &request_id,
3003 &NodeBuilder::new("ack")
3004 .attr("class", "call")
3005 .attr("type", "waiting_room_toggle")
3006 .attr("id", request_id.as_str())
3007 .build(),
3008 )
3009 .await;
3010
3011 assert!(matches!(
3012 toggle.await.expect("toggle task"),
3013 Err(CallError::Media(
3014 "call was replaced while applying group control"
3015 ))
3016 ));
3017 assert!(
3018 registry.group_state(call_id).is_none(),
3019 "the stale ACK must not synthesize waiting-room state on the replacement"
3020 );
3021 registry.remove_if_current(call_id, replacement);
3022 }
3023
3024 #[cfg(feature = "voip-runtime")]
3025 #[tokio::test]
3026 async fn approval_toggle_serializes_with_authoritative_waiting_room_updates() {
3027 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3028 let call_id = "TEST-APPROVAL-SERIALIZATION";
3029 let creator = Jid::new("333333333333333", Server::Lid);
3030 let registry = client.call_registry();
3031 let generation = registry
3032 .insert_call_link_checked(CallSession::new_outgoing(
3033 call_id,
3034 Jid::new(call_id, Server::Call),
3035 creator.clone(),
3036 ))
3037 .expect("valid call-link session");
3038 let room = |transaction_id, enabled| {
3039 WaitingRoom::builder()
3040 .call_id(call_id.to_string())
3041 .call_creator(creator.clone())
3042 .link_token("TEST-CALL-LINK".to_string())
3043 .media(CallLinkMedia::Audio)
3044 .enabled(enabled)
3045 .is_admin(true)
3046 .transaction_id(transaction_id)
3047 .users(Vec::new())
3048 .build()
3049 };
3050 assert_eq!(
3051 registry.apply_waiting_room(room(1, false)),
3052 wacore::voip::GroupStateApply::Applied
3053 );
3054 let transition_lock = registry
3055 .group_transition_lock(call_id, generation)
3056 .expect("active group transition");
3057
3058 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3059 let request_client = client.clone();
3060 let request_creator = creator.clone();
3061 let toggle = tokio::spawn(async move {
3062 request_client
3063 .voip()
3064 .set_approval_required_for_generation(call_id, &request_creator, generation, true)
3065 .await
3066 });
3067 let request = sent.await.expect("waiting-room toggle request");
3068 let request_id = request
3069 .as_node_ref()
3070 .attrs()
3071 .optional_string("id")
3072 .expect("request id")
3073 .into_owned();
3074
3075 let update_registry = registry.clone();
3076 let update = room(2, false);
3077 let authoritative = tokio::spawn(async move {
3078 let _guard = transition_lock.lock().await;
3079 update_registry.apply_waiting_room_if_current(update, generation)
3080 });
3081 tokio::task::yield_now().await;
3082 assert!(
3083 !authoritative.is_finished(),
3084 "the authoritative snapshot must wait for the toggle ACK and local commit"
3085 );
3086
3087 crate::test_utils::answer_iq(
3088 &client,
3089 &request_id,
3090 &NodeBuilder::new("ack")
3091 .attr("class", "call")
3092 .attr("type", "waiting_room_toggle")
3093 .attr("id", request_id.as_str())
3094 .build(),
3095 )
3096 .await;
3097 toggle.await.expect("toggle task").expect("toggle response");
3098 assert_eq!(
3099 authoritative.await.expect("authoritative update task"),
3100 wacore::voip::GroupStateApply::Applied
3101 );
3102 assert!(
3103 registry
3104 .group_state_if_current(call_id, generation)
3105 .and_then(|state| state.waiting_room().cloned())
3106 .is_some_and(|room| room.transaction_id == Some(2) && !room.enabled),
3107 "the newer authoritative snapshot must win after the serialized local toggle"
3108 );
3109 registry.remove_if_current(call_id, generation);
3110 }
3111
3112 #[cfg(feature = "voip-runtime")]
3113 #[tokio::test]
3114 async fn waiting_room_user_acks_are_bound_to_the_originating_generation() {
3115 let user = Jid::new("444444444444444", Server::Lid);
3116 for (index, action) in [WaitingRoomUserAction::Admit, WaitingRoomUserAction::Deny]
3117 .into_iter()
3118 .enumerate()
3119 {
3120 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3121 let call_id = format!("TEST-WAITING-ACTION-{index}");
3122 let creator = Jid::new("333333333333333", Server::Lid);
3123 let registry = client.call_registry();
3124 let first = registry
3125 .insert_call_link_checked(CallSession::new_outgoing(
3126 &call_id,
3127 Jid::new(&call_id, Server::Call),
3128 creator.clone(),
3129 ))
3130 .expect("valid call-link session");
3131 assert_eq!(
3132 registry.apply_waiting_room(
3133 WaitingRoom::builder()
3134 .call_id(call_id.clone())
3135 .call_creator(creator.clone())
3136 .link_token("TEST-CALL-LINK".to_string())
3137 .media(CallLinkMedia::Audio)
3138 .enabled(true)
3139 .is_admin(true)
3140 .transaction_id(1)
3141 .users(Vec::new())
3142 .build(),
3143 ),
3144 wacore::voip::GroupStateApply::Applied
3145 );
3146
3147 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3148 let request_client = client.clone();
3149 let request_call_id = call_id.clone();
3150 let request_creator = creator.clone();
3151 let request_user = user.clone();
3152 let control = tokio::spawn(async move {
3153 match action {
3154 WaitingRoomUserAction::Admit => {
3155 request_client
3156 .voip()
3157 .admit_waiting_user_for_generation(
3158 &request_call_id,
3159 &request_creator,
3160 first,
3161 &request_user,
3162 )
3163 .await
3164 }
3165 WaitingRoomUserAction::Deny => {
3166 request_client
3167 .voip()
3168 .deny_waiting_user_for_generation(
3169 &request_call_id,
3170 &request_creator,
3171 first,
3172 &request_user,
3173 )
3174 .await
3175 }
3176 }
3177 });
3178 let request = sent.await.expect("waiting-room user request");
3179 let request_id = request
3180 .as_node_ref()
3181 .attrs()
3182 .optional_string("id")
3183 .expect("request id")
3184 .into_owned();
3185
3186 let replacement = registry.insert(CallSession::new_outgoing(
3187 &call_id,
3188 Jid::new(&call_id, Server::Call),
3189 creator,
3190 ));
3191 assert_ne!(replacement, first);
3192 let action_type = match action {
3193 WaitingRoomUserAction::Admit => "waiting_room_admit",
3194 WaitingRoomUserAction::Deny => "waiting_room_deny",
3195 };
3196 crate::test_utils::answer_iq(
3197 &client,
3198 &request_id,
3199 &NodeBuilder::new("ack")
3200 .attr("class", "call")
3201 .attr("type", action_type)
3202 .attr("id", request_id.as_str())
3203 .build(),
3204 )
3205 .await;
3206
3207 assert!(matches!(
3208 control.await.expect("waiting-room control task"),
3209 Err(CallError::Media(
3210 "call was replaced while applying group control"
3211 ))
3212 ));
3213 registry.remove_if_current(&call_id, replacement);
3214 }
3215 }
3216
3217 #[cfg(feature = "voip-runtime")]
3218 #[tokio::test]
3219 async fn call_link_admission_is_buffered_until_ack_registration() {
3220 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3221 let creator = Jid::new("333333333333333", Server::Lid);
3222 let call_id = "BUFFERED-ADMISSION";
3223 let local_device = Jid::new("111111111111111", Server::Lid).with_device(1);
3224 let mut participant = GroupCallParticipant::new(
3225 local_device.to_non_ad(),
3226 vec![GroupCallDevice::new(local_device.clone())],
3227 );
3228 participant.state = Some("connected".to_string());
3229 let update = GroupCallUpdate::builder()
3230 .call_id(call_id.to_string())
3231 .call_creator(creator.clone())
3232 .transaction_id(8)
3233 .media("audio".to_string())
3234 .connected_limit(32)
3235 .joinable(true)
3236 .av_upgradable(true)
3237 .rekey_requested(false)
3238 .participants(vec![participant])
3239 .build();
3240 let creator_sender = creator.clone().with_device(1);
3241
3242 let pending = client.begin_call_link_join();
3243 assert_eq!(
3244 client.buffer_pending_call_link_update(&update, &creator_sender),
3245 PendingCallLinkBuffer::Buffered,
3246 "the creator's admission update must survive until the ACK registers its call id"
3247 );
3248 assert_eq!(
3249 client.buffer_pending_call_link_update(
3250 &update,
3251 &Jid::new("999999999999999", Server::Lid)
3252 ),
3253 PendingCallLinkBuffer::NotPending,
3254 "an unrelated sender cannot populate the pre-registration buffer"
3255 );
3256 let pending_memory = client.memory_report().await.pending_call_link_updates;
3257 assert_eq!(pending_memory.entries, 1);
3258 assert!(pending_memory.bytes > 0);
3259
3260 let mut session =
3261 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3262 let _ = session.transition_to(CallPhase::Calling);
3263 let _ = session.transition_to(CallPhase::WaitingRoom);
3264 let generation = client
3265 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3266 .await
3267 .expect("valid buffered admission");
3268 assert!(client.call_registry().set_group_invite_self_device(
3269 call_id,
3270 generation,
3271 GroupCallDevice::new(local_device).with_capability(1, [1]),
3272 ));
3273 assert_eq!(
3274 client.call_registry().phase_if_current(call_id, generation),
3275 Some(CallPhase::Connecting),
3276 "consuming the buffered admission must perform the waiting-room transition"
3277 );
3278 assert_eq!(
3279 client
3280 .call_registry()
3281 .group_state_if_current(call_id, generation)
3282 .and_then(|state| { state.snapshot().map(|snapshot| snapshot.transaction_id) }),
3283 Some(8)
3284 );
3285 assert_eq!(
3286 client
3287 .memory_report()
3288 .await
3289 .pending_call_link_updates
3290 .entries,
3291 0
3292 );
3293 let mut later = update;
3294 later.transaction_id = 9;
3295 assert_eq!(
3296 client.buffer_pending_call_link_update(&later, &creator_sender),
3297 PendingCallLinkBuffer::NotPending,
3298 "an already registered generation must dispatch instead of entering an orphan buffer"
3299 );
3300
3301 drop(pending);
3302 client
3303 .call_registry()
3304 .remove_if_current(call_id, generation);
3305 }
3306
3307 #[cfg(feature = "voip-runtime")]
3308 #[tokio::test]
3309 async fn call_link_termination_before_registration_rejects_the_join() {
3310 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3311 let creator = Jid::new("333333333333333", Server::Lid);
3312 let call_id = "TERMINATED-CALL-LINK";
3313 let _pending = client.begin_call_link_join();
3314 assert!(
3315 client
3316 .retain_or_apply_pending_call_link_terminate(
3317 call_id,
3318 &creator,
3319 &creator.clone().with_device(1),
3320 )
3321 .await
3322 );
3323
3324 let mut session =
3325 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3326 let _ = session.transition_to(CallPhase::Calling);
3327 let _ = session.transition_to(CallPhase::WaitingRoom);
3328 assert_eq!(
3329 client
3330 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3331 .await,
3332 Err(wacore::voip::GroupStateApply::InvalidSnapshot)
3333 );
3334 assert_eq!(
3335 client.call_registry().generation_of(call_id),
3336 None,
3337 "a terminal control that overtakes registration must prevent publication"
3338 );
3339 }
3340
3341 #[cfg(feature = "voip-runtime")]
3342 #[tokio::test]
3343 async fn call_link_termination_removes_a_generation_that_won_registration() {
3344 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3345 let creator = Jid::new("333333333333333", Server::Lid);
3346 let call_id = "REGISTERED-THEN-TERMINATED-CALL-LINK";
3347 let _pending = client.begin_call_link_join();
3348 let mut session =
3349 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
3350 let _ = session.transition_to(CallPhase::Calling);
3351 let _ = session.transition_to(CallPhase::WaitingRoom);
3352 let generation = client
3353 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3354 .await
3355 .expect("registration wins the answer-transition lane");
3356
3357 assert!(
3358 client
3359 .retain_or_apply_pending_call_link_terminate(
3360 call_id,
3361 &creator,
3362 &creator.with_device(1),
3363 )
3364 .await
3365 );
3366 assert_eq!(
3367 client.call_registry().generation_of(call_id),
3368 None,
3369 "the terminal control must remove the just-published generation"
3370 );
3371 assert!(
3372 !client.call_registry().is_current(call_id, generation),
3373 "the removed generation cannot remain active"
3374 );
3375 }
3376
3377 #[cfg(feature = "voip-runtime")]
3378 #[tokio::test]
3379 async fn call_link_epoch_before_registration_is_replayed_to_the_generation() {
3380 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3381 let creator = Jid::new("333333333333333", Server::Lid);
3382 let call_id = "EPOCH-CALL-LINK";
3383 let _pending = client.begin_call_link_join();
3384 assert_eq!(
3385 client.buffer_pending_call_link_epoch(
3386 call_id,
3387 &creator,
3388 &creator.clone().with_device(1),
3389 7,
3390 &[7; 32],
3391 ),
3392 PendingCallLinkBuffer::Buffered
3393 );
3394
3395 let mut session =
3396 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3397 let _ = session.transition_to(CallPhase::Calling);
3398 let _ = session.transition_to(CallPhase::Connecting);
3399 let generation = client
3400 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3401 .await
3402 .expect("valid call-link generation");
3403 assert_eq!(
3404 client
3405 .call_registry()
3406 .pending_group_epoch_transaction_if_current(call_id, generation),
3407 Some(7),
3408 "the decrypted epoch must survive until the media driver attaches"
3409 );
3410 client
3411 .call_registry()
3412 .remove_if_current(call_id, generation);
3413 }
3414
3415 #[cfg(feature = "voip-runtime")]
3416 #[tokio::test]
3417 async fn staged_call_link_epoch_and_termination_revalidate_provenance() {
3418 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3419 let call_id = "PROVENANCE-CALL-LINK";
3420 let creator = Jid::new("333333333333333", Server::Lid);
3421 let asserted_creator = Jid::new("999999999999999", Server::Lid);
3422 let asserted_sender = asserted_creator.clone().with_device(7);
3423 let _pending = client.begin_call_link_join();
3424
3425 assert_eq!(
3426 client.buffer_pending_call_link_epoch(
3427 call_id,
3428 &asserted_creator,
3429 &asserted_sender,
3430 7,
3431 &[7; 32],
3432 ),
3433 PendingCallLinkBuffer::Buffered
3434 );
3435 assert_eq!(
3436 client
3437 .buffer_pending_call_link_terminate(call_id, &asserted_creator, &asserted_sender,),
3438 PendingCallLinkBuffer::Buffered
3439 );
3440
3441 let mut session =
3442 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3443 let _ = session.transition_to(CallPhase::Calling);
3444 let _ = session.transition_to(CallPhase::Connecting);
3445 let generation = client
3446 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3447 .await
3448 .expect("unauthorized staged controls must not abort the legitimate join");
3449 assert!(
3450 client.call_registry().is_current(call_id, generation),
3451 "the unauthorized terminal marker must be ignored after registration"
3452 );
3453 assert_eq!(
3454 client
3455 .call_registry()
3456 .pending_group_epoch_transaction_if_current(call_id, generation),
3457 None,
3458 "the unauthorized epoch must not enter the registered generation"
3459 );
3460 client
3461 .call_registry()
3462 .remove_if_current(call_id, generation);
3463 }
3464
3465 #[cfg(feature = "voip-runtime")]
3466 #[tokio::test]
3467 async fn concurrent_call_link_join_waits_for_the_unknown_call_id_lane() {
3468 use std::time::Duration;
3469
3470 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3471 client
3472 .persistence_manager()
3473 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3474 Jid::new("111111111111111", Server::Lid).with_device(1),
3475 )))
3476 .await;
3477
3478 let first_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3479 let first_client = client.clone();
3480 let first = tokio::spawn(async move {
3481 first_client
3482 .voip()
3483 .join_call_link_registration_with_audio(
3484 "FIRST-CALL-LINK",
3485 CallLinkMedia::Audio,
3486 AudioFormat::OPUS_16KHZ_60MS,
3487 )
3488 .await
3489 });
3490 first_request.await.expect("first link_join request");
3491
3492 let second_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3493 let second_client = client.clone();
3494 let second = tokio::spawn(async move {
3495 second_client
3496 .voip()
3497 .join_call_link_registration_with_audio(
3498 "SECOND-CALL-LINK",
3499 CallLinkMedia::Audio,
3500 AudioFormat::OPUS_16KHZ_60MS,
3501 )
3502 .await
3503 });
3504 assert!(
3505 tokio::time::timeout(Duration::from_millis(25), second_request)
3506 .await
3507 .is_err(),
3508 "a second unknown-call-id join must wait instead of sharing the first join's buffer"
3509 );
3510
3511 let released_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3512 first.abort();
3513 let _ = first.await;
3514 tokio::time::timeout(Duration::from_secs(1), released_request)
3515 .await
3516 .expect("the second join lane should be released")
3517 .expect("second link_join request");
3518 second.abort();
3519 let _ = second.await;
3520 }
3521
3522 #[cfg(feature = "voip-runtime")]
3523 #[tokio::test]
3524 async fn registered_call_link_releases_the_unknown_id_lane_before_heartbeat() {
3525 use std::time::Duration;
3526 use wacore::handshake::NoiseCipher;
3527
3528 struct GatedTransport {
3529 started: async_channel::Sender<()>,
3530 release: async_channel::Receiver<()>,
3531 gate_next_send: std::sync::atomic::AtomicBool,
3532 }
3533
3534 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
3535 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
3536 impl crate::transport::Transport for GatedTransport {
3537 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
3538 if self.gate_next_send.swap(false, Ordering::AcqRel) {
3539 self.started
3540 .send(())
3541 .await
3542 .map_err(|_| anyhow::anyhow!("heartbeat observer closed"))?;
3543 self.release
3544 .recv()
3545 .await
3546 .map_err(|_| anyhow::anyhow!("heartbeat gate closed"))?;
3547 }
3548 Ok(())
3549 }
3550
3551 async fn disconnect(&self) {}
3552 }
3553
3554 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3555 client
3556 .persistence_manager()
3557 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3558 Jid::new("111111111111111", Server::Lid).with_device(1),
3559 )))
3560 .await;
3561 let creator = Jid::new("333333333333333", Server::Lid);
3562 let first_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3563 let first_client = client.clone();
3564 let first = tokio::spawn(async move {
3565 first_client
3566 .voip()
3567 .join_call_link_registration_with_audio(
3568 "FIRST-CALL-LINK",
3569 CallLinkMedia::Audio,
3570 AudioFormat::OPUS_16KHZ_60MS,
3571 )
3572 .await
3573 });
3574 let request = first_request.await.expect("first link_join request");
3575 let request_id = request
3576 .as_node_ref()
3577 .attrs()
3578 .optional_string("id")
3579 .expect("request id")
3580 .into_owned();
3581 let (started_tx, started_rx) = async_channel::bounded(1);
3582 let (release_tx, release_rx) = async_channel::bounded(1);
3583 let gated_socket = crate::socket::NoiseSocket::new(
3584 Arc::new(crate::runtime_impl::TokioRuntime),
3585 Arc::new(GatedTransport {
3586 started: started_tx,
3587 release: release_rx,
3588 gate_next_send: std::sync::atomic::AtomicBool::new(true),
3589 }),
3590 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
3591 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
3592 );
3593 *client.noise_socket.lock().await = Some(Arc::new(gated_socket));
3594 crate::test_utils::answer_iq(
3595 &client,
3596 &request_id,
3597 &NodeBuilder::new("ack")
3598 .attr("class", "call")
3599 .attr("type", "link_join")
3600 .attr("id", request_id.as_str())
3601 .children([NodeBuilder::new("waiting_room")
3602 .attr("call-id", "FIRST-CALL-ID")
3603 .attr("call-creator", creator.clone())
3604 .attr("link-token", "FIRST-CALL-LINK")
3605 .attr("media", "audio")
3606 .attr("enabled", "1")
3607 .attr("is_admin", "0")
3608 .attr("transaction-id", "1")
3609 .build()])
3610 .build(),
3611 )
3612 .await;
3613 started_rx
3614 .recv()
3615 .await
3616 .expect("first waiting-room heartbeat entered the gated transport");
3617
3618 let second_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3619 let second_client = client.clone();
3620 let second = tokio::spawn(async move {
3621 second_client
3622 .voip()
3623 .join_call_link_registration_with_audio(
3624 "SECOND-CALL-LINK",
3625 CallLinkMedia::Audio,
3626 AudioFormat::OPUS_16KHZ_60MS,
3627 )
3628 .await
3629 });
3630 let request = tokio::time::timeout(Duration::from_secs(1), second_request)
3631 .await
3632 .expect("registration must release the lane while the heartbeat remains gated")
3633 .expect("second link_join request");
3634 assert_eq!(
3635 request
3636 .as_node_ref()
3637 .children()
3638 .expect("second request action")[0]
3639 .tag,
3640 "link_join"
3641 );
3642 let second_sender = creator.clone().with_device(1);
3643 let second_update = GroupCallUpdate::builder()
3644 .call_id("SECOND-CALL-ID".to_string())
3645 .call_creator(creator)
3646 .transaction_id(1)
3647 .media("audio".to_string())
3648 .connected_limit(32)
3649 .joinable(true)
3650 .av_upgradable(true)
3651 .rekey_requested(false)
3652 .participants(Vec::new())
3653 .build();
3654 assert_eq!(
3655 client.buffer_pending_call_link_update(&second_update, &second_sender),
3656 PendingCallLinkBuffer::Buffered,
3657 "the second join must not inherit the first call id's provisional binding"
3658 );
3659
3660 release_tx.send(()).await.expect("release heartbeat send");
3661 second.abort();
3662 let _ = second.await;
3663 let registration = first
3664 .await
3665 .expect("first join task")
3666 .expect("first waiting-room registration");
3667 drop(registration);
3668 }
3669
3670 #[cfg(feature = "voip-runtime")]
3671 #[tokio::test]
3672 async fn pending_call_link_transitions_are_bounded_by_retained_bytes() {
3673 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3674 let creator = Jid::new("333333333333333", Server::Lid);
3675 let sender = creator.clone().with_device(1);
3676 let _pending = client.begin_call_link_join();
3677 let chunk = MAX_PENDING_CALL_LINK_TRANSITION_BYTES / 3;
3678 let mut accepted = 0;
3679 for index in 0..4 {
3680 let participant = GroupCallParticipant::new(
3681 creator.clone(),
3682 vec![GroupCallDevice::new(sender.clone()).with_capability(1, vec![7; chunk])],
3683 );
3684 let update = GroupCallUpdate::builder()
3685 .call_id(format!("BUFFERED-BYTES-{index}"))
3686 .call_creator(creator.clone())
3687 .transaction_id(1)
3688 .media("audio".to_string())
3689 .connected_limit(32)
3690 .joinable(true)
3691 .av_upgradable(true)
3692 .rekey_requested(false)
3693 .participants(vec![participant])
3694 .build();
3695 accepted += usize::from(
3696 client.buffer_pending_call_link_update(&update, &sender)
3697 == PendingCallLinkBuffer::Buffered,
3698 );
3699 }
3700 let stats = client.memory_report().await.pending_call_link_updates;
3701 assert!(
3702 accepted < 4,
3703 "the aggregate byte budget must reject excess staged snapshots"
3704 );
3705 assert!(
3706 stats.bytes <= MAX_PENDING_CALL_LINK_TRANSITION_BYTES as u64,
3707 "retained staged snapshots must stay within the aggregate byte budget"
3708 );
3709
3710 let oversized = GroupCallUpdate::builder()
3711 .call_id("BUFFERED-OVERSIZED".to_string())
3712 .call_creator(creator.clone())
3713 .transaction_id(1)
3714 .media("audio".to_string())
3715 .connected_limit(32)
3716 .joinable(true)
3717 .av_upgradable(true)
3718 .rekey_requested(false)
3719 .participants(vec![GroupCallParticipant::new(
3720 creator,
3721 vec![
3722 GroupCallDevice::new(sender.clone())
3723 .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3724 ],
3725 )])
3726 .build();
3727 assert_eq!(
3728 client.buffer_pending_call_link_update(&oversized, &sender),
3729 PendingCallLinkBuffer::Saturated,
3730 "one staged snapshot cannot consume the entire retained-byte budget"
3731 );
3732 }
3733
3734 #[cfg(feature = "voip-runtime")]
3735 #[tokio::test]
3736 async fn saturated_call_link_admission_fails_instead_of_falling_back() {
3737 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3738 let creator = Jid::new("333333333333333", Server::Lid);
3739 let sender = creator.clone().with_device(1);
3740 let call_id = "SATURATED-ADMISSION";
3741 let _pending = client.begin_call_link_join();
3742 let update = GroupCallUpdate::builder()
3743 .call_id(call_id.to_string())
3744 .call_creator(creator.clone())
3745 .transaction_id(1)
3746 .media("audio".to_string())
3747 .connected_limit(32)
3748 .joinable(true)
3749 .av_upgradable(true)
3750 .rekey_requested(false)
3751 .participants(vec![GroupCallParticipant::new(
3752 creator.clone(),
3753 vec![
3754 GroupCallDevice::new(sender.clone())
3755 .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3756 ],
3757 )])
3758 .build();
3759 assert_eq!(
3760 client.buffer_pending_call_link_update(&update, &sender),
3761 PendingCallLinkBuffer::Saturated,
3762 "the admission is handled locally even when it exceeds the staging budget"
3763 );
3764
3765 let mut session =
3766 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3767 let _ = session.transition_to(CallPhase::Calling);
3768 let _ = session.transition_to(CallPhase::WaitingRoom);
3769 assert_eq!(
3770 client
3771 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
3772 .await,
3773 Err(wacore::voip::GroupStateApply::InvalidSnapshot),
3774 "a saturated join must fail rather than wait forever for a discarded admission"
3775 );
3776 assert_eq!(client.call_registry().generation_of(call_id), None);
3777 }
3778
3779 #[cfg(feature = "voip-runtime")]
3780 #[tokio::test]
3781 async fn saturated_admission_is_retained_when_unrelated_call_ids_fill_the_payload_budget() {
3782 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3783 let creator = Jid::new("333333333333333", Server::Lid);
3784 let sender = creator.clone().with_device(1);
3785 let _pending = client.begin_call_link_join();
3786 for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3787 let unrelated_creator = Jid::new(format!("55555555555{index:04}"), Server::Lid);
3788 let unrelated_sender = unrelated_creator.clone().with_device(1);
3789 assert_eq!(
3790 client.buffer_pending_call_link_terminate(
3791 &format!("UNRELATED-{index}"),
3792 &unrelated_creator,
3793 &unrelated_sender,
3794 ),
3795 PendingCallLinkBuffer::Buffered
3796 );
3797 }
3798
3799 let call_id = "SATURATED-AFTER-UNRELATED";
3800 let oversized = GroupCallUpdate::builder()
3801 .call_id(call_id.to_string())
3802 .call_creator(creator.clone())
3803 .transaction_id(1)
3804 .media("audio".to_string())
3805 .connected_limit(32)
3806 .joinable(true)
3807 .av_upgradable(true)
3808 .rekey_requested(false)
3809 .participants(vec![GroupCallParticipant::new(
3810 creator.clone(),
3811 vec![
3812 GroupCallDevice::new(sender.clone())
3813 .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3814 ],
3815 )])
3816 .build();
3817 assert_eq!(
3818 client.buffer_pending_call_link_update(&oversized, &sender),
3819 PendingCallLinkBuffer::Saturated
3820 );
3821
3822 let mut session =
3823 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3824 let _ = session.transition_to(CallPhase::Calling);
3825 let _ = session.transition_to(CallPhase::WaitingRoom);
3826 assert_eq!(
3827 client
3828 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
3829 .await,
3830 Err(wacore::voip::GroupStateApply::InvalidSnapshot),
3831 "binding the ACK must retain the exact overflow identity outside the full payload map"
3832 );
3833 assert_eq!(client.call_registry().generation_of(call_id), None);
3834 }
3835
3836 #[cfg(feature = "voip-runtime")]
3837 #[tokio::test]
3838 async fn unrelated_saturation_does_not_reject_the_valid_call_link_join() {
3839 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3840 let unrelated_creator = Jid::new("333333333333333", Server::Lid);
3841 let unrelated_sender = unrelated_creator.clone().with_device(1);
3842 let _pending = client.begin_call_link_join();
3843 for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3844 assert!(
3845 client
3846 .buffer_pending_call_link_terminate(
3847 &format!("UNRELATED-{index}"),
3848 &unrelated_creator,
3849 &unrelated_sender,
3850 )
3851 .suppresses_dispatch()
3852 );
3853 }
3854 let mut oversized = GroupCallUpdate::builder()
3855 .call_id("UNRELATED-SATURATED-CALL-0".to_string())
3856 .call_creator(unrelated_creator.clone())
3857 .transaction_id(1)
3858 .media("audio".to_string())
3859 .connected_limit(32)
3860 .joinable(true)
3861 .av_upgradable(true)
3862 .rekey_requested(false)
3863 .participants(vec![GroupCallParticipant::new(
3864 unrelated_creator,
3865 vec![
3866 GroupCallDevice::new(unrelated_sender.clone())
3867 .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3868 ],
3869 )])
3870 .build();
3871 for index in 0..=MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
3872 oversized.call_id = format!("UNRELATED-SATURATED-CALL-{index}");
3873 assert_eq!(
3874 client.buffer_pending_call_link_update(&oversized, &unrelated_sender),
3875 PendingCallLinkBuffer::Saturated,
3876 "every oversized unrelated identity must remain handled locally"
3877 );
3878 }
3879
3880 let call_id = "VALID-CALL-LINK";
3881 let call_creator = Jid::new("444444444444444", Server::Lid);
3882 let ack = NodeBuilder::new("ack")
3883 .attr("class", "call")
3884 .attr("type", "link_join")
3885 .children([NodeBuilder::new("waiting_room")
3886 .attr("call-id", call_id)
3887 .build()])
3888 .build();
3889 client.bind_pending_call_link_join_ack(&ack.as_node_ref());
3890 assert!(
3891 client.prepare_pending_call_link_join_retry(call_id),
3892 "exhausted pre-ACK identity metadata requires one exact-call refresh"
3893 );
3894 assert_eq!(
3895 client
3896 .memory_report()
3897 .await
3898 .pending_call_link_updates
3899 .entries,
3900 0,
3901 "binding the ACK must discard every unrelated candidate bucket"
3902 );
3903 assert_eq!(
3904 client.buffer_pending_call_link_terminate(
3905 "LATE-UNRELATED",
3906 &unrelated_sender.to_non_ad(),
3907 &unrelated_sender,
3908 ),
3909 PendingCallLinkBuffer::NotPending,
3910 "later unrelated controls cannot consume the bound join's budget"
3911 );
3912 let mut participant = GroupCallParticipant::new(
3913 call_creator.clone(),
3914 vec![GroupCallDevice::new(call_creator.clone().with_device(1))],
3915 );
3916 participant.state = Some("connected".to_string());
3917 let admitted = GroupCallUpdate::builder()
3918 .call_id(call_id.to_string())
3919 .call_creator(call_creator.clone())
3920 .transaction_id(1)
3921 .media("audio".to_string())
3922 .connected_limit(32)
3923 .joinable(true)
3924 .av_upgradable(true)
3925 .rekey_requested(false)
3926 .participants(vec![participant])
3927 .build();
3928 assert_eq!(
3929 client
3930 .buffer_pending_call_link_update(&admitted, &call_creator.clone().with_device(1),),
3931 PendingCallLinkBuffer::Buffered
3932 );
3933 let mut session =
3934 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), call_creator);
3935 let _ = session.transition_to(CallPhase::Calling);
3936 let _ = session.transition_to(CallPhase::WaitingRoom);
3937 let generation = client
3938 .register_call_link_session(session, None, CallLinkMedia::Audio, "VALID-CALL-LINK")
3939 .await
3940 .expect("an unrelated saturated control cannot abort the ACK's actual call id");
3941 assert!(
3942 client.call_registry().is_current(call_id, generation),
3943 "the legitimate call-link generation must remain registered"
3944 );
3945 assert_eq!(
3946 client
3947 .call_registry()
3948 .group_state_if_current(call_id, generation)
3949 .and_then(|state| state.snapshot().map(|snapshot| snapshot.transaction_id)),
3950 Some(1)
3951 );
3952 client
3953 .call_registry()
3954 .remove_if_current(call_id, generation);
3955 }
3956
3957 #[cfg(feature = "voip-runtime")]
3958 #[tokio::test]
3959 async fn ambiguous_pre_ack_saturation_retries_after_binding_the_exact_call_id() {
3960 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3961 client
3962 .persistence_manager()
3963 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3964 Jid::new("111111111111111", Server::Lid).with_device(1),
3965 )))
3966 .await;
3967 let creator = Jid::new("333333333333333", Server::Lid);
3968 let sender = creator.clone().with_device(1);
3969 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3970 let join_client = client.clone();
3971 let join = tokio::spawn(async move {
3972 join_client
3973 .voip()
3974 .join_call_link_registration_with_audio(
3975 "RETRIED-CALL-LINK",
3976 CallLinkMedia::Audio,
3977 AudioFormat::OPUS_16KHZ_60MS,
3978 )
3979 .await
3980 });
3981 let first_request = sent.await.expect("initial link_join request");
3982
3983 for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3984 assert_eq!(
3985 client.buffer_pending_call_link_terminate(
3986 &format!("UNRELATED-FILLED-{index}"),
3987 &creator,
3988 &sender,
3989 ),
3990 PendingCallLinkBuffer::Buffered
3991 );
3992 }
3993 let mut oversized = GroupCallUpdate::builder()
3994 .call_id("UNRELATED-OVERFLOW-0".to_string())
3995 .call_creator(creator.clone())
3996 .transaction_id(1)
3997 .media("audio".to_string())
3998 .connected_limit(32)
3999 .joinable(true)
4000 .av_upgradable(true)
4001 .rekey_requested(false)
4002 .participants(vec![GroupCallParticipant::new(
4003 creator.clone(),
4004 vec![
4005 GroupCallDevice::new(sender.clone())
4006 .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
4007 ],
4008 )])
4009 .build();
4010 for index in 0..=MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
4011 oversized.call_id = format!("UNRELATED-OVERFLOW-{index}");
4012 assert_eq!(
4013 client.buffer_pending_call_link_update(&oversized, &sender),
4014 PendingCallLinkBuffer::Saturated
4015 );
4016 }
4017
4018 let first_request_id = first_request
4019 .as_node_ref()
4020 .attrs()
4021 .optional_string("id")
4022 .expect("initial request id")
4023 .into_owned();
4024 let refreshed_sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4025 crate::test_utils::answer_iq(
4026 &client,
4027 &first_request_id,
4028 &NodeBuilder::new("ack")
4029 .attr("class", "call")
4030 .attr("type", "link_join")
4031 .attr("id", first_request_id.as_str())
4032 .children([NodeBuilder::new("waiting_room")
4033 .attr("call-id", "RETRIED-CALL-ID")
4034 .attr("call-creator", creator.clone())
4035 .attr("link-token", "RETRIED-CALL-LINK")
4036 .attr("media", "audio")
4037 .attr("enabled", "1")
4038 .attr("is_admin", "0")
4039 .attr("transaction-id", "1")
4040 .build()])
4041 .build(),
4042 )
4043 .await;
4044 let refreshed_request = refreshed_sent.await.expect("refreshed link_join request");
4045 assert_eq!(
4046 refreshed_request
4047 .as_node_ref()
4048 .children()
4049 .expect("refreshed request action")[0]
4050 .tag,
4051 "link_join"
4052 );
4053 let refreshed_request_id = refreshed_request
4054 .as_node_ref()
4055 .attrs()
4056 .optional_string("id")
4057 .expect("refreshed request id")
4058 .into_owned();
4059 crate::test_utils::answer_iq(
4060 &client,
4061 &refreshed_request_id,
4062 &NodeBuilder::new("ack")
4063 .attr("class", "call")
4064 .attr("type", "link_join")
4065 .attr("id", refreshed_request_id.as_str())
4066 .children([NodeBuilder::new("waiting_room")
4067 .attr("call-id", "RETRIED-CALL-ID")
4068 .attr("call-creator", creator)
4069 .attr("link-token", "RETRIED-CALL-LINK")
4070 .attr("media", "audio")
4071 .attr("enabled", "1")
4072 .attr("is_admin", "0")
4073 .attr("transaction-id", "2")
4074 .build()])
4075 .build(),
4076 )
4077 .await;
4078 let registration = join
4079 .await
4080 .expect("join task")
4081 .expect("an unrelated overflow must recover through the bound retry");
4082 assert_eq!(registration.join.call_id, "RETRIED-CALL-ID");
4083 assert!(
4084 client
4085 .call_registry()
4086 .is_current("RETRIED-CALL-ID", registration.generation)
4087 );
4088 client
4089 .call_registry()
4090 .remove_if_current("RETRIED-CALL-ID", registration.generation);
4091 }
4092
4093 #[cfg(feature = "voip-runtime")]
4094 #[tokio::test]
4095 async fn call_link_ack_paths_bind_before_waking_the_waiter() {
4096 for owned_fast_path in [false, true] {
4097 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4098 let unrelated_creator = Jid::new("333333333333333", Server::Lid);
4099 let unrelated_sender = unrelated_creator.clone().with_device(1);
4100 let _pending = client.begin_call_link_join();
4101 assert_eq!(
4102 client.buffer_pending_call_link_terminate(
4103 "UNRELATED-CALL-LINK",
4104 &unrelated_creator,
4105 &unrelated_sender,
4106 ),
4107 PendingCallLinkBuffer::Buffered
4108 );
4109
4110 let request_id = if owned_fast_path {
4111 "OWNED-LINK-JOIN-ACK"
4112 } else {
4113 "SHARED-LINK-JOIN-ACK"
4114 };
4115 let (sender, receiver) = futures::channel::oneshot::channel();
4116 client
4117 .response_waiters_guard()
4118 .insert(request_id.to_string(), ResponseWaiter::Iq(sender));
4119 let ack = NodeBuilder::new("ack")
4120 .attr("id", request_id)
4121 .attr("class", "call")
4122 .attr("type", "link_join")
4123 .children([NodeBuilder::new("waiting_room")
4124 .attr("call-id", "ACTUAL-CALL-LINK")
4125 .build()])
4126 .build();
4127 let node = crate::test_utils::node_to_owned_ref(&ack);
4128 let handled = if owned_fast_path {
4129 let node = Arc::try_unwrap(node)
4130 .unwrap_or_else(|_| panic!("the test owns the ACK allocation"));
4131 client.handle_ack_response_owned(node)
4132 } else {
4133 client.handle_ack_response_arc(&node)
4134 };
4135 assert!(handled, "the ACK must resolve its registered waiter");
4136 assert_eq!(
4137 client
4138 .memory_report()
4139 .await
4140 .pending_call_link_updates
4141 .entries,
4142 0,
4143 "the ACK call id must be bound before the waiter can observe the response"
4144 );
4145 assert_eq!(
4146 client.buffer_pending_call_link_terminate(
4147 "LATE-UNRELATED-CALL",
4148 &unrelated_creator,
4149 &unrelated_sender,
4150 ),
4151 PendingCallLinkBuffer::NotPending,
4152 "later unrelated controls cannot consume the bound join's budget"
4153 );
4154 let response = receiver.await.expect("the ACK waiter should be woken");
4155 assert!(
4156 response
4157 .get()
4158 .get_attr("id")
4159 .is_some_and(|value| value.as_str() == request_id)
4160 );
4161 }
4162 }
4163
4164 #[cfg(feature = "voip-runtime")]
4165 #[tokio::test]
4166 async fn call_link_registration_replays_staged_transitions_in_order() {
4167 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4168 let creator = Jid::new("333333333333333", Server::Lid);
4169 let call_id = "ORDERED-CALL-LINK";
4170 let local_device = Jid::new("111111111111111", Server::Lid).with_device(1);
4171 let mut participant = GroupCallParticipant::new(
4172 local_device.to_non_ad(),
4173 vec![GroupCallDevice::new(local_device.clone())],
4174 );
4175 participant.state = Some("connected".to_string());
4176 let relay = GroupCallRelay::builder()
4177 .transaction_id(8)
4178 .self_pid(1)
4179 .uuid("TEST-RELAY".to_string())
4180 .participant_uuid("TEST-PARTICIPANT".to_string())
4181 .attribute_padding(false)
4182 .warp_mi_tag_len(4)
4183 .key(vec![7; 32])
4184 .tokens(vec![vec![9; 16]])
4185 .endpoints(vec![
4186 GroupCallRelayEndpoint::builder()
4187 .relay_id(1)
4188 .token_id(0)
4189 .auth_token_id(0)
4190 .relay_name("test-relay".to_string())
4191 .is_fna(false)
4192 .ipv4("203.0.113.7".to_string())
4193 .port(3478)
4194 .build(),
4195 ])
4196 .build();
4197 let first = GroupCallUpdate::builder()
4198 .call_id(call_id.to_string())
4199 .call_creator(creator.clone())
4200 .transaction_id(8)
4201 .media("audio".to_string())
4202 .connected_limit(32)
4203 .joinable(true)
4204 .av_upgradable(true)
4205 .rekey_requested(true)
4206 .participants(vec![participant.clone()])
4207 .relay(relay)
4208 .build();
4209 let second = GroupCallUpdate::builder()
4210 .call_id(call_id.to_string())
4211 .call_creator(creator.clone())
4212 .transaction_id(9)
4213 .media("audio".to_string())
4214 .connected_limit(32)
4215 .joinable(true)
4216 .av_upgradable(true)
4217 .rekey_requested(false)
4218 .participants(vec![participant])
4219 .build();
4220 let initial_room = WaitingRoom::builder()
4221 .call_id(call_id.to_string())
4222 .call_creator(creator.clone())
4223 .link_token("TEST-CALL-LINK".to_string())
4224 .media(CallLinkMedia::Audio)
4225 .enabled(true)
4226 .is_admin(false)
4227 .transaction_id(1)
4228 .users(Vec::new())
4229 .build();
4230 let newer_room = WaitingRoom::builder()
4231 .call_id(call_id.to_string())
4232 .call_creator(creator.clone())
4233 .link_token("TEST-CALL-LINK".to_string())
4234 .media(CallLinkMedia::Audio)
4235 .enabled(true)
4236 .is_admin(true)
4237 .transaction_id(2)
4238 .users(Vec::new())
4239 .build();
4240
4241 let pending = client.begin_call_link_join();
4242 let sender = creator.clone().with_device(1);
4243 assert_eq!(
4244 client.buffer_pending_call_link_update(&first, &sender),
4245 PendingCallLinkBuffer::Buffered
4246 );
4247 assert_eq!(
4248 client.buffer_pending_call_link_waiting_room(&newer_room, &sender),
4249 PendingCallLinkBuffer::Buffered
4250 );
4251 assert_eq!(
4252 client.buffer_pending_call_link_update(&second, &sender),
4253 PendingCallLinkBuffer::Buffered
4254 );
4255 assert_eq!(
4256 client
4257 .memory_report()
4258 .await
4259 .pending_call_link_updates
4260 .entries,
4261 3
4262 );
4263
4264 let mut session =
4265 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
4266 let _ = session.transition_to(CallPhase::Calling);
4267 let _ = session.transition_to(CallPhase::WaitingRoom);
4268 let registration_lane = client.lock_answer_transition(call_id).await;
4269 let register_client = client.clone();
4270 let registration = tokio::spawn(async move {
4271 register_client
4272 .register_call_link_session(
4273 session,
4274 Some(initial_room),
4275 CallLinkMedia::Audio,
4276 "TEST-CALL-LINK",
4277 )
4278 .await
4279 });
4280 tokio::task::yield_now().await;
4281 assert_eq!(
4282 client.call_registry().generation_of(call_id),
4283 None,
4284 "call-link insertion must share the call-id registration lane"
4285 );
4286 drop(registration_lane);
4287 let generation = registration
4288 .await
4289 .expect("registration task")
4290 .expect("valid staged transitions");
4291 assert!(client.call_registry().set_group_invite_self_device(
4292 call_id,
4293 generation,
4294 GroupCallDevice::new(local_device).with_capability(1, [1]),
4295 ));
4296 let state = client
4297 .call_registry()
4298 .group_state_if_current(call_id, generation)
4299 .expect("registered group state");
4300 let snapshot = state.snapshot().expect("latest admission roster");
4301 assert_eq!(snapshot.transaction_id, 9);
4302 assert!(snapshot.relay.is_some(), "roster-only update retains relay");
4303 assert!(
4304 snapshot.rekey_requested,
4305 "the earlier unfulfilled rekey obligation survives the roster-only update"
4306 );
4307 assert!(
4308 state
4309 .waiting_room()
4310 .is_some_and(|room| room.transaction_id == Some(2) && room.is_admin)
4311 );
4312 assert_eq!(
4313 client.call_registry().phase_if_current(call_id, generation),
4314 Some(CallPhase::Connecting)
4315 );
4316 assert_eq!(
4317 client
4318 .memory_report()
4319 .await
4320 .pending_call_link_updates
4321 .entries,
4322 0
4323 );
4324
4325 drop(pending);
4326 client
4327 .call_registry()
4328 .remove_if_current(call_id, generation);
4329 }
4330
4331 #[cfg(feature = "voip-runtime")]
4332 #[tokio::test]
4333 async fn call_link_rekey_targets_the_latest_post_registration_roster() {
4334 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4335 let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
4336 client
4337 .persistence_manager()
4338 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4339 own_lid.clone(),
4340 )))
4341 .await;
4342 let creator = Jid::new("333333333333333", Server::Lid);
4343 let call_id = "POST-REGISTRATION-REKEY";
4344 let mut participant =
4345 GroupCallParticipant::new(own_lid.to_non_ad(), vec![GroupCallDevice::new(own_lid)]);
4346 participant.state = Some("connected".to_string());
4347 let initial = GroupCallUpdate::builder()
4348 .call_id(call_id.to_string())
4349 .call_creator(creator.clone())
4350 .transaction_id(1)
4351 .media("audio".to_string())
4352 .connected_limit(32)
4353 .joinable(true)
4354 .av_upgradable(true)
4355 .rekey_requested(true)
4356 .participants(vec![participant.clone()])
4357 .build();
4358 let mut session =
4359 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
4360 session.group = Some(initial.clone());
4361 let _ = session.transition_to(CallPhase::Calling);
4362 let _ = session.transition_to(CallPhase::Connecting);
4363 let generation = client
4364 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
4365 .await
4366 .expect("registered admitted call link");
4367
4368 let mut current = initial.clone();
4369 current.transaction_id = 2;
4370 current.rekey_requested = false;
4371 assert_eq!(
4372 client
4373 .call_registry()
4374 .apply_group_update_if_current(current, generation),
4375 wacore::voip::GroupStateApply::Applied
4376 );
4377 let mut join = wacore::types::group_call::CallLinkJoin::builder()
4378 .token("TEST-CALL-LINK".to_string())
4379 .media(CallLinkMedia::Audio)
4380 .call_id(call_id.to_string())
4381 .call_creator(creator)
4382 .waiting_room_enabled(false)
4383 .in_waiting_room(false)
4384 .is_admin(false)
4385 .group(initial)
4386 .build();
4387
4388 assert!(
4389 !client
4390 .voip()
4391 .synchronize_call_link_admission(&mut join, generation, true)
4392 .await
4393 .expect("latest admission state")
4394 );
4395 assert_eq!(
4396 join.group.as_ref().map(|update| update.transaction_id),
4397 Some(2)
4398 );
4399 assert_eq!(
4400 client
4401 .call_registry()
4402 .pending_group_epoch_transaction_if_current(call_id, generation),
4403 Some(2),
4404 "the ACK rekey obligation must publish against the latest serialized roster"
4405 );
4406 client
4407 .call_registry()
4408 .remove_if_current(call_id, generation);
4409 }
4410
4411 #[cfg(feature = "voip-runtime")]
4412 #[tokio::test]
4413 async fn invalid_admitted_call_link_snapshot_is_not_registered() {
4414 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4415 let creator = Jid::new("333333333333333", Server::Lid);
4416 let call_id = "INVALID-CALL-LINK";
4417 let mut invalid = GroupCallUpdate::builder()
4418 .call_id(call_id.to_string())
4419 .call_creator(creator.clone())
4420 .transaction_id(1)
4421 .media("audio".to_string())
4422 .connected_limit(32)
4423 .joinable(true)
4424 .av_upgradable(true)
4425 .rekey_requested(false)
4426 .participants(Vec::new())
4427 .build();
4428 invalid.connected_limit = 0;
4429 let mut session =
4430 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
4431 session.group = Some(invalid);
4432
4433 assert_eq!(
4434 client
4435 .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
4436 .await,
4437 Err(wacore::voip::GroupStateApply::InvalidSnapshot)
4438 );
4439 assert_eq!(client.call_registry().generation_of(call_id), None);
4440 }
4441
4442 #[cfg(feature = "voip-runtime")]
4443 #[tokio::test]
4444 async fn buffered_call_link_admission_cannot_cross_generations() {
4445 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4446 let creator = Jid::new("333333333333333", Server::Lid);
4447 let call_id = "BUFFERED-ADMISSION-GENERATION";
4448 let mut participant = GroupCallParticipant::new(
4449 creator.clone(),
4450 vec![GroupCallDevice::new(creator.clone().with_device(1))],
4451 );
4452 participant.state = Some("connected".to_string());
4453 let update = GroupCallUpdate::builder()
4454 .call_id(call_id.to_string())
4455 .call_creator(creator.clone())
4456 .transaction_id(8)
4457 .media("audio".to_string())
4458 .connected_limit(32)
4459 .joinable(true)
4460 .av_upgradable(true)
4461 .rekey_requested(false)
4462 .participants(vec![participant])
4463 .build();
4464
4465 let registry = client.call_registry();
4466 let stale = registry.insert(CallSession::new_outgoing(
4467 call_id,
4468 Jid::new(call_id, Server::Call),
4469 creator.clone(),
4470 ));
4471 let replacement = registry.insert(CallSession::new_outgoing(
4472 call_id,
4473 Jid::new(call_id, Server::Call),
4474 creator,
4475 ));
4476 assert_ne!(stale, replacement);
4477 assert_eq!(
4478 client.apply_pending_call_link_update(update, stale),
4479 wacore::voip::GroupStateApply::UnknownCall
4480 );
4481 assert!(
4482 registry
4483 .group_state_if_current(call_id, replacement)
4484 .is_none(),
4485 "a buffered snapshot from the joining generation must not mutate its replacement"
4486 );
4487 registry.remove_if_current(call_id, replacement);
4488 }
4489
4490 #[cfg(feature = "voip-runtime")]
4491 #[tokio::test]
4492 async fn call_link_waiting_room_cannot_cross_generations() {
4493 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4494 let creator = Jid::new("333333333333333", Server::Lid);
4495 let call_id = "WAITING-ROOM-GENERATION";
4496 let registry = client.call_registry();
4497 let stale = registry.insert(CallSession::new_outgoing(
4498 call_id,
4499 Jid::new(call_id, Server::Call),
4500 creator.clone(),
4501 ));
4502 let replacement = registry.insert(CallSession::new_outgoing(
4503 call_id,
4504 Jid::new(call_id, Server::Call),
4505 creator.clone(),
4506 ));
4507 let room = WaitingRoom::builder()
4508 .call_id(call_id.to_string())
4509 .call_creator(creator)
4510 .link_token("TEST-CALL-LINK".to_string())
4511 .media(CallLinkMedia::Audio)
4512 .enabled(true)
4513 .is_admin(true)
4514 .transaction_id(1)
4515 .users(Vec::new())
4516 .build();
4517
4518 assert_eq!(
4519 registry.apply_waiting_room_if_current(room, stale),
4520 wacore::voip::GroupStateApply::UnknownCall
4521 );
4522 assert!(
4523 registry
4524 .group_state_if_current(call_id, replacement)
4525 .and_then(|state| state.waiting_room().cloned())
4526 .is_none(),
4527 "a stale join cannot grant waiting-room admin state to its replacement"
4528 );
4529 registry.remove_if_current(call_id, replacement);
4530 }
4531
4532 #[cfg(feature = "voip-runtime")]
4533 #[tokio::test]
4534 async fn early_group_invite_accept_preserves_media_attachment_generation() {
4535 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4536 let creator = call_creator();
4537 let call_id = "ATTACHABLE-GROUP-INVITE";
4538 let update = GroupCallUpdate::builder()
4539 .call_id(call_id.to_string())
4540 .call_creator(creator.clone())
4541 .transaction_id(1)
4542 .media("audio".to_string())
4543 .connected_limit(32)
4544 .joinable(true)
4545 .av_upgradable(true)
4546 .rekey_requested(false)
4547 .participants(Vec::new())
4548 .build();
4549 let mut incoming = IncomingCall::new_for_test(
4550 creator.clone(),
4551 "ATTACHABLE-GROUP-INVITE-STANZA".to_string(),
4552 wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4553 CallAction::Offer {
4554 call_id: call_id.to_string(),
4555 call_creator: creator.clone(),
4556 caller_pn: None,
4557 caller_country_code: None,
4558 device_class: None,
4559 joinable: true,
4560 is_video: false,
4561 audio: Vec::new(),
4562 group_jid: None,
4563 },
4564 );
4565 incoming.group = Some(Box::new(update.clone()));
4566 let mut ringing = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
4567 ringing.group = Some(update);
4568 let generation = client
4569 .call_registry()
4570 .insert_ringing_group_if_inactive(ringing)
4571 .expect("valid group snapshot")
4572 .expect("ringing invitation");
4573 incoming.set_ringing_generation(generation);
4574
4575 client
4576 .voip()
4577 .accept_group_invite(&incoming)
4578 .await
4579 .expect("early group invitation accept");
4580
4581 assert_eq!(
4582 client
4583 .call_registry()
4584 .ringing_group_generation(call_id, &creator),
4585 Some(generation),
4586 "the media accept builder must still be able to claim the exact ringing generation"
4587 );
4588 assert_eq!(
4589 client.call_registry().phase_if_current(call_id, generation),
4590 Some(CallPhase::Ringing)
4591 );
4592 assert!(client.call_registry().take_ringing(call_id));
4593 client
4594 .call_registry()
4595 .remove_if_current(call_id, generation);
4596 }
4597
4598 #[cfg(feature = "voip-runtime")]
4599 #[tokio::test]
4600 async fn group_invite_preaccept_and_accept_are_bound_to_the_retained_offer_generation() {
4601 let client = crate::test_utils::create_test_client().await;
4602 let retained_creator = call_creator();
4603 let replacement_creator = retained_creator.clone();
4604 let call_id = "REPLACED-GROUP-INVITE";
4605 let group_update = |creator: &Jid| {
4606 GroupCallUpdate::builder()
4607 .call_id(call_id.to_string())
4608 .call_creator(creator.clone())
4609 .transaction_id(1)
4610 .media("audio".to_string())
4611 .connected_limit(32)
4612 .joinable(true)
4613 .av_upgradable(true)
4614 .rekey_requested(false)
4615 .participants(Vec::new())
4616 .build()
4617 };
4618 let retained_update = group_update(&retained_creator);
4619 let mut incoming = IncomingCall::new_for_test(
4620 retained_creator.clone(),
4621 "RETAINED-GROUP-INVITE".to_string(),
4622 wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4623 CallAction::Offer {
4624 call_id: call_id.to_string(),
4625 call_creator: retained_creator.clone(),
4626 caller_pn: None,
4627 caller_country_code: None,
4628 device_class: None,
4629 joinable: true,
4630 is_video: false,
4631 audio: Vec::new(),
4632 group_jid: None,
4633 },
4634 );
4635 incoming.group = Some(Box::new(retained_update.clone()));
4636 let mut retained =
4637 CallSession::new_incoming(call_id, retained_creator.clone(), retained_creator);
4638 retained.group = Some(retained_update);
4639 let stale = client.call_registry().insert_ringing_group(retained);
4640 incoming.set_ringing_generation(stale);
4641
4642 let replacement_update = group_update(&replacement_creator);
4643 let mut replacement =
4644 CallSession::new_incoming(call_id, replacement_creator.clone(), replacement_creator);
4645 replacement.group = Some(replacement_update);
4646 let current = client.call_registry().insert_ringing_group(replacement);
4647
4648 assert!(matches!(
4649 client.voip().preaccept_group_invite(&incoming).await,
4650 Err(CallError::CallEndedDuringSetup)
4651 ));
4652 assert!(matches!(
4653 client.voip().accept_group_invite(&incoming).await,
4654 Err(CallError::CallEndedDuringSetup)
4655 ));
4656 assert_eq!(client.call_registry().generation_of(call_id), Some(current));
4657 assert_eq!(
4658 client.call_registry().phase_if_current(call_id, current),
4659 Some(CallPhase::Ringing)
4660 );
4661 }
4662
4663 #[cfg(feature = "voip-runtime")]
4664 #[tokio::test]
4665 async fn group_invite_accept_does_not_consume_a_replacement_generation() {
4666 struct GatedTransport {
4667 started: async_channel::Sender<()>,
4668 release: async_channel::Receiver<()>,
4669 }
4670
4671 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4672 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4673 impl crate::transport::Transport for GatedTransport {
4674 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4675 let _ = self.started.try_send(());
4676 self.release.recv().await?;
4677 Ok(())
4678 }
4679
4680 async fn disconnect(&self) {}
4681 }
4682
4683 let client = crate::test_utils::create_test_client().await;
4684 let creator = call_creator();
4685 let call_id = "ACTIVE-GROUP-INVITE";
4686 let update = GroupCallUpdate::builder()
4687 .call_id(call_id.to_string())
4688 .call_creator(creator.clone())
4689 .transaction_id(1)
4690 .media("audio".to_string())
4691 .connected_limit(32)
4692 .joinable(true)
4693 .av_upgradable(true)
4694 .rekey_requested(false)
4695 .participants(Vec::new())
4696 .build();
4697 let mut incoming = IncomingCall::new_for_test(
4698 creator.clone(),
4699 "GROUP-INVITE-STANZA".to_string(),
4700 wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4701 CallAction::Offer {
4702 call_id: call_id.to_string(),
4703 call_creator: creator.clone(),
4704 caller_pn: None,
4705 caller_country_code: None,
4706 device_class: None,
4707 joinable: true,
4708 is_video: false,
4709 audio: Vec::new(),
4710 group_jid: None,
4711 },
4712 );
4713 incoming.group = Some(Box::new(update.clone()));
4714 let mut ringing = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
4715 ringing.group = Some(update.clone());
4716 let stale = client
4717 .call_registry()
4718 .insert_ringing_group_if_inactive(ringing)
4719 .expect("valid group snapshot")
4720 .expect("ringing invitation");
4721 incoming.set_ringing_generation(stale);
4722
4723 let (started_tx, started_rx) = async_channel::bounded(1);
4724 let (release_tx, release_rx) = async_channel::bounded(1);
4725 let noise_socket = crate::socket::NoiseSocket::new(
4726 Arc::new(crate::runtime_impl::TokioRuntime),
4727 Arc::new(GatedTransport {
4728 started: started_tx,
4729 release: release_rx,
4730 }),
4731 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4732 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4733 );
4734 *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
4735
4736 let accept = tokio::spawn({
4737 let client = client.clone();
4738 let incoming = incoming.clone();
4739 async move { client.voip().accept_group_invite(&incoming).await }
4740 });
4741 started_rx.recv().await.expect("accept send entered");
4742 let mut replacement = CallSession::new_incoming(call_id, creator.clone(), creator);
4743 replacement.group = Some(update);
4744 let current = client.call_registry().insert_ringing_group(replacement);
4745 assert_ne!(current, stale);
4746 release_tx.send(()).await.expect("release accept send");
4747
4748 assert!(matches!(
4749 accept.await.expect("accept task"),
4750 Err(CallError::CallEndedDuringSetup)
4751 ));
4752 assert_eq!(client.call_registry().generation_of(call_id), Some(current));
4753 assert_eq!(
4754 client.call_registry().phase_if_current(call_id, current),
4755 Some(CallPhase::Ringing)
4756 );
4757 assert!(
4758 client.call_registry().take_ringing(call_id),
4759 "the stale accept must leave the replacement ringing"
4760 );
4761 }
4762
4763 #[cfg(feature = "voip-runtime")]
4764 #[tokio::test]
4765 async fn cancelling_call_link_request_removes_response_waiter() {
4766 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4767 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4768 let request_client = client.clone();
4769 let request = tokio::spawn(async move {
4770 request_client
4771 .voip()
4772 .create_call_link(CallLinkMedia::Audio)
4773 .await
4774 });
4775 let node = sent.await.expect("link_create request");
4776 let request_id = node
4777 .as_node_ref()
4778 .attrs()
4779 .optional_string("id")
4780 .expect("request id")
4781 .into_owned();
4782 assert!(
4783 client.response_waiters_guard().contains_key(&request_id),
4784 "the request must register its ACK waiter before sending"
4785 );
4786
4787 request.abort();
4788 assert!(
4789 request
4790 .await
4791 .expect_err("request should be cancelled")
4792 .is_cancelled()
4793 );
4794 tokio::task::yield_now().await;
4795 assert!(
4796 !client.response_waiters_guard().contains_key(&request_id),
4797 "cancelling a call-service request must not leak its waiter"
4798 );
4799 }
4800
4801 #[cfg(feature = "voip-runtime")]
4802 #[tokio::test]
4803 async fn cancelling_registered_call_link_join_removes_its_generation() {
4804 use wacore::handshake::NoiseCipher;
4805
4806 struct BlockingTransport {
4807 started: async_channel::Sender<()>,
4808 }
4809
4810 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4811 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4812 impl crate::transport::Transport for BlockingTransport {
4813 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4814 let _ = self.started.try_send(());
4815 futures::future::pending().await
4816 }
4817
4818 async fn disconnect(&self) {}
4819 }
4820
4821 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4822 client
4823 .persistence_manager()
4824 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4825 Jid::new("111111111111111", Server::Lid).with_device(1),
4826 )))
4827 .await;
4828 let creator = Jid::new("333333333333333", Server::Lid);
4829 let join_sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4830 let join_client = client.clone();
4831 let join = tokio::spawn(async move {
4832 join_client
4833 .voip()
4834 .join_call_link_with_audio(
4835 "CANCELLED-CALL-LINK",
4836 CallLinkMedia::Audio,
4837 AudioFormat::OPUS_16KHZ_60MS,
4838 )
4839 .await
4840 });
4841 let request = join_sent.await.expect("link_join request");
4842 let request_id = request
4843 .as_node_ref()
4844 .attrs()
4845 .optional_string("id")
4846 .expect("request id")
4847 .into_owned();
4848 let (started_tx, started_rx) = async_channel::bounded(1);
4849 let blocking_socket = crate::socket::NoiseSocket::new(
4850 Arc::new(crate::runtime_impl::TokioRuntime),
4851 Arc::new(BlockingTransport {
4852 started: started_tx,
4853 }),
4854 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4855 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4856 );
4857 *client.noise_socket.lock().await = Some(Arc::new(blocking_socket));
4858 crate::test_utils::answer_iq(
4859 &client,
4860 &request_id,
4861 &NodeBuilder::new("ack")
4862 .attr("class", "call")
4863 .attr("type", "link_join")
4864 .attr("id", request_id.as_str())
4865 .children([NodeBuilder::new("waiting_room")
4866 .attr("call-id", "CANCELLED-CALL-ID")
4867 .attr("call-creator", creator)
4868 .attr("link-token", "CANCELLED-CALL-LINK")
4869 .attr("media", "audio")
4870 .attr("enabled", "1")
4871 .attr("is_admin", "0")
4872 .attr("transaction-id", "1")
4873 .build()])
4874 .build(),
4875 )
4876 .await;
4877 started_rx.recv().await.expect("heartbeat send must start");
4878 assert!(
4879 client
4880 .call_registry()
4881 .generation_of("CANCELLED-CALL-ID")
4882 .is_some(),
4883 "the join must register before its heartbeat completes"
4884 );
4885
4886 join.abort();
4887 assert!(
4888 join.await
4889 .expect_err("join should be cancelled")
4890 .is_cancelled()
4891 );
4892 tokio::task::yield_now().await;
4893 assert_eq!(
4894 client.call_registry().generation_of("CANCELLED-CALL-ID"),
4895 None,
4896 "cancelling after registration must reap only that generation"
4897 );
4898 }
4899
4900 #[cfg(feature = "voip-runtime")]
4901 #[tokio::test]
4902 async fn cancelling_an_admitted_call_link_registration_sends_terminate() {
4903 let (client, sends) = make_client_with_count().await;
4904 let call_id = "CANCELLED-ADMITTED-CALL";
4905 let creator = Jid::new("333333333333333", Server::Lid);
4906 let mut session =
4907 CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
4908 let _ = session.transition_to(CallPhase::Calling);
4909 let _ = session.transition_to(CallPhase::Connecting);
4910 let registry = client.call_registry();
4911 let generation = registry.insert(session);
4912 let registration = super::CallLinkRegistrationGuard::new(
4913 &client,
4914 registry.clone(),
4915 call_id,
4916 creator,
4917 generation,
4918 );
4919
4920 drop(registration);
4921
4922 tokio::time::timeout(Duration::from_secs(2), async {
4923 while sends.load(Ordering::SeqCst) == 0 {
4924 tokio::task::yield_now().await;
4925 }
4926 })
4927 .await
4928 .expect("admitted cancellation must send a call-scoped terminate");
4929 assert_eq!(sends.load(Ordering::SeqCst), 1);
4930 assert_eq!(registry.generation_of(call_id), None);
4931 }
4932
4933 #[cfg(feature = "voip-runtime")]
4934 #[tokio::test]
4935 async fn call_link_join_reports_admission_committed_during_heartbeat() {
4936 use wacore::handshake::NoiseCipher;
4937
4938 struct GatedTransport {
4939 started: async_channel::Sender<()>,
4940 release: async_channel::Receiver<()>,
4941 }
4942
4943 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4944 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4945 impl crate::transport::Transport for GatedTransport {
4946 async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4947 self.started
4948 .send(())
4949 .await
4950 .map_err(|_| anyhow::anyhow!("heartbeat observer closed"))?;
4951 self.release
4952 .recv()
4953 .await
4954 .map_err(|_| anyhow::anyhow!("heartbeat gate closed"))?;
4955 Ok(())
4956 }
4957
4958 async fn disconnect(&self) {}
4959 }
4960
4961 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4962 let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
4963 client
4964 .persistence_manager()
4965 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4966 own_lid.clone(),
4967 )))
4968 .await;
4969 let creator = Jid::new("333333333333333", Server::Lid);
4970 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4971 let join_client = client.clone();
4972 let join = tokio::spawn(async move {
4973 join_client
4974 .voip()
4975 .join_call_link_registration_with_audio(
4976 "ADMISSION-RACE-LINK",
4977 CallLinkMedia::Audio,
4978 AudioFormat::OPUS_16KHZ_60MS,
4979 )
4980 .await
4981 });
4982 let request = sent.await.expect("link_join request");
4983 let request_id = request
4984 .as_node_ref()
4985 .attrs()
4986 .optional_string("id")
4987 .expect("request id")
4988 .into_owned();
4989 let (started_tx, started_rx) = async_channel::bounded(1);
4990 let (release_tx, release_rx) = async_channel::bounded(1);
4991 let gated_socket = crate::socket::NoiseSocket::new(
4992 Arc::new(crate::runtime_impl::TokioRuntime),
4993 Arc::new(GatedTransport {
4994 started: started_tx,
4995 release: release_rx,
4996 }),
4997 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4998 NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4999 );
5000 *client.noise_socket.lock().await = Some(Arc::new(gated_socket));
5001 crate::test_utils::answer_iq(
5002 &client,
5003 &request_id,
5004 &NodeBuilder::new("ack")
5005 .attr("class", "call")
5006 .attr("type", "link_join")
5007 .attr("id", request_id.as_str())
5008 .children([NodeBuilder::new("waiting_room")
5009 .attr("call-id", "ADMISSION-RACE-CALL")
5010 .attr("call-creator", creator.clone())
5011 .attr("link-token", "ADMISSION-RACE-LINK")
5012 .attr("media", "audio")
5013 .attr("enabled", "1")
5014 .attr("is_admin", "0")
5015 .attr("transaction-id", "1")
5016 .build()])
5017 .build(),
5018 )
5019 .await;
5020 started_rx.recv().await.expect("heartbeat send started");
5021
5022 let registry = client.call_registry();
5023 let generation = registry
5024 .generation_of("ADMISSION-RACE-CALL")
5025 .expect("registered waiting-room generation");
5026 let mut participant =
5027 GroupCallParticipant::new(own_lid.to_non_ad(), vec![GroupCallDevice::new(own_lid)]);
5028 participant.state = Some("connected".to_string());
5029 let admitted = GroupCallUpdate::builder()
5030 .call_id("ADMISSION-RACE-CALL".to_string())
5031 .call_creator(creator)
5032 .transaction_id(2)
5033 .media("audio".to_string())
5034 .connected_limit(32)
5035 .joinable(true)
5036 .av_upgradable(true)
5037 .rekey_requested(false)
5038 .participants(vec![participant])
5039 .build();
5040 let transition_lock = registry
5041 .group_transition_lock("ADMISSION-RACE-CALL", generation)
5042 .expect("group transition lane");
5043 let transition_guard = transition_lock.lock().await;
5044 assert_eq!(
5045 registry.apply_group_update_if_current(admitted, generation),
5046 wacore::voip::GroupStateApply::Applied
5047 );
5048 assert_eq!(
5049 registry.phase_if_current("ADMISSION-RACE-CALL", generation),
5050 Some(CallPhase::Connecting)
5051 );
5052 drop(transition_guard);
5053 release_tx.send(()).await.expect("release heartbeat send");
5054
5055 let registration = join.await.expect("join task").expect("join response");
5056 assert_eq!(registration.generation, generation);
5057 assert!(!registration.join.in_waiting_room);
5058 assert_eq!(
5059 registration
5060 .join
5061 .group
5062 .as_ref()
5063 .map(|update| update.transaction_id),
5064 Some(2),
5065 "the public result must report admission committed during the heartbeat"
5066 );
5067 registry.remove_if_current("ADMISSION-RACE-CALL", generation);
5068 }
5069
5070 #[cfg(feature = "voip-runtime")]
5071 #[tokio::test]
5072 async fn immediately_admitted_call_link_preserves_token_and_origin_generation() {
5073 let (client, _transport) = crate::test_utils::create_iq_test_client().await;
5074 client
5075 .persistence_manager()
5076 .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
5077 Jid::new("111111111111111", Server::Lid).with_device(1),
5078 )))
5079 .await;
5080 let creator = Jid::new("333333333333333", Server::Lid);
5081 let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
5082 let join_client = client.clone();
5083 let join = tokio::spawn(async move {
5084 join_client
5085 .voip()
5086 .join_call_link_registration_with_audio(
5087 "REQUESTED-CALL-LINK",
5088 CallLinkMedia::Video,
5089 AudioFormat::OPUS_16KHZ_60MS,
5090 )
5091 .await
5092 });
5093 let request = sent.await.expect("link_join request");
5094 let request_id = request
5095 .as_node_ref()
5096 .attrs()
5097 .optional_string("id")
5098 .expect("request id")
5099 .into_owned();
5100 crate::test_utils::answer_iq(
5101 &client,
5102 &request_id,
5103 &NodeBuilder::new("ack")
5104 .attr("class", "call")
5105 .attr("type", "link_join")
5106 .attr("id", request_id.as_str())
5107 .children([
5108 NodeBuilder::new("waiting_room")
5109 .attr("call-id", "ADMITTED-CALL-ID")
5110 .attr("call-creator", creator.clone())
5111 .attr("link-token", "REQUESTED-CALL-LINK")
5112 .attr("media", "video")
5113 .attr("enabled", "1")
5114 .attr("is_admin", "1")
5115 .attr("transaction-id", "1")
5116 .build(),
5117 NodeBuilder::new("group_info")
5118 .attr("call-id", "ADMITTED-CALL-ID")
5119 .attr("call-creator", creator)
5120 .attr("transaction-id", "1")
5121 .attr("connected-limit", "32")
5122 .attr("media", "video")
5123 .build(),
5124 ])
5125 .build(),
5126 )
5127 .await;
5128
5129 let admitted_registration = join.await.expect("join task").expect("join response");
5130 let admitted = admitted_registration.join;
5131 assert_eq!(admitted.token, "REQUESTED-CALL-LINK");
5132 assert!(!admitted.in_waiting_room);
5133 let generation = client
5134 .call_registry()
5135 .generation_of("ADMITTED-CALL-ID")
5136 .expect("registered admitted call");
5137 assert_eq!(
5138 admitted_registration.generation, generation,
5139 "the join result must retain the generation it created"
5140 );
5141 assert!(
5142 client
5143 .call_registry()
5144 .group_state("ADMITTED-CALL-ID")
5145 .and_then(|state| state.waiting_room().cloned())
5146 .is_some_and(|room| room.is_admin && room.enabled),
5147 "admitted joins must retain waiting-room admin state from the ACK"
5148 );
5149 let replacement = client.call_registry().insert(CallSession::new_outgoing(
5150 "ADMITTED-CALL-ID",
5151 Jid::new("ADMITTED-CALL-ID", Server::Call),
5152 Jid::new("333333333333333", Server::Lid),
5153 ));
5154 assert_ne!(replacement, admitted_registration.generation);
5155 assert!(
5156 client
5157 .call_registry()
5158 .snapshot_if_current("ADMITTED-CALL-ID", admitted_registration.generation)
5159 .is_none(),
5160 "a stale starter must not attach through a replacement generation"
5161 );
5162 client
5163 .call_registry()
5164 .remove_if_current("ADMITTED-CALL-ID", replacement);
5165 }
5166}