1use super::*;
5use crate::calls::incoming::{CancelNotification, InviteNotification};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum IncomingPhase {
9 Invited,
10 Acknowledged,
11 Joining,
12 Joined,
13 Ending,
14 TerminalPending,
15 Uncertain,
16 Finished,
17}
18struct Invite {
19 room: VoiceRoom,
20 caller: String,
21 caller_openid: Vec<u8>,
22 callee_openid: Vec<u8>,
23 caller_member: Option<u16>,
24}
25
26pub struct NativeIncomingSession {
30 base: NativePersonalSession,
31 invite: Invite,
32 phase: IncomingPhase,
33 ack_spent: bool,
34 join_spent: bool,
35 reject_spent: bool,
36}
37impl NativeIncomingSession {
38 pub fn peer_microphone_muted(&self) -> Option<bool> {
40 (self.phase == IncomingPhase::Joined)
41 .then(|| self.base.peer_microphone_muted())
42 .flatten()
43 }
44 #[allow(clippy::too_many_arguments)]
47 pub fn authenticate(
48 client: &impl PersonalTransport,
49 state: &mut AccountState,
50 authority: AppAuthority,
51 host_task: u32,
52 device32: [u8; 32],
53 mut parameters: VoiceParameters,
54 notification: &InviteNotification,
55 expected_caller: &str,
56 ) -> Result<Self> {
57 ensure!(
58 notification.unknown_fields.is_empty(),
59 "Unknown incoming invitation fields"
60 );
61 ensure!(
62 notification.room_type == Some(2)
63 && notification.app_id.as_deref() == Some("wechat")
64 && notification.sub_app_id.as_deref() == Some("voip-2p"),
65 "Unsupported incoming call family"
66 );
67 ensure!(
68 notification.callee_username.as_deref() == Some(state.own_username.as_str())
69 && notification.caller_username.as_deref() == Some(expected_caller)
70 && expected_caller != state.own_username
71 && !expected_caller.is_empty()
72 && expected_caller.len() <= 128,
73 "Incoming account or caller mismatch"
74 );
75 ensure!(
76 notification.usernames.len() <= 2
77 && notification
78 .usernames
79 .iter()
80 .all(|name| name == expected_caller || name == &state.own_username),
81 "Unsupported incoming participants"
82 );
83 let room = VoiceRoom {
84 room: notification
85 .room_id
86 .ok_or_else(|| anyhow::anyhow!("Missing incoming room"))?,
87 room_key: notification
88 .room_key
89 .ok_or_else(|| anyhow::anyhow!("Missing incoming room key"))?,
90 own_member: notification
91 .member_id
92 .ok_or_else(|| anyhow::anyhow!("Missing incoming member"))?,
93 };
94 ensure!(
95 room.room != 0 && room.room_key != 0 && u16::try_from(room.own_member).is_ok(),
96 "Invalid incoming room identity"
97 );
98 let caller_openid = notification
99 .caller_openid
100 .as_deref()
101 .ok_or_else(|| anyhow::anyhow!("Missing incoming caller identity"))?
102 .as_bytes()
103 .to_vec();
104 let callee_openid = notification
105 .callee_openid
106 .as_deref()
107 .ok_or_else(|| anyhow::anyhow!("Missing incoming recipient identity"))?
108 .as_bytes()
109 .to_vec();
110 ensure!(
111 !caller_openid.is_empty()
112 && caller_openid.len() <= 128
113 && !callee_openid.is_empty()
114 && callee_openid.len() <= 128
115 && caller_openid != callee_openid,
116 "Invalid incoming app identities"
117 );
118 let owner = notification.wechat_group_id.as_deref().unwrap_or("");
119 ensure!(owner.len() <= 256, "Incoming owner group too large");
120 let group = fields(¶meters.group)?;
121 ensure!(
122 number(&group, 5, None)? == 1
123 && number(&group, 2, None)? == 0
124 && data(&group, 4)?.is_empty(),
125 "Incoming session requires fresh selected group mode"
126 );
127 parameters.group = bind_owner_group(¶meters.group, owner.as_bytes())?;
128 let caller_member = notification
129 .caller_member_id
130 .map(u16::try_from)
131 .transpose()?;
132 if let Some(member) = caller_member {
133 ensure!(
134 u32::from(member) != room.own_member,
135 "Incoming member collision"
136 );
137 }
138 let mut base = NativePersonalSession::authenticate(
139 client, state, authority, host_task, device32, parameters,
140 )?;
141 base.control = Some(PersonalControl {
142 identity: room,
143 peer: expected_caller.to_owned(),
144 origin: ControlOrigin::Incoming,
145 peer_joined: None,
146 terminal: None,
147 invitation_pending: false,
148 pending_recipients: vec![],
149 });
150 base.phase = PersonalPhase::Ringing;
151 Ok(Self {
152 base,
153 invite: Invite {
154 room,
155 caller: expected_caller.to_owned(),
156 caller_openid,
157 callee_openid,
158 caller_member,
159 },
160 phase: IncomingPhase::Invited,
161 ack_spent: false,
162 join_spent: false,
163 reject_spent: false,
164 })
165 }
166 pub fn phase(&self) -> IncomingPhase {
167 self.phase
168 }
169 pub fn room(&self) -> VoiceRoom {
172 self.invite.room
173 }
174 pub fn ringing_ack(
177 &mut self,
178 client: &impl PersonalTransport,
179 state: &mut AccountState,
180 current: AppAuthority,
181 tasks: TaskIds,
182 ) -> Result<()> {
183 self.base.check(state, current)?;
184 ensure!(
185 self.phase == IncomingPhase::Invited && !self.ack_spent,
186 "Incoming ACK already spent"
187 );
188 self.base.reserve(tasks)?;
189 self.ack_spent = true;
190 self.phase = IncomingPhase::Uncertain;
191 let result = self.ringing_ack_once(client, state, tasks);
192 if result
193 .as_ref()
194 .is_err_and(|error| error.is::<CallRpcNotSubmitted>())
195 {
196 self.phase = IncomingPhase::Invited;
199 }
200 result
201 }
202 fn ringing_ack_once(
203 &mut self,
204 client: &impl PersonalTransport,
205 state: &mut AccountState,
206 tasks: TaskIds,
207 ) -> Result<()> {
208 let request = personal::acknowledge_voice(
209 &self.base.parameters.inputs(),
210 self.invite.room,
211 tasks.sdk,
212 )?;
213 let body = self.base.host(
214 client,
215 state,
216 CallHostRoute::RingingAck,
217 request.body(),
218 tasks.host,
219 )?;
220 let outer = fields(&body)?;
221 only(&outer, &[1, 2])?;
222 ensure!(
223 p::optional_data(&outer, 2)?.is_none_or(|auth| auth.is_empty()),
224 "Unexpected incoming auth rotation"
225 );
226 let ack = fields(data(&outer, 1)?)?;
227 only(&ack, &[1, 2, 3, 4, 5, 6, 7, 8, 10])?;
228 ensure!(
229 number(&ack, 1, None)? == self.invite.room.room
230 && number(&ack, 2, None)? == self.invite.room.room_key,
231 "Incoming ACK room mismatch"
232 );
233 let member = u16::try_from(number(&ack, 3, None)?)?;
234 let live_id = number(&ack, 4, Some(0))?;
235 for tag in [5, 6, 7, 8, 10] {
236 p::optional_data(&ack, tag)?;
237 }
238 self.invite.room.own_member = u32::from(member);
239 self.base
240 .control
241 .as_mut()
242 .expect("incoming control")
243 .identity = self.invite.room;
244 let group = fields(&self.base.parameters.group)?;
245 let cloud_group = std::str::from_utf8(data(&group, 4)?)?;
246 self.base.parameters.group =
247 replace_group(&self.base.parameters.group, live_id, cloud_group)?;
248 let (sequence, members) = if let Some(raw) = p::optional_data(&ack, 6)? {
250 let list = fields(raw)?;
251 only(&list, &[1, 2, 3])?;
252 p::optional_data(&list, 3)?;
253 let sequence = u32::try_from(number(&list, 1, Some(0))?)?;
254 let members = if sequence == 0 {
255 BTreeMap::new()
256 } else {
257 parse_roster_entries(
258 repeated(&list, 2, 16)?,
259 &self.base.own_username,
260 &self.invite.caller,
261 )?
262 };
263 (sequence, members)
264 } else {
265 (0, BTreeMap::new())
266 };
267 for (name, entry) in &members {
268 let expected = if name == &self.base.own_username {
269 &self.invite.callee_openid
270 } else {
271 &self.invite.caller_openid
272 };
273 ensure!(
274 &entry.openid == expected,
275 "Incoming ACK app identity mismatch"
276 );
277 if name == &self.base.own_username {
278 ensure!(entry.member == member, "Incoming ACK own member mismatch");
279 } else if let Some(expected) = self.invite.caller_member {
280 ensure!(
281 entry.member == expected,
282 "Incoming ACK caller member mismatch"
283 );
284 }
285 }
286 if sequence != 0 {
287 if let Some(action) = termination::classify(
288 termination::Origin::Callee,
289 3,
290 members
291 .get(&self.base.own_username)
292 .map(|entry| entry.status),
293 members.get(&self.invite.caller).map(|entry| entry.status),
294 false,
295 )? {
296 ensure!(
297 matches!(action, RemoteTermination::LocalStop(_)),
298 "Unsupported incoming ACK terminal operation"
299 );
300 self.base
301 .control
302 .as_mut()
303 .expect("incoming control")
304 .terminal = Some(action);
305 self.base.phase = PersonalPhase::Finished;
306 self.phase = IncomingPhase::Finished;
307 return Ok(());
308 }
309 }
310 if self.invite.caller_member.is_none() {
311 self.invite.caller_member = members.get(&self.invite.caller).map(|entry| entry.member);
312 }
313 ensure!(
314 self.invite
315 .caller_member
316 .is_none_or(|caller| caller != member)
317 && live_id != 0,
318 "Incomplete incoming ACK control context"
319 );
320 let strategy = p::optional_data(&ack, 7)?.unwrap_or_default();
321 ensure!(
322 number(&fields(strategy)?, 14, Some(0))? & (1 << 36) == 0,
323 "Alternate incoming sync unsupported"
324 );
325 let mut original_members = BTreeMap::from([(
328 self.base.own_username.clone(),
329 Member {
330 member,
331 status: 0,
332 openid: self.invite.callee_openid.clone(),
333 },
334 )]);
335 if let Some(caller_member) = self.invite.caller_member {
336 original_members.insert(
337 self.invite.caller.clone(),
338 Member {
339 member: caller_member,
340 status: 0,
341 openid: self.invite.caller_openid.clone(),
342 },
343 );
344 }
345 self.base.active = Some(ActiveRoom {
346 identity: self.invite.room,
347 peer: self.invite.caller.clone(),
348 live_id,
349 cookies: p::optional_data(&ack, 5)?.unwrap_or_default().to_vec(),
350 strategy: Zeroizing::new(strategy.to_vec()),
351 members,
352 original_members,
353 unbound_peer_openid: self
354 .invite
355 .caller_member
356 .is_none()
357 .then(|| self.invite.caller_openid.clone()),
358 member_sequence: sequence,
359 cap_sequence: 0,
360 engine: vec![],
361 channel: vec![],
362 av_sequence: 0,
363 av: BTreeMap::new(),
364 media_taken: false,
365 });
366 self.base.phase = PersonalPhase::Ringing;
367 self.phase = IncomingPhase::Acknowledged;
368 Ok(())
369 }
370 pub fn accept(
373 &mut self,
374 client: &impl PersonalTransport,
375 state: &mut AccountState,
376 current: AppAuthority,
377 tasks: TaskIds,
378 ) -> Result<()> {
379 self.base.check(state, current)?;
380 ensure!(
381 self.phase == IncomingPhase::Acknowledged && !self.join_spent,
382 "Incoming call not acknowledged or Join spent"
383 );
384 self.base.reserve(tasks)?;
385 self.join_spent = true;
386 self.phase = IncomingPhase::Joining;
387 let result = self.accept_once(client, state, current, tasks);
388 if let Err(error) = &result {
389 if error.is::<CallRpcNotSubmitted>() {
390 self.phase = IncomingPhase::Acknowledged;
391 self.base.phase = PersonalPhase::Ringing;
392 } else if !error.is::<UnsupportedCallMedia>() {
393 self.phase = IncomingPhase::Uncertain;
394 self.base.phase = PersonalPhase::Uncertain;
395 }
396 }
397 result
398 }
399 fn accept_once(
400 &mut self,
401 client: &impl PersonalTransport,
402 state: &mut AccountState,
403 current: AppAuthority,
404 tasks: TaskIds,
405 ) -> Result<()> {
406 let request = personal::join(
407 &self.base.parameters.inputs(),
408 self.invite.room,
409 0,
410 tasks.sdk,
411 )?;
412 let raw = self.transfer(
413 client,
414 state,
415 current,
416 CallOperation::Join,
417 request.body.as_deref().expect("prepared Join"),
418 tasks.host,
419 )?;
420 let joined = fields(&raw)?;
421 only(
422 &joined,
423 &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18],
424 )?;
425 ensure!(
426 number(&joined, 1, None)? == self.invite.room.room
427 && number(&joined, 2, None)? == self.invite.room.room_key,
428 "Incoming Join room mismatch"
429 );
430 ensure!(
431 matches!(number(&joined, 8, Some(2))?, 2) && number(&joined, 18, Some(0))? == 0,
432 "Unsupported incoming Join mode"
433 );
434 if let Some(value) = p::optional(&joined, 5)? {
436 ensure!(
437 matches!(value, Value::Varint(_)),
438 "Invalid incoming opaque field"
439 );
440 }
441 repeated(&joined, 6, 16)?;
442 let own_member = u16::try_from(number(&joined, 3, None)?)?;
443 let room = VoiceRoom {
444 room: self.invite.room.room,
445 room_key: self.invite.room.room_key,
446 own_member: u32::from(own_member),
447 };
448 let raw_members = data(&joined, 13)?;
449 let members = roster(
450 raw_members,
451 &self.base.own_username,
452 &self.invite.caller,
453 own_member,
454 )?;
455 ensure!(
456 members[&self.invite.caller].status == 1
457 && members[&self.invite.caller].openid == self.invite.caller_openid
458 && members[&self.base.own_username].openid == self.invite.callee_openid
459 && self
460 .invite
461 .caller_member
462 .is_none_or(|member| member == members[&self.invite.caller].member),
463 "Incoming Join participant mismatch"
464 );
465 let control = self.base.control.as_mut().expect("incoming control");
466 control.identity = room;
467 control.peer_joined = Some(true);
468 self.invite.room = room;
469 self.base.phase = PersonalPhase::Joined;
470 self.phase = IncomingPhase::Joined;
471 let strategy = data(&joined, 4)?;
472 let parsed_strategy = fields(strategy)?;
473 ensure!(
474 number(&parsed_strategy, 14, None)? & (1 << 36) == 0,
475 "Alternate incoming sync unsupported"
476 );
477 let live_id = number(&joined, 11, None)?;
478 ensure!(live_id != 0, "Missing incoming live identity");
479 let old_group = fields(&self.base.parameters.group)?;
480 let cloud_group = std::str::from_utf8(data(&old_group, 4)?)?;
481 self.base.parameters.group =
482 replace_group(&self.base.parameters.group, live_id, cloud_group)?;
483 let (cap_sequence, engine, channel) = cap(data(&parsed_strategy, 13)?)?;
484 let (av_sequence, av) = av(
485 data(&joined, 9)?,
486 own_member,
487 members[&self.invite.caller].member,
488 )?;
489 let member_sequence = u32::try_from(number(&fields(raw_members)?, 1, Some(0))?)?;
490 if let Some(acked) = self.base.active.as_ref() {
491 ensure!(
492 member_sequence >= acked.member_sequence,
493 "Incoming Join roster sequence regressed"
494 );
495 if member_sequence == acked.member_sequence && member_sequence != 0 {
496 ensure!(
497 members
498 .iter()
499 .all(|(name, entry)| acked
500 .members
501 .get(name)
502 .is_some_and(|old| old.member == entry.member
503 && old.openid == entry.openid
504 && old.status == entry.status)),
505 "Incoming Join changed a stale roster"
506 );
507 }
508 }
509
510 self.base.active = Some(ActiveRoom {
511 identity: room,
512 peer: self.invite.caller.clone(),
513 live_id,
514 cookies: data(&joined, 12)?.to_vec(),
515 strategy: Zeroizing::new(strategy.to_vec()),
516 original_members: members.clone(),
517 unbound_peer_openid: None,
518 members,
519 member_sequence,
520 cap_sequence,
521 engine,
522 channel,
523 av_sequence,
524 av,
525 media_taken: false,
526 });
527 let _ = self.base.media_ready(state, current)?;
528 Ok(())
529 }
530 fn transfer(
531 &mut self,
532 client: &impl PersonalTransport,
533 state: &mut AccountState,
534 current: AppAuthority,
535 operation: CallOperation,
536 inner: &[u8],
537 host_task: u32,
538 ) -> Result<Vec<u8>> {
539 let parameters = AppFrameParameters {
540 sdk_version: self.base.parameters.sdk_version()?,
541 cloud_device_id: &self.base.parameters.cloud_device_id,
542 host_client_version: state.device.client_version as u32,
543 app_config_version: self.base.parameters.app_config_version,
544 };
545 let body =
546 app_frame::prepare_transfer(&self.base.app, current, operation, inner, ¶meters)?;
547 let reply = self
548 .base
549 .host(client, state, CallHostRoute::Transfer, &body, host_task)?;
550 app_frame::decode_transfer_reply(&self.base.app, current, operation, &reply)
551 }
552 pub fn reject(
555 &mut self,
556 client: &impl PersonalTransport,
557 state: &mut AccountState,
558 current: AppAuthority,
559 tasks: TaskIds,
560 ) -> Result<()> {
561 self.base.check(state, current)?;
562 ensure!(
563 self.phase == IncomingPhase::Acknowledged && !self.reject_spent,
564 "Incoming rejection unavailable"
565 );
566 self.base.reserve(tasks)?;
567 self.reject_spent = true;
568 self.phase = IncomingPhase::Ending;
569 let result: Result<()> = (|| {
570 let request =
571 personal::reject(&self.base.parameters.inputs(), self.invite.room, tasks.sdk)?;
572 let raw = self.transfer(
573 client,
574 state,
575 current,
576 CallOperation::Reject,
577 request.body.as_deref().expect("prepared Reject"),
578 tasks.host,
579 )?;
580 let reply = fields(&raw)?;
581 only(&reply, &[1, 2, 3])?;
582 if let Some(members) = p::optional_data(&reply, 3)? {
583 fields(members)?;
584 }
585 ensure!(
586 number(&reply, 1, None)? == self.invite.room.room
587 && number(&reply, 2, None)? == self.invite.room.room_key,
588 "Incoming Reject room mismatch"
589 );
590 Ok(())
591 })();
592 let unsent = result
593 .as_ref()
594 .is_err_and(|error| error.is::<CallRpcNotSubmitted>());
595 self.phase = if result.is_ok() {
596 IncomingPhase::Finished
597 } else if unsent {
598 IncomingPhase::Acknowledged
599 } else {
600 IncomingPhase::Uncertain
601 };
602 self.base.phase = if result.is_ok() {
603 PersonalPhase::Finished
604 } else if unsent {
605 PersonalPhase::Ringing
606 } else {
607 PersonalPhase::Uncertain
608 };
609 result
610 }
611 pub fn poll(
612 &mut self,
613 client: &impl PersonalTransport,
614 state: &mut AccountState,
615 current: AppAuthority,
616 tasks: TaskIds,
617 ) -> Result<SyncProgress> {
618 ensure!(
619 matches!(
620 self.phase,
621 IncomingPhase::Acknowledged | IncomingPhase::Joined
622 ),
623 "Incoming sync unavailable"
624 );
625 let result = self.base.poll(client, state, current, tasks);
626 if result.is_ok() && self.invite.caller_member.is_none() {
627 self.invite.caller_member = self
628 .base
629 .active
630 .as_ref()
631 .and_then(|active| active.original_members.get(&self.invite.caller))
632 .map(|member| member.member);
633 }
634 if self.base.phase == PersonalPhase::Uncertain {
635 self.phase = IncomingPhase::Uncertain;
636 }
637 if let Ok(SyncProgress::Terminal(action)) = result.as_ref() {
638 self.phase = if matches!(action, RemoteTermination::LocalStop(_)) {
639 IncomingPhase::Finished
640 } else {
641 IncomingPhase::TerminalPending
642 };
643 }
644 result
645 }
646 pub fn finish_remote(
648 &mut self,
649 client: &impl PersonalTransport,
650 state: &mut AccountState,
651 current: AppAuthority,
652 tasks: TaskIds,
653 talk_success: bool,
654 ) -> Result<()> {
655 ensure!(
656 self.phase == IncomingPhase::TerminalPending,
657 "No incoming remote teardown"
658 );
659 self.phase = IncomingPhase::Ending;
660 let result = self
661 .base
662 .finish_remote(client, state, current, tasks, talk_success);
663 self.phase = if result.is_ok() {
664 IncomingPhase::Finished
665 } else {
666 IncomingPhase::Uncertain
667 };
668 result
669 }
670 pub fn media_ready(&self, state: &AccountState, current: AppAuthority) -> Result<bool> {
671 ensure!(
672 self.phase == IncomingPhase::Joined,
673 "Incoming media unavailable"
674 );
675 self.base.media_ready(state, current)
676 }
677 pub fn take_media(
678 &mut self,
679 state: &AccountState,
680 current: AppAuthority,
681 connection_index: usize,
682 udp_index: usize,
683 ) -> Result<NegotiatedMedia> {
684 ensure!(
685 self.phase == IncomingPhase::Joined,
686 "Incoming media unavailable"
687 );
688 self.base
689 .take_media(state, current, connection_index, udp_index)
690 }
691 pub fn exit(
692 &mut self,
693 client: &impl PersonalTransport,
694 state: &mut AccountState,
695 current: AppAuthority,
696 tasks: TaskIds,
697 talk_success: bool,
698 ) -> Result<()> {
699 ensure!(
700 self.phase == IncomingPhase::Joined,
701 "Incoming exit unavailable"
702 );
703 self.phase = IncomingPhase::Ending;
704 let result = self.base.exit(client, state, current, tasks, talk_success);
705 self.phase = if result.is_ok() {
706 IncomingPhase::Finished
707 } else {
708 IncomingPhase::Uncertain
709 };
710 result
711 }
712 pub fn retire_cancel(
715 &mut self,
716 state: &AccountState,
717 current: AppAuthority,
718 cancel: &CancelNotification,
719 ) -> Result<bool> {
720 self.base.check(state, current)?;
721 if !matches!(
722 self.phase,
723 IncomingPhase::Invited | IncomingPhase::Acknowledged
724 ) {
725 return Ok(false);
726 }
727 ensure!(
728 cancel.unknown_fields.is_empty(),
729 "Unknown incoming cancellation fields"
730 );
731 if cancel.room_id != Some(self.invite.room.room)
732 || cancel.room_key != Some(self.invite.room.room_key)
733 || cancel.member_id != Some(self.invite.room.own_member)
734 {
735 return Ok(false);
736 }
737 self.phase = IncomingPhase::Finished;
738 self.base.phase = PersonalPhase::Finished;
739 Ok(true)
740 }
741}
742fn bind_owner_group(raw: &[u8], owner: &[u8]) -> Result<Vec<u8>> {
743 let values = fields(raw)?;
744 let old = p::optional_data(&values, 6)?.unwrap_or_default();
745 ensure!(
746 old.is_empty() || old == owner,
747 "Incoming owner-group mismatch"
748 );
749 let mut encoded = Vec::new();
750 for field in values {
751 if field.number == 6 {
752 continue;
753 }
754 encoded.push(match field.value {
755 Value::Varint(value) => p::uint(field.number, value),
756 Value::Bytes(value) => p::bytes(field.number, &value),
757 _ => bail!("Unsupported incoming group wire type"),
758 });
759 }
760 encoded.push(p::bytes(6, owner));
761 p::message(&encoded)
762}
763
764#[cfg(test)]
765mod tests {
766 use super::super::tests::{
767 auth_reply, authority, av_status, capabilities_for_codec, exit_reply, host_reply, member,
768 parameters, state, FakeTransport,
769 };
770 use super::*;
771 fn notification() -> InviteNotification {
772 InviteNotification::decode(
773 &p::message(&[
774 p::uint(1, 7),
775 p::uint(2, 9),
776 p::uint(3, 0),
777 p::uint(4, 2),
778 p::bytes(5, b"peer"),
779 p::bytes(6, b"own"),
780 p::bytes(7, b"peer"),
781 p::bytes(8, b"own"),
782 p::bytes(9, b"own"),
783 p::bytes(9, b"peer"),
784 p::bytes(10, b"wechat"),
785 p::bytes(11, b"voip-2p"),
786 p::uint(12, 1),
787 p::bytes(13, b"owner-group"),
788 ])
789 .unwrap(),
790 )
791 .unwrap()
792 }
793 fn ack_reply() -> Vec<u8> {
794 host_reply(&p::bytes(
795 1,
796 &p::message(&[
797 p::uint(1, 7),
798 p::uint(2, 9),
799 p::uint(3, 0),
800 p::uint(4, 99),
801 p::bytes(5, b"ack-cursor"),
802 p::bytes(
803 6,
804 &p::message(&[
805 p::uint(1, 1),
806 p::bytes(2, &member("own", 0, 3)),
807 p::bytes(2, &member("peer", 1, 1)),
808 ])
809 .unwrap(),
810 ),
811 p::bytes(7, &p::uint(14, 0)),
812 ])
813 .unwrap(),
814 ))
815 }
816 fn wrapped_reply(command: u32, body: &[u8]) -> Vec<u8> {
817 let frame = app_frame::TaskFrame {
818 command,
819 integers: BTreeMap::from([
820 (4, 0),
821 (7, 0),
822 (8, 0),
823 (21, 0),
824 (22, 17),
825 (23, 0),
826 (25, 0),
827 (26, 0),
828 ]),
829 strings: BTreeMap::new(),
830 payload: p::message(&[p::bytes(1, &[8, 0]), p::uint(2, 17), p::bytes(3, body)])
831 .unwrap(),
832 }
833 .encode()
834 .unwrap();
835 host_reply(&p::message(&[p::uint(1, 0), p::bytes(2, &frame)]).unwrap())
836 }
837 fn join_reply(codec: u64, forged_openid: bool) -> Vec<u8> {
838 let (engine, channel) = capabilities_for_codec(codec);
839 let cap = p::message(&[
840 p::uint(1, 1),
841 p::bytes(
842 2,
843 &p::message(&[p::bytes(1, &engine), p::bytes(2, &channel)]).unwrap(),
844 ),
845 ])
846 .unwrap();
847 let address = p::message(&[p::bytes(1, b"127.0.0.1"), p::uint(2, 6000)]).unwrap();
848 let strategy = p::message(&[
849 p::bytes(10, &[7; 32]),
850 p::bytes(13, &cap),
851 p::uint(14, 0),
852 p::bytes(
853 15,
854 &p::message(&[p::uint(1, 1), p::bytes(4, &address)]).unwrap(),
855 ),
856 ])
857 .unwrap();
858 let mut peer = member("peer", 1, 1);
859 if forged_openid {
860 let mut values = fields(&peer).unwrap();
861 values.retain(|field| field.number != 1);
862 peer = p::message(
863 &values
864 .into_iter()
865 .map(|field| match field.value {
866 Value::Varint(value) => p::uint(field.number, value),
867 Value::Bytes(value) => p::bytes(field.number, &value),
868 _ => unreachable!(),
869 })
870 .chain([p::bytes(1, b"wrong")])
871 .collect::<Vec<_>>(),
872 )
873 .unwrap();
874 }
875 let members = p::message(&[
876 p::uint(1, 2),
877 p::bytes(2, &member("own", 0, 1)),
878 p::bytes(2, &peer),
879 ])
880 .unwrap();
881 wrapped_reply(
884 3361,
885 &p::message(&[
886 p::uint(1, 7),
887 p::uint(2, 9),
888 p::uint(3, 0),
889 p::bytes(4, &strategy),
890 p::bytes(6, b"opaque-one"),
891 p::bytes(6, b"opaque-two"),
892 p::uint(5, 0),
893 p::bytes(6, &[]),
894 p::uint(8, 2),
895 p::bytes(9, &av_status(1, 1)),
896 p::uint(11, 99),
897 p::bytes(12, b"joined-cursor"),
898 p::bytes(13, &members),
899 ])
900 .unwrap(),
901 )
902 }
903 fn transport(replies: Vec<(CallHostRoute, Vec<u8>)>) -> FakeTransport {
904 FakeTransport(std::cell::RefCell::new(replies.into()))
905 }
906 fn session(transport: &FakeTransport, state: &mut AccountState) -> NativeIncomingSession {
907 NativeIncomingSession::authenticate(
908 transport,
909 state,
910 authority(),
911 1,
912 [2; 32],
913 parameters(),
914 ¬ification(),
915 "peer",
916 )
917 .unwrap()
918 }
919 fn tasks(number: u32) -> TaskIds {
920 TaskIds {
921 sdk: number,
922 host: number + 1,
923 }
924 }
925 struct Unsent<'a> {
926 inner: &'a FakeTransport,
927 route: CallHostRoute,
928 attempts: std::cell::Cell<usize>,
929 }
930 impl PersonalTransport for Unsent<'_> {
931 fn call_rpc(
932 &self,
933 state: &mut AccountState,
934 route: CallHostRoute,
935 request: &[u8],
936 ) -> Result<Vec<u8>> {
937 if route == self.route {
938 self.attempts.set(self.attempts.get() + 1);
939 return Err(CallRpcNotSubmitted.into());
940 }
941 self.inner.call_rpc(state, route, request)
942 }
943 }
944 #[test]
945 fn known_unsent_callee_actions_keep_cancel_phase_but_are_never_retried() {
946 for operation in 0..3 {
947 let transport = transport(if operation == 0 {
948 vec![(CallHostRoute::AppAuth, auth_reply())]
949 } else {
950 vec![
951 (CallHostRoute::AppAuth, auth_reply()),
952 (CallHostRoute::RingingAck, ack_reply()),
953 ]
954 });
955 let mut state = state();
956 let mut session = session(&transport, &mut state);
957 if operation != 0 {
958 session
959 .ringing_ack(&transport, &mut state, authority(), tasks(1))
960 .unwrap();
961 }
962 let unsent = Unsent {
963 inner: &transport,
964 route: if operation == 0 {
965 CallHostRoute::RingingAck
966 } else {
967 CallHostRoute::Transfer
968 },
969 attempts: std::cell::Cell::new(0),
970 };
971 for task in [2, 3] {
972 let result = match operation {
973 0 => session.ringing_ack(&unsent, &mut state, authority(), tasks(task)),
974 1 => session.accept(&unsent, &mut state, authority(), tasks(task)),
975 _ => session.reject(&unsent, &mut state, authority(), tasks(task)),
976 };
977 assert!(result.is_err());
978 if task == 2 {
979 assert!(result.unwrap_err().is::<CallRpcNotSubmitted>());
980 }
981 }
982 assert_eq!(unsent.attempts.get(), 1);
983 assert_eq!(
984 session.phase(),
985 if operation == 0 {
986 IncomingPhase::Invited
987 } else {
988 IncomingPhase::Acknowledged
989 }
990 );
991 let cancel = CancelNotification::decode(
992 &p::message(&[p::uint(1, 7), p::uint(2, 9), p::uint(3, 0)]).unwrap(),
993 )
994 .unwrap();
995 assert!(session.retire_cancel(&state, authority(), &cancel).unwrap());
996 assert_eq!(session.phase(), IncomingPhase::Finished);
997 assert!(transport.0.borrow().is_empty());
998 }
999 }
1000 #[test]
1001 fn incoming_reject_uses_its_own_operation_and_cannot_be_replayed() {
1002 let reply = wrapped_reply(
1003 3095,
1004 &p::message(&[p::uint(1, 7), p::uint(2, 9), p::bytes(3, &[])]).unwrap(),
1005 );
1006 let transport = transport(vec![
1007 (CallHostRoute::AppAuth, auth_reply()),
1008 (CallHostRoute::RingingAck, ack_reply()),
1009 (CallHostRoute::Transfer, reply),
1010 ]);
1011 let mut state = state();
1012 let mut session = session(&transport, &mut state);
1013 assert!(session
1014 .reject(&transport, &mut state, authority(), tasks(1))
1015 .is_err());
1016 session
1017 .ringing_ack(&transport, &mut state, authority(), tasks(1))
1018 .unwrap();
1019 session
1020 .reject(&transport, &mut state, authority(), tasks(2))
1021 .unwrap();
1022 assert_eq!(session.phase(), IncomingPhase::Finished);
1023 assert!(session
1024 .reject(&transport, &mut state, authority(), tasks(3))
1025 .is_err());
1026 assert!(transport.0.borrow().is_empty());
1027 }
1028 #[test]
1029 fn incoming_known_unselected_media_keeps_joined_control_for_one_exit() {
1030 let transport = transport(vec![
1031 (CallHostRoute::AppAuth, auth_reply()),
1032 (CallHostRoute::RingingAck, ack_reply()),
1033 (CallHostRoute::Transfer, join_reply(0, false)),
1034 (CallHostRoute::Transfer, exit_reply()),
1035 ]);
1036 let mut state = state();
1037 let mut session = session(&transport, &mut state);
1038 session
1039 .ringing_ack(&transport, &mut state, authority(), tasks(1))
1040 .unwrap();
1041 assert!(session
1042 .accept(&transport, &mut state, authority(), tasks(2))
1043 .unwrap_err()
1044 .is::<UnsupportedCallMedia>());
1045 assert_eq!(session.phase(), IncomingPhase::Joined);
1046 assert!(session.take_media(&state, authority(), 0, 0).is_err());
1047 session
1048 .exit(&transport, &mut state, authority(), tasks(3), false)
1049 .unwrap();
1050 assert!(transport.0.borrow().is_empty());
1051 }
1052 #[test]
1053 fn incoming_participant_mismatch_is_uncertain_and_cannot_be_canceled_as_caller() {
1054 let transport = transport(vec![
1055 (CallHostRoute::AppAuth, auth_reply()),
1056 (CallHostRoute::RingingAck, ack_reply()),
1057 (CallHostRoute::Transfer, join_reply(17, true)),
1058 ]);
1059 let mut state = state();
1060 let mut session = session(&transport, &mut state);
1061 session
1062 .ringing_ack(&transport, &mut state, authority(), tasks(1))
1063 .unwrap();
1064 assert!(session
1065 .accept(&transport, &mut state, authority(), tasks(2))
1066 .is_err());
1067 assert_eq!(session.phase(), IncomingPhase::Uncertain);
1068 assert!(session
1069 .exit(&transport, &mut state, authority(), tasks(3), false)
1070 .is_err());
1071 assert!(transport.0.borrow().is_empty());
1072 }
1073 #[test]
1074 fn incoming_builders_have_source_selected_ack_and_reject_tags() {
1075 let parameters = parameters();
1076 let room = VoiceRoom {
1077 room: 7,
1078 room_key: 9,
1079 own_member: 0,
1080 };
1081 let request = personal::acknowledge_voice(¶meters.inputs(), room, 1).unwrap();
1082 assert_eq!(request.operation, personal::HostOperation::RingingAck);
1083 let wrapper = fields(request.body()).unwrap();
1084 assert_eq!(
1085 wrapper.iter().map(|field| field.number).collect::<Vec<_>>(),
1086 vec![1, 3]
1087 );
1088 let ack = fields(data(&wrapper, 1).unwrap()).unwrap();
1089 assert_eq!(
1090 ack.iter().map(|field| field.number).collect::<Vec<_>>(),
1091 vec![1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15]
1092 );
1093 assert_eq!(number(&ack, 4, None).unwrap(), 1);
1094 assert_eq!(number(&ack, 13, None).unwrap(), 1);
1095 let reject = personal::reject(¶meters.inputs(), room, 2).unwrap();
1096 assert_eq!(reject.command_id, Some(3095));
1097 let body = fields(reject.body.as_deref().unwrap()).unwrap();
1098 assert_eq!(
1099 body.iter().map(|field| field.number).collect::<Vec<_>>(),
1100 vec![1, 2, 3, 4, 5, 6]
1101 );
1102 assert_eq!(number(&body, 6, None).unwrap(), 1);
1103 let join = personal::join(¶meters.inputs(), room, 0, 3).unwrap();
1104 assert_eq!(
1105 number(&fields(join.body.as_deref().unwrap()).unwrap(), 8, None).unwrap(),
1106 0
1107 );
1108 }
1109
1110 #[test]
1111 fn acked_callee_polls_without_join_and_only_newer_full_roster_handles_elsewhere() {
1112 use super::super::tests::roster_sync;
1113 let transport = transport(vec![
1114 (CallHostRoute::AppAuth, auth_reply()),
1115 (CallHostRoute::RingingAck, ack_reply()),
1116 (
1117 CallHostRoute::LiveSync,
1118 roster_sync(1, Some(1), Some(1), false),
1119 ),
1120 (
1121 CallHostRoute::LiveSync,
1122 roster_sync(2, Some(1), Some(1), true),
1123 ),
1124 ]);
1125 let mut state = state();
1126 let mut session = session(&transport, &mut state);
1127 session
1128 .ringing_ack(&transport, &mut state, authority(), tasks(1))
1129 .unwrap();
1130 assert_eq!(
1131 session
1132 .poll(&transport, &mut state, authority(), tasks(2))
1133 .unwrap(),
1134 SyncProgress::Waiting
1135 );
1136 assert_eq!(session.phase(), IncomingPhase::Acknowledged);
1137 assert_eq!(session.base.active.as_ref().unwrap().member_sequence, 1);
1138 assert_eq!(
1139 session
1140 .poll(&transport, &mut state, authority(), tasks(3))
1141 .unwrap(),
1142 SyncProgress::Terminal(RemoteTermination::LocalStop(
1143 LocalStopReason::HandledElsewhere { sdk_end: 13 }
1144 ))
1145 );
1146 assert_eq!(session.phase(), IncomingPhase::Finished);
1147 assert!(transport.0.borrow().is_empty());
1148 for wrong_identity in [false, true] {
1152 let empty_ack = host_reply(&p::bytes(
1153 1,
1154 &p::message(&[
1155 p::uint(1, 7),
1156 p::uint(2, 9),
1157 p::uint(3, 0),
1158 p::uint(4, 99),
1159 p::bytes(5, b"ack-cursor"),
1160 p::bytes(6, &p::message(&[p::uint(1, 0), p::bytes(3, b"")]).unwrap()),
1161 ])
1162 .unwrap(),
1163 ));
1164 let wire = self::transport(vec![
1165 (CallHostRoute::AppAuth, auth_reply()),
1166 (CallHostRoute::RingingAck, empty_ack),
1167 (
1168 CallHostRoute::LiveSync,
1169 roster_sync(1, Some(3), Some(1), false),
1170 ),
1171 ]);
1172 let mut next_state = super::super::tests::state();
1173 let mut next = self::session(&wire, &mut next_state);
1174 next.invite.caller_member = None;
1175 if wrong_identity {
1176 next.invite.caller_openid = b"different-peer".to_vec();
1177 }
1178 next.ringing_ack(&wire, &mut next_state, authority(), tasks(1))
1179 .unwrap();
1180 assert!(!next
1181 .base
1182 .active
1183 .as_ref()
1184 .unwrap()
1185 .original_members
1186 .contains_key("peer"));
1187 let update = next.poll(&wire, &mut next_state, authority(), tasks(2));
1188 if wrong_identity {
1189 assert!(update.is_err());
1190 assert!(next.invite.caller_member.is_none());
1191 } else {
1192 assert_eq!(update.unwrap(), SyncProgress::Waiting);
1193 assert_eq!(next.invite.caller_member, Some(1));
1194 assert!(next
1195 .base
1196 .active
1197 .as_ref()
1198 .unwrap()
1199 .unbound_peer_openid
1200 .is_none());
1201 }
1202 }
1203 }
1204 #[test]
1205 fn joined_callee_remote_hangup_requests_exit_even_after_peer_is_not_joined() {
1206 use super::super::tests::{recorded_exit_reason, roster_sync, RecordingTransport};
1207 let endpoint = p::message(&[p::bytes(1, b"192.0.2.1"), p::uint(2, 24000)]).unwrap();
1208 let connection = p::message(&[
1209 p::uint(1, 3),
1210 p::bytes(2, b"fixture"),
1211 p::uint(3, 1),
1212 p::bytes(4, &endpoint),
1213 p::bytes(5, &endpoint),
1214 ])
1215 .unwrap();
1216 let relay = p::message(&[p::bytes(1, &connection), p::bytes(2, &endpoint)]).unwrap();
1217 let item = |kind, body: &[u8]| p::message(&[p::uint(1, kind), p::bytes(2, body)]).unwrap();
1218 let notify = p::message(&[
1219 p::uint(3, 7),
1220 p::uint(4, 9),
1221 p::bytes(5, &item(17, &relay)),
1222 p::bytes(5, &item(12, &av_status(3, 0))),
1223 ])
1224 .unwrap();
1225 let message = p::message(&[p::uint(1, 10002), p::bytes(2, ¬ify)]).unwrap();
1226 let relay_update = host_reply(
1227 &p::message(&[p::bytes(1, &item(1, &message)), p::bytes(2, b"updated")]).unwrap(),
1228 );
1229 let transport = RecordingTransport {
1230 inner: transport(vec![
1231 (CallHostRoute::AppAuth, auth_reply()),
1232 (CallHostRoute::RingingAck, ack_reply()),
1233 (CallHostRoute::Transfer, join_reply(17, false)),
1234 (
1235 CallHostRoute::LiveSync,
1236 roster_sync(3, Some(1), Some(2), false),
1237 ),
1238 (
1239 CallHostRoute::LiveSync,
1240 roster_sync(4, Some(1), Some(3), false),
1241 ),
1242 (
1243 CallHostRoute::LiveSync,
1244 roster_sync(5, Some(1), Some(1), false),
1245 ),
1246 (CallHostRoute::LiveSync, relay_update),
1247 (
1248 CallHostRoute::LiveSync,
1249 roster_sync(6, Some(1), Some(4), false),
1250 ),
1251 (CallHostRoute::Transfer, exit_reply()),
1252 ]),
1253 requests: Default::default(),
1254 };
1255 let mut state = state();
1256 let mut session = NativeIncomingSession::authenticate(
1257 &transport,
1258 &mut state,
1259 authority(),
1260 1,
1261 [2; 32],
1262 parameters(),
1263 ¬ification(),
1264 "peer",
1265 )
1266 .unwrap();
1267 session
1268 .ringing_ack(&transport, &mut state, authority(), tasks(1))
1269 .unwrap();
1270 session
1271 .accept(&transport, &mut state, authority(), tasks(2))
1272 .unwrap();
1273 for task in [3, 4] {
1274 assert_eq!(
1275 session
1276 .poll(&transport, &mut state, authority(), tasks(task))
1277 .unwrap(),
1278 SyncProgress::Waiting
1279 );
1280 assert_eq!(session.phase(), IncomingPhase::Joined);
1281 assert_eq!(session.base.phase(), PersonalPhase::Joined);
1282 assert!(!session.media_ready(&state, authority()).unwrap());
1283 }
1284 assert_eq!(
1285 session
1286 .poll(&transport, &mut state, authority(), tasks(5))
1287 .unwrap(),
1288 SyncProgress::MediaReady
1289 );
1290 assert_eq!(session.phase(), IncomingPhase::Joined);
1291 session.take_media(&state, authority(), 0, 0).unwrap();
1292 let original_strategy = session.base.active.as_ref().unwrap().strategy.clone();
1293 assert_eq!(
1294 session
1295 .poll(&transport, &mut state, authority(), tasks(6))
1296 .unwrap(),
1297 SyncProgress::MediaReady
1298 );
1299 assert_eq!(session.peer_microphone_muted(), Some(true));
1300 let active = session.base.active.as_ref().unwrap();
1301 assert_eq!(*active.strategy, *original_strategy);
1302 assert_eq!(active.cookies, b"updated");
1303 assert!(active.media_taken);
1304 assert!(matches!(
1305 session
1306 .poll(&transport, &mut state, authority(), tasks(7))
1307 .unwrap(),
1308 SyncProgress::Terminal(RemoteTermination::Exit(RemoteExitReason::LastPeerLeft))
1309 ));
1310 session
1311 .finish_remote(&transport, &mut state, authority(), tasks(8), false)
1312 .unwrap();
1313 assert_eq!(session.phase(), IncomingPhase::Finished);
1314 assert_eq!(
1315 recorded_exit_reason(&transport.requests.borrow().last().unwrap().1),
1316 5
1317 );
1318 }
1319}