use anyhow::{Result, anyhow};
use wacore_binary::builder::NodeBuilder;
use wacore_binary::{Jid, Node, NodeRef};
use crate::time::from_secs;
use crate::types::call::{CallAction, CallActionTag, CallAudioCodec, IncomingCall, VideoState};
pub fn parse_call_stanza(node: &NodeRef<'_>) -> Result<Option<IncomingCall>> {
if node.tag != "call" {
return Err(anyhow!("expected <call>, got <{}>", node.tag));
}
let Some((child, action_tag)) = node.children().and_then(|children| {
children.iter().find_map(|child| {
CallActionTag::try_from(child.tag.as_ref())
.ok()
.map(|action_tag| (child, action_tag))
})
}) else {
return Ok(None);
};
let mut attrs = node.attrs();
let from = attrs
.optional_jid("from")
.ok_or_else(|| anyhow!("<call> missing 'from' attribute"))?;
let stanza_id = attrs
.optional_string("id")
.map(|s| s.into_owned())
.unwrap_or_default();
let notify = attrs
.optional_string("notify")
.and_then(|s| (!s.is_empty()).then(|| s.into_owned()));
let platform = attrs.optional_string("platform").map(|s| s.into_owned());
let version = attrs.optional_string("version").map(|s| s.into_owned());
let participant = attrs.optional_jid("participant");
let recipient = attrs.optional_jid("recipient");
let ts = attrs
.optional_unix_time("t")
.ok_or_else(|| anyhow!("<call> missing or invalid 't' attribute"))?;
let timestamp = from_secs(ts).ok_or_else(|| anyhow!("<call> 't'={ts} out of range"))?;
let offline = attrs.optional_string("offline").is_some();
attrs.finish().map_err(|e| anyhow!("<call> attrs: {e}"))?;
let is_offer = action_tag == CallActionTag::Offer;
let action = parse_action(child, action_tag)?;
let group = if is_offer {
super::group_call::parse_group_invite_snapshot(child)?.map(Box::new)
} else {
None
};
#[cfg(feature = "voip")]
let media = is_offer
.then(|| parse_media_offer(node, child, participant.as_ref().unwrap_or(&from)))
.flatten()
.map(Box::new);
let call = IncomingCall::builder()
.from(from)
.stanza_id(stanza_id)
.maybe_notify(notify)
.maybe_platform(platform)
.maybe_version(version)
.maybe_participant(participant)
.maybe_recipient(recipient)
.timestamp(timestamp)
.offline(offline)
.action(action)
.maybe_group(group);
#[cfg(feature = "voip")]
let call = call.maybe_media(media);
Ok(Some(call.build()))
}
#[cfg(feature = "voip")]
fn parse_media_offer(
call: &NodeRef<'_>,
offer: &NodeRef<'_>,
peer: &Jid,
) -> Option<crate::types::call::MediaOffer> {
use crate::types::call::{MediaOffer, OfferRecipientEnc};
use crate::types::group_call::GroupCallDevice;
let mut encs = Vec::new();
if let Some(enc_node) = offer.get_optional_child("enc") {
if let Some(enc) = parse_offer_enc(enc_node) {
encs.push(OfferRecipientEnc { to: None, enc });
}
} else if let Some(dest) = offer.get_optional_child("destination") {
for to in dest
.children()
.unwrap_or_default()
.iter()
.filter(|c| c.tag.as_ref() == "to")
{
if let Some(enc_node) = to.get_optional_child("enc")
&& let Some(enc) = parse_offer_enc(enc_node)
&& let Some(to_jid) = to.get_attr("jid").and_then(|v| v.to_jid())
{
encs.push(OfferRecipientEnc {
to: Some(to_jid),
enc,
});
}
}
}
if encs.is_empty() {
return None;
}
let relay = find_relay(call).and_then(crate::voip::relay_parse::parse_relay_data);
let (peer_abtest_bucket, peer_abtest_bucket_id_list) = offer
.get_optional_child("metadata")
.map(|metadata| {
let mut attrs = metadata.attrs();
(
attrs
.optional_string("peer_abtest_bucket")
.map(|value| value.into_owned()),
attrs
.optional_string("peer_abtest_bucket_id_list")
.map(|value| value.into_owned()),
)
})
.unwrap_or_default();
let peer_device = offer
.get_optional_child("capability")
.and_then(|capability| {
let bytes = capability.content_bytes()?.to_vec();
if bytes.is_empty() {
return None;
}
let capability_version = match capability.get_attr("ver") {
None => 1,
Some(version) => version.as_str().parse::<u32>().ok()?,
};
let mut device = GroupCallDevice::new(peer.clone());
device.capability_version = Some(capability_version);
device.capability = bytes;
Some(device)
});
Some(MediaOffer {
encs,
relay,
peer_abtest_bucket,
peer_abtest_bucket_id_list,
peer_device,
})
}
#[cfg(feature = "voip")]
fn parse_offer_enc(enc_node: &NodeRef<'_>) -> Option<crate::types::call::OfferEnc> {
use crate::types::call::OfferEnc;
let ciphertext = enc_node.content_bytes()?.to_vec();
let enc_type = enc_node
.get_attr("type")
.map(|v| v.as_str().to_string())
.unwrap_or_else(|| "pkmsg".into());
let version = enc_node
.get_attr("v")
.and_then(|v| v.as_str().parse::<u8>().ok())
.unwrap_or(2);
Some(OfferEnc {
enc_type,
version,
ciphertext,
})
}
#[cfg(feature = "voip")]
pub fn find_relay<'a, 'b>(nr: &'b NodeRef<'a>) -> Option<&'b NodeRef<'a>> {
if nr.tag.as_ref() == "relay" {
return Some(nr);
}
nr.children().and_then(|cs| cs.iter().find_map(find_relay))
}
fn parse_audio_codec(node: &NodeRef<'_>) -> Result<CallAudioCodec> {
let mut a = node.attrs();
let enc = a
.required_string("enc")
.map_err(|e| anyhow!("<audio> missing 'enc': {e}"))?
.into_owned();
let rate_raw = a
.optional_u64("rate")
.ok_or_else(|| anyhow!("<audio enc={enc}> missing or invalid 'rate'"))?;
let rate = u32::try_from(rate_raw)
.map_err(|_| anyhow!("<audio enc={enc}> 'rate'={rate_raw} overflows u32"))?;
a.finish().map_err(|e| anyhow!("<audio> attrs: {e}"))?;
Ok(CallAudioCodec { enc, rate })
}
fn parse_action(node: &NodeRef<'_>, action_tag: CallActionTag) -> Result<CallAction> {
let mut attrs = node.attrs();
let call_id = attrs
.required_string("call-id")
.map_err(|e| anyhow!("<{}> missing 'call-id': {e}", node.tag))?
.into_owned();
let call_creator = attrs
.optional_jid("call-creator")
.ok_or_else(|| anyhow!("<{}> missing 'call-creator'", node.tag))?;
Ok(match action_tag {
CallActionTag::GroupUpdate => CallAction::GroupUpdate {
update: super::group_call::parse_group_update(node)?.into(),
},
CallActionTag::EncRekey => CallAction::EncRekey {
rekey: super::group_call::parse_group_enc_rekey(node)?.into(),
},
CallActionTag::WaitingRoomUpdate => CallAction::WaitingRoomUpdate {
room: super::group_call::parse_waiting_room_update(node)?.into(),
},
CallActionTag::RaiseHand => CallAction::RaiseHand {
call_id,
call_creator,
raised: super::group_call::parse_raise_hand(node)?,
},
CallActionTag::ScreenShare => CallAction::ScreenShare {
call_id,
call_creator,
screen_share: super::group_call::parse_screen_share(node)?,
},
CallActionTag::Offer => {
let caller_pn = attrs.optional_jid("caller_pn");
let caller_country_code = attrs
.optional_string("caller_country_code")
.map(|s| s.into_owned());
let device_class = attrs
.optional_string("device_class")
.map(|s| s.into_owned());
let joinable = attrs
.optional_string("joinable")
.map(|s| s == "1")
.unwrap_or(false);
let group_jid = attrs.optional_jid("group-jid");
attrs.finish().map_err(|e| anyhow!("<offer> attrs: {e}"))?;
let children = node.children().unwrap_or_default();
let is_video = children.iter().any(|c| c.tag == "video");
let audio = children
.iter()
.filter(|c| c.tag == "audio")
.map(parse_audio_codec)
.collect::<Result<Vec<_>>>()?;
CallAction::Offer {
call_id,
call_creator,
caller_pn,
caller_country_code,
device_class,
joinable,
is_video,
audio,
group_jid,
}
}
CallActionTag::OfferNotice => {
let is_video = attrs.optional_string("media").is_some_and(|s| s == "video");
let is_group = attrs.optional_string("type").is_some_and(|s| s == "group");
attrs
.finish()
.map_err(|e| anyhow!("<offer_notice> attrs: {e}"))?;
CallAction::OfferNotice {
call_id,
call_creator,
is_video,
is_group,
}
}
CallActionTag::PreAccept => {
attrs
.finish()
.map_err(|e| anyhow!("<preaccept> attrs: {e}"))?;
let audio = node
.children()
.unwrap_or_default()
.iter()
.filter(|child| child.tag == "audio")
.map(parse_audio_codec)
.collect::<Result<Vec<_>>>()?;
CallAction::PreAccept {
call_id,
call_creator,
audio,
}
}
CallActionTag::Transport => {
let p2p_cand_round = attrs
.optional_string("p2p-cand-round")
.map(|s| s.into_owned());
let transport_message_type = attrs
.optional_string("transport-message-type")
.map(|s| s.into_owned());
attrs
.finish()
.map_err(|e| anyhow!("<transport> attrs: {e}"))?;
CallAction::Transport {
call_id,
call_creator,
p2p_cand_round,
transport_message_type,
}
}
CallActionTag::RelayLatency => {
attrs
.finish()
.map_err(|e| anyhow!("<relaylatency> attrs: {e}"))?;
CallAction::RelayLatency {
call_id,
call_creator,
}
}
CallActionTag::Accept => {
attrs.finish().map_err(|e| anyhow!("<accept> attrs: {e}"))?;
let audio = node
.children()
.unwrap_or_default()
.iter()
.filter(|child| child.tag == "audio")
.map(parse_audio_codec)
.collect::<Result<Vec<_>>>()?;
CallAction::Accept {
call_id,
call_creator,
audio,
}
}
CallActionTag::Reject => {
let reason = attrs.optional_string("reason").map(|c| c.into_owned());
attrs.finish().map_err(|e| anyhow!("<reject> attrs: {e}"))?;
CallAction::Reject {
call_id,
call_creator,
reason,
}
}
CallActionTag::VideoState => {
let state_raw = attrs
.optional_string("state")
.and_then(|s| s.parse::<i32>().ok())
.ok_or_else(|| anyhow!("<video> missing or non-numeric 'state'"))?;
let orientation = attrs
.optional_string("device_orientation")
.and_then(|s| s.parse::<u8>().ok())
.filter(|orientation| *orientation <= 3);
let dec = attrs.optional_string("dec").map(|s| s.into_owned());
let _ = attrs.optional_string("voip_settings");
attrs.finish().map_err(|e| anyhow!("<video> attrs: {e}"))?;
CallAction::VideoState {
call_id,
call_creator,
state: VideoState::from(state_raw),
orientation,
dec,
}
}
CallActionTag::Terminate => {
let reason = attrs.optional_string("reason").map(|c| c.into_owned());
let duration = attrs
.optional_u64("duration")
.and_then(|v| u32::try_from(v).ok());
let audio_duration = attrs
.optional_u64("audio_duration")
.and_then(|v| u32::try_from(v).ok());
attrs
.finish()
.map_err(|e| anyhow!("<terminate> attrs: {e}"))?;
CallAction::Terminate {
call_id,
call_creator,
reason,
duration,
audio_duration,
}
}
})
}
pub fn build_offer_ack_receipt(call: &IncomingCall, own_ad: Option<&Jid>) -> Option<Node> {
let CallAction::Offer {
call_id,
call_creator,
..
} = &call.action
else {
return None;
};
let mut receipt = NodeBuilder::new("receipt")
.attr("to", &call.from)
.attr("id", call.stanza_id.as_str());
if let Some(jid) = own_ad {
receipt = receipt.attr("from", jid);
}
let offer = NodeBuilder::new("offer")
.attr("call-id", call_id.as_str())
.attr("call-creator", call_creator)
.build();
Some(receipt.children([offer]).build())
}
pub const CAPABILITY_OFFER: [u8; 7] = [0x01, 0x05, 0xf7, 0x09, 0xe0, 0xbb, 0x13];
pub const CAPABILITY_PREACCEPT: [u8; 7] = [0x01, 0x05, 0xf7, 0x09, 0xe0, 0xbb, 0x07];
pub const DEFAULT_AUDIO_RATES: &[&str] = &["8000", "16000"];
pub const CAPABILITY_VIDEO_OFFER: [u8; 7] = [0x01, 0x05, 0xf7, 0x09, 0xe0, 0xfa, 0x13];
const fn without_mlow_capability(mut capability: [u8; 7]) -> [u8; 7] {
capability[5] &= 0x7f;
capability
}
pub const CAPABILITY_STANDARD_OPUS_OFFER: [u8; 7] = without_mlow_capability(CAPABILITY_OFFER);
pub const CAPABILITY_STANDARD_OPUS_PREACCEPT: [u8; 7] =
without_mlow_capability(CAPABILITY_PREACCEPT);
pub const CAPABILITY_STANDARD_OPUS_VIDEO_OFFER: [u8; 7] =
without_mlow_capability(CAPABILITY_VIDEO_OFFER);
pub const TERMINATE_REASON_ACCEPTED_ELSEWHERE: &str = "accepted_elsewhere";
pub const TERMINATE_REASON_REJECTED_ELSEWHERE: &str = "rejected_elsewhere";
pub const TERMINATE_REASON_TIMEOUT: &str = "timeout";
pub const TERMINATE_REASON_GROUP_CALL_ENDED: &str = "group_call_ended";
pub const REJECT_REASON_BUSY: &str = "busy";
pub fn encode_latency(rtt_ms: u32) -> String {
(0x0200_0000u32.wrapping_add(rtt_ms)).to_string()
}
pub struct OfferDeviceKey {
pub device_jid: Jid,
pub ciphertext: Vec<u8>,
pub enc_type: String,
}
pub struct OfferParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub call_creator: &'a Jid,
pub device_keys: &'a [OfferDeviceKey],
pub privacy_token: Option<&'a [u8]>,
pub capability: Option<&'a [u8]>,
pub device_identity: Option<&'a [u8]>,
pub id: Option<&'a str>,
pub multi_device: bool,
pub video: bool,
pub audio_rates: &'a [&'a str],
}
pub fn build_offer(p: &OfferParams<'_>) -> Node {
let mut children: Vec<Node> = Vec::new();
if let Some(privacy) = p.privacy_token {
children.push(NodeBuilder::new("privacy").bytes(privacy.to_vec()).build());
}
children.extend(p.audio_rates.iter().map(|rate| audio_opus(rate)));
if p.video {
children.push(video_offer_node());
}
children.push(NodeBuilder::new("net").attr("medium", "3").build());
if let Some(cap) = p.capability {
children.push(capability_node(cap));
}
if p.device_keys.len() > 1 || p.multi_device {
let to_nodes: Vec<Node> = p
.device_keys
.iter()
.map(|dk| {
NodeBuilder::new("to")
.attr("jid", &dk.device_jid)
.children([enc_node(dk)])
.build()
})
.collect();
children.push(NodeBuilder::new("destination").children(to_nodes).build());
} else if let Some(dk) = p.device_keys.first() {
children.push(enc_node(dk));
}
children.push(encopt_node());
if let Some(di) = p.device_identity {
children.push(
NodeBuilder::new("device-identity")
.bytes(di.to_vec())
.build(),
);
}
call_wrap(
p.to,
p.id,
offer_action("offer", p.call_id, p.call_creator, children),
)
}
fn enc_node(dk: &OfferDeviceKey) -> Node {
NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", dk.enc_type.clone())
.attr("count", "0")
.bytes(dk.ciphertext.clone())
.build()
}
const STANDARD_OPUS_PT120_SETTINGS: &[u8] =
br#"{"encode":{"use_mlow_codec_v1":"false"},"options":{"enable_48khz_rtp_clock":"false"}}"#;
const STANDARD_OPUS_PT111_SETTINGS: &[u8] =
br#"{"encode":{"use_mlow_codec_v1":"false"},"options":{"enable_48khz_rtp_clock":"true"}}"#;
pub fn standard_opus_voip_settings(enable_48khz_rtp_clock: bool) -> &'static [u8] {
if enable_48khz_rtp_clock {
STANDARD_OPUS_PT111_SETTINGS
} else {
STANDARD_OPUS_PT120_SETTINGS
}
}
pub struct AcceptParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub id: &'a str,
pub call_creator: &'a Jid,
pub audio_rates: &'a [&'a str],
pub relay_te: Option<&'a [u8]>,
pub rte: Option<&'a [u8]>,
pub voip_settings: Option<&'a [u8]>,
pub capability: Option<&'a [u8]>,
pub video: bool,
pub peer_abtest_bucket: Option<&'a str>,
pub peer_abtest_bucket_id_list: Option<&'a str>,
}
pub fn build_accept(p: &AcceptParams<'_>) -> Node {
let mut children: Vec<Node> = p.audio_rates.iter().map(|rate| audio_opus(rate)).collect();
if p.video {
children.push(video_accept_node());
}
if let Some(te) = p.relay_te {
children.push(
NodeBuilder::new("te")
.attr("priority", "2")
.bytes(te.to_vec())
.build(),
);
}
children.push(NodeBuilder::new("net").attr("medium", "2").build());
children.push(encopt_node());
if p.peer_abtest_bucket.is_some() || p.peer_abtest_bucket_id_list.is_some() {
let mut metadata = NodeBuilder::new("metadata");
if let Some(bucket) = p.peer_abtest_bucket {
metadata = metadata.attr("peer_abtest_bucket", bucket.to_string());
}
if let Some(ids) = p.peer_abtest_bucket_id_list {
metadata = metadata.attr("peer_abtest_bucket_id_list", ids.to_string());
}
children.push(metadata.build());
}
if let Some(cap) = p.capability {
children.push(capability_node(cap));
}
if let Some(rte) = p.rte {
children.push(NodeBuilder::new("rte").bytes(rte.to_vec()).build());
}
if let Some(vs) = p.voip_settings {
children.push(
NodeBuilder::new("voip_settings")
.attr("uncompressed", "1")
.bytes(vs.to_vec())
.build(),
);
}
call_wrap(
p.to,
Some(p.id),
offer_action("accept", p.call_id, p.call_creator, children),
)
}
const VIDEO_SCREEN_WIDTH: &str = "1920";
const VIDEO_SCREEN_HEIGHT: &str = "1080";
fn video_offer_node() -> Node {
NodeBuilder::new("video")
.attr("enc", "h264")
.attr("dec", "h264")
.attr("orientation", "0")
.attr("screen_width", VIDEO_SCREEN_WIDTH)
.attr("screen_height", VIDEO_SCREEN_HEIGHT)
.attr("device_orientation", "0")
.build()
}
fn video_accept_node() -> Node {
NodeBuilder::new("video")
.attr("dec", "H264")
.attr("device_orientation", "0")
.build()
}
fn video_preaccept_node() -> Node {
NodeBuilder::new("video")
.attr("dec", "H264")
.attr("device_orientation", "0")
.attr("screen_width", "0")
.attr("screen_height", "0")
.build()
}
fn audio_opus(rate: &str) -> Node {
NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", rate)
.build()
}
fn encopt_node() -> Node {
NodeBuilder::new("encopt").attr("keygen", "2").build()
}
fn capability_node(blob: &[u8]) -> Node {
NodeBuilder::new("capability")
.attr("ver", "1")
.bytes(blob.to_vec())
.build()
}
pub fn build_preaccept(
call_id: &str,
to: &Jid,
call_creator: &Jid,
wrapper_id: &str,
audio_rates: &[&str],
video: bool,
) -> Node {
build_preaccept_with_capability(
call_id,
to,
call_creator,
wrapper_id,
audio_rates,
if video {
&CAPABILITY_OFFER
} else {
&CAPABILITY_PREACCEPT
},
video,
)
}
pub fn build_preaccept_with_capability(
call_id: &str,
to: &Jid,
call_creator: &Jid,
wrapper_id: &str,
audio_rates: &[&str],
capability: &[u8],
video: bool,
) -> Node {
let mut children: Vec<Node> = audio_rates.iter().map(|rate| audio_opus(rate)).collect();
if video {
children.push(video_preaccept_node());
}
children.push(encopt_node());
children.push(capability_node(capability));
call_wrap(
to,
Some(wrapper_id),
offer_action("preaccept", call_id, call_creator, children),
)
}
pub struct TransportParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub call_creator: &'a Jid,
pub p2p_cand_round: Option<&'a str>,
pub transport_message_type: Option<&'a str>,
pub relay_te: Option<&'a [u8]>,
}
pub fn build_transport(p: &TransportParams<'_>) -> Node {
let mut action = NodeBuilder::new("transport")
.attr("call-id", p.call_id)
.attr("call-creator", p.call_creator);
if let Some(round) = p.p2p_cand_round {
action = action.attr("p2p-cand-round", round.to_string());
}
if let Some(mt) = p.transport_message_type {
action = action.attr("transport-message-type", mt.to_string());
}
let mut children: Vec<Node> = Vec::new();
if let Some(te) = p.relay_te {
children.push(
NodeBuilder::new("te")
.attr("priority", "1")
.bytes(te.to_vec())
.build(),
);
}
let mut net = NodeBuilder::new("net").attr("medium", "2");
if p.transport_message_type != Some("9") {
net = net.attr("protocol", "0");
}
children.push(net.build());
call_wrap(p.to, None, action.children(children).build())
}
pub struct RelayLatencyParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub call_creator: &'a Jid,
pub latency_ms: u32,
pub relay_name: &'a str,
pub address_bytes: &'a [u8],
pub devices: &'a [Jid],
}
pub fn build_relay_latency(p: &RelayLatencyParams<'_>) -> Node {
let mut children: Vec<Node> = vec![
NodeBuilder::new("te")
.attr("latency", encode_latency(p.latency_ms))
.attr("relay_name", p.relay_name.to_string())
.bytes(p.address_bytes.to_vec())
.build(),
];
if !p.devices.is_empty() {
children.push(destination_to(p.devices));
}
call_wrap(
p.to,
None,
offer_action("relaylatency", p.call_id, p.call_creator, children),
)
}
pub fn build_heartbeat(call_id: &str, call_creator: &Jid, wrapper_id: &str) -> Node {
let action = NodeBuilder::new("heartbeat")
.attr("call-id", call_id)
.attr("call-creator", call_creator)
.build();
NodeBuilder::new("call")
.attr("to", format!("{call_id}@call"))
.attr("id", wrapper_id.to_string())
.children([action])
.build()
}
pub struct TerminateParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub id: Option<&'a str>,
pub call_creator: &'a Jid,
pub reason: Option<&'a str>,
}
pub fn build_terminate(p: &TerminateParams<'_>) -> Node {
let mut action = NodeBuilder::new("terminate")
.attr("call-id", p.call_id)
.attr("call-creator", p.call_creator);
if let Some(reason) = p.reason {
action = action.attr("reason", reason.to_string());
}
call_wrap(p.to, p.id, action.build())
}
pub struct VideoStateParams<'a> {
pub call_id: &'a str,
pub to: &'a Jid,
pub id: &'a str,
pub call_creator: &'a Jid,
pub state: VideoState,
pub dec: Option<&'a str>,
pub device_orientation: Option<u8>,
}
pub fn build_video_state(p: &VideoStateParams<'_>) -> Node {
let mut action = NodeBuilder::new("video")
.attr("call-id", p.call_id)
.attr("call-creator", p.call_creator)
.attr("state", p.state.code().to_string());
if let Some(dec) = p.dec {
action = action.attr("dec", dec.to_string());
}
if p.state == VideoState::UpgradeRequestV2 {
action = action.attr("voip_settings", "video");
}
if let Some(o) = p.device_orientation {
action = action.attr("device_orientation", o.to_string());
}
call_wrap(p.to, Some(p.id), action.build())
}
pub fn build_call_video_ack(call: &IncomingCall, original: &NodeRef<'_>) -> Option<Node> {
if call.stanza_id.is_empty() {
return None;
}
build_typed_call_ack(original, "video")
}
#[cold]
#[inline(never)]
pub(crate) fn build_typed_call_ack(original: &NodeRef<'_>, action_type: &str) -> Option<Node> {
if original.tag != "call" || action_type.is_empty() {
return None;
}
let id = original.get_attr("id")?.to_node_value();
let from = original.get_attr("from")?.to_node_value();
let mut ack = NodeBuilder::new("ack")
.attr("class", "call")
.attr("id", id)
.attr("to", from)
.attr("type", action_type);
if let Some(participant) = original.get_attr("participant")
&& original
.get_attr("from")
.is_none_or(|from| participant.as_str() != from.as_str())
{
ack = ack.attr("participant", participant.to_node_value());
}
if let Some(recipient) = original.get_attr("recipient") {
ack = ack.attr("recipient", recipient.to_node_value());
}
Some(ack.build())
}
pub fn build_mute_v2(call_id: &str, to: &Jid, call_creator: &Jid, mute_state: &str) -> Node {
let action = NodeBuilder::new("mute_v2")
.attr("call-id", call_id)
.attr("call-creator", call_creator)
.attr("mute-state", mute_state.to_string())
.build();
call_wrap(to, None, action)
}
pub fn build_reject(call_id: &str, to: &Jid, call_creator: &Jid, wrapper_id: &str) -> Node {
call_wrap(
to,
Some(wrapper_id),
NodeBuilder::new("reject")
.attr("call-id", call_id)
.attr("call-creator", call_creator)
.attr("count", "0")
.build(),
)
}
fn offer_action(tag: &'static str, call_id: &str, call_creator: &Jid, children: Vec<Node>) -> Node {
NodeBuilder::new(tag)
.attr("call-id", call_id)
.attr("call-creator", call_creator)
.children(children)
.build()
}
fn destination_to(devices: &[Jid]) -> Node {
let tos: Vec<Node> = devices
.iter()
.map(|jid| NodeBuilder::new("to").attr("jid", jid).build())
.collect();
NodeBuilder::new("destination").children(tos).build()
}
fn call_wrap(to: &Jid, id: Option<&str>, action: Node) -> Node {
let mut call = NodeBuilder::new("call").attr("to", to);
if let Some(id) = id {
call = call.attr("id", id.to_string());
}
call.children([action]).build()
}
#[cfg(test)]
mod tests {
use super::*;
use wacore_binary::builder::NodeBuilder;
use wacore_binary::{Jid, Server};
fn fake_caller_lid() -> Jid {
Jid::new("111111111111111", Server::Lid)
}
fn fake_caller_pn() -> Jid {
Jid::new("15555550100", Server::Pn)
}
fn base_call_builder() -> NodeBuilder {
NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "STANZA-ID-0001")
.attr("version", "2.25.37.76")
.attr("platform", "android")
.attr("notify", "Test Caller")
.attr("t", "1766847151")
.attr("e", "0")
}
fn offer_builder_base() -> NodeBuilder {
NodeBuilder::new("offer")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CALL-ID-0001")
}
fn as_ref<'a>(n: &'a Node) -> NodeRef<'a> {
n.as_node_ref()
}
#[cfg(feature = "voip")]
fn parsed_peer_capability(
version: Option<&str>,
) -> Option<crate::types::group_call::GroupCallDevice> {
let mut capability = NodeBuilder::new("capability").bytes(CAPABILITY_OFFER.to_vec());
if let Some(version) = version {
capability = capability.attr("ver", version);
}
let node = base_call_builder()
.children([offer_builder_base()
.children([
NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "16000")
.build(),
NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "pkmsg")
.bytes(vec![1, 2, 3, 4])
.build(),
capability.build(),
])
.build()])
.build();
parse_call_stanza(&as_ref(&node))
.expect("offer parses")
.expect("recognized call")
.media
.expect("media offer")
.peer_device
}
#[cfg(feature = "voip")]
#[test]
fn capability_version_defaults_only_when_absent() {
assert_eq!(
parsed_peer_capability(None).and_then(|device| device.capability_version),
Some(1)
);
assert_eq!(
parsed_peer_capability(Some("7")).and_then(|device| device.capability_version),
Some(7)
);
assert!(
parsed_peer_capability(Some("invalid")).is_none(),
"an explicitly malformed version must discard the entire capability"
);
assert!(parsed_peer_capability(Some("4294967296")).is_none());
}
#[cfg(feature = "voip")]
#[test]
fn offer_captures_enc_and_relay_for_media() {
let relay = NodeBuilder::new("relay")
.children([
NodeBuilder::new("warp_mi_tag_len")
.bytes(b"4".to_vec())
.build(),
NodeBuilder::new("token")
.attr("id", "0")
.bytes(vec![0xaa, 0xbb])
.build(),
NodeBuilder::new("te2")
.attr("relay_id", "1")
.attr("relay_name", "gru1c02")
.attr("token_id", "0")
.attr("auth_token_id", "1")
.bytes(vec![157, 240, 226, 133, 0x0d, 0x96])
.build(),
])
.build();
let node = base_call_builder()
.children([
offer_builder_base()
.children([
NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "pkmsg")
.bytes(vec![1, 2, 3, 4])
.build(),
NodeBuilder::new("capability")
.attr("ver", "1")
.bytes(CAPABILITY_OFFER.to_vec())
.build(),
NodeBuilder::new("metadata")
.attr("peer_abtest_bucket", "video_interop_holdout")
.attr("peer_abtest_bucket_id_list", "110001,110002")
.build(),
])
.build(),
relay,
])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let media = call.media.expect("offer with <enc> must capture media");
let enc = media
.enc_for(None)
.expect("a bare <enc> is addressed to us");
assert_eq!(enc.enc_type, "pkmsg");
assert_eq!(enc.version, 2);
assert_eq!(enc.ciphertext, vec![1, 2, 3, 4]);
assert_eq!(
media.peer_abtest_bucket.as_deref(),
Some("video_interop_holdout")
);
assert_eq!(
media.peer_abtest_bucket_id_list.as_deref(),
Some("110001,110002")
);
let peer_device = media.peer_device.as_ref().expect("active peer capability");
assert_eq!(peer_device.jid, fake_caller_lid());
assert_eq!(peer_device.capability_version, Some(1));
assert_eq!(peer_device.capability, CAPABILITY_OFFER);
let rd = media.relay.expect("the <relay> must be parsed");
assert_eq!(rd.warp_mi_tag_len, Some(4));
assert_eq!(rd.relay_tokens[0], vec![0xaa, 0xbb]);
assert_eq!(rd.endpoints[0].relay_name, "gru1c02");
}
#[cfg(feature = "voip")]
#[test]
fn peer_capability_binds_to_the_routed_participant() {
let participant = fake_caller_lid().with_device(3);
let node = base_call_builder()
.attr("participant", &participant)
.children([offer_builder_base()
.children([
NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "pkmsg")
.bytes(vec![1, 2, 3, 4])
.build(),
NodeBuilder::new("capability")
.attr("ver", "1")
.bytes(CAPABILITY_OFFER.to_vec())
.build(),
])
.build()])
.build();
let device = parse_call_stanza(&as_ref(&node))
.unwrap()
.unwrap()
.media
.expect("media offer")
.peer_device
.expect("peer capability");
assert_eq!(device.jid, participant);
}
#[cfg(feature = "voip")]
#[test]
fn offer_without_enc_has_no_media() {
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "16000")
.build()])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
assert!(call.media.is_none());
}
#[cfg(feature = "voip")]
#[test]
fn offer_multi_device_selects_enc_for_our_device() {
let dev1: Jid = "111111111111111:3@lid".parse().unwrap();
let dev2: Jid = "111111111111111:7@lid".parse().unwrap();
let to1 = NodeBuilder::new("to")
.attr("jid", &dev1)
.children([NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "pkmsg")
.bytes(vec![0xA1])
.build()])
.build();
let to2 = NodeBuilder::new("to")
.attr("jid", &dev2)
.children([NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "msg")
.bytes(vec![0xB2])
.build()])
.build();
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("destination").children([to1, to2]).build()])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let media = call.media.expect("multi-device offer captures media");
assert_eq!(media.enc_for(Some(&dev2)).unwrap().ciphertext, vec![0xB2]);
assert_eq!(media.enc_for(Some(&dev2)).unwrap().enc_type, "msg");
assert_eq!(media.enc_for(Some(&dev1)).unwrap().ciphertext, vec![0xA1]);
let other: Jid = "222222222222222:1@lid".parse().unwrap();
assert!(media.enc_for(Some(&other)).is_none());
}
#[cfg(feature = "voip")]
#[test]
fn offer_to_jid_matches_our_own_jid_field_for_field() {
let wire_to = Jid {
user: "111111111111111".into(),
server: Server::Lid,
agent: 0,
device: 7,
integrator: 0,
};
let to = NodeBuilder::new("to")
.attr("jid", &wire_to)
.children([NodeBuilder::new("enc")
.attr("v", "2")
.attr("type", "msg")
.bytes(vec![0xB2])
.build()])
.build();
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("destination").children([to]).build()])
.build()])
.build();
let bytes = wacore_binary::marshal::marshal(&node).unwrap();
let decoded = wacore_binary::marshal::unmarshal_ref(&bytes[1..]).unwrap();
let call = parse_call_stanza(&decoded).unwrap().unwrap();
let media = call.media.expect("offer captures media");
let from_wire = media.encs[0].to.as_ref().expect("<to jid> survives decode");
let from_text: Jid = "111111111111111:7@lid".parse().unwrap();
assert_eq!(
from_wire, &from_text,
"the decoded <to jid> must equal the same JID read back as text, which is \
how our own LID reaches `enc_for`"
);
assert_eq!(
media
.enc_for(Some(&from_text))
.expect("callKey for our device")
.ciphertext,
vec![0xB2],
);
}
#[test]
fn offer_audio_only() {
let node = base_call_builder()
.children([offer_builder_base()
.attr("caller_pn", fake_caller_pn())
.attr("device_class", "2016")
.attr("joinable", "1")
.attr("caller_country_code", "BR")
.children([
NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "16000")
.build(),
NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "8000")
.build(),
])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
assert_eq!(call.stanza_id, "STANZA-ID-0001");
assert_eq!(call.from, fake_caller_lid());
assert_eq!(call.timestamp.timestamp(), 1766847151);
assert!(!call.offline);
assert_eq!(call.notify.as_deref(), Some("Test Caller"));
assert_eq!(call.platform.as_deref(), Some("android"));
match call.action {
CallAction::Offer {
call_id,
call_creator,
caller_pn,
caller_country_code,
device_class,
joinable,
is_video,
audio,
group_jid,
} => {
assert_eq!(call_id, "CALL-ID-0001");
assert_eq!(call_creator, fake_caller_lid());
assert_eq!(caller_pn, Some(fake_caller_pn()));
assert_eq!(caller_country_code.as_deref(), Some("BR"));
assert_eq!(device_class.as_deref(), Some("2016"));
assert!(joinable);
assert!(!is_video);
assert_eq!(audio.len(), 2);
assert_eq!(audio[0].enc, "opus");
assert_eq!(audio[0].rate, 16000);
assert_eq!(audio[1].rate, 8000);
assert_eq!(group_jid, None);
}
other => panic!("expected Offer, got {other:?}"),
}
}
#[test]
fn offer_video() {
let node = base_call_builder()
.children([offer_builder_base()
.children([
NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "16000")
.build(),
NodeBuilder::new("video").build(),
])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::Offer {
is_video, audio, ..
} => {
assert!(is_video);
assert_eq!(audio.len(), 1);
}
other => panic!("expected Offer, got {other:?}"),
}
}
#[test]
fn offer_minimum_attrs() {
let node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "STANZA-ID-0001")
.attr("t", "1766847151")
.children([offer_builder_base().build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
assert_eq!(call.notify, None);
assert_eq!(call.platform, None);
assert_eq!(call.version, None);
match call.action {
CallAction::Offer {
caller_pn,
caller_country_code,
device_class,
joinable,
is_video,
audio,
..
} => {
assert_eq!(caller_pn, None);
assert_eq!(caller_country_code, None);
assert_eq!(device_class, None);
assert!(!joinable);
assert!(!is_video);
assert!(audio.is_empty());
}
other => panic!("expected Offer, got {other:?}"),
}
}
#[test]
fn offer_with_group_jid() {
let group_jid = Jid::new("123456789", Server::Group);
let node = base_call_builder()
.children([offer_builder_base()
.attr("group-jid", group_jid.clone())
.children([NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "16000")
.build()])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::Offer {
group_jid: parsed_group,
..
} => {
assert_eq!(parsed_group, Some(group_jid));
}
other => panic!("expected Offer, got {other:?}"),
}
}
#[test]
fn offer_notice_group_audio_call() {
let node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "STANZA-ID-GROUP")
.attr("t", "1766847151")
.children([NodeBuilder::new("offer_notice")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "GROUP-CALL-ID")
.attr("media", "audio")
.attr("type", "group")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::OfferNotice {
call_id,
call_creator,
is_video,
is_group,
} => {
assert_eq!(call_id, "GROUP-CALL-ID");
assert_eq!(call_creator, fake_caller_lid());
assert!(!is_video);
assert!(is_group);
}
other => panic!("expected OfferNotice, got {other:?}"),
}
}
#[test]
fn offer_notice_video_flag() {
let node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "STANZA-ID-GROUP")
.attr("t", "1766847151")
.children([NodeBuilder::new("offer_notice")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "GROUP-CALL-ID")
.attr("media", "video")
.attr("type", "group")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::OfferNotice {
is_video, is_group, ..
} => {
assert!(is_video);
assert!(is_group);
}
other => panic!("expected OfferNotice, got {other:?}"),
}
}
#[test]
fn preaccept_accept_reject_variants() {
for (tag, expected_variant) in [
("preaccept", "pre_accept"),
("accept", "accept"),
("reject", "reject"),
] {
let node = base_call_builder()
.children([NodeBuilder::new(tag)
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
assert_eq!(call.action.call_id(), "CID");
let name = match call.action {
CallAction::PreAccept { .. } => "pre_accept",
CallAction::Accept { .. } => "accept",
CallAction::Reject { .. } => "reject",
_ => "other",
};
assert_eq!(name, expected_variant);
}
}
#[test]
fn preaccept_and_accept_preserve_audio_selection() {
for tag in ["preaccept", "accept"] {
let node = base_call_builder()
.children([NodeBuilder::new(tag)
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.children([audio_opus("8000")])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let audio = match call.action {
CallAction::PreAccept { audio, .. } | CallAction::Accept { audio, .. } => audio,
other => panic!("expected {tag}, got {other:?}"),
};
assert_eq!(
audio,
[CallAudioCodec {
enc: "opus".to_string(),
rate: 8_000,
}]
);
}
}
#[test]
fn terminate_with_duration() {
let node = base_call_builder()
.children([NodeBuilder::new("terminate")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.attr("reason", "timeout")
.attr("duration", "3670")
.attr("audio_duration", "3670")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::Terminate {
reason,
duration,
audio_duration,
..
} => {
assert_eq!(reason.as_deref(), Some("timeout"));
assert_eq!(duration, Some(3670));
assert_eq!(audio_duration, Some(3670));
}
other => panic!("expected Terminate, got {other:?}"),
}
}
#[test]
fn reject_preserves_a_busy_reason() {
let node = base_call_builder()
.children([NodeBuilder::new("reject")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.attr("count", "0")
.attr("reason", REJECT_REASON_BUSY)
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::Reject { reason, .. } => {
assert_eq!(reason.as_deref(), Some(REJECT_REASON_BUSY));
}
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn reject_without_a_reason_parses_as_none() {
let node = base_call_builder()
.children([NodeBuilder::new("reject")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::Reject { reason, .. } => assert_eq!(reason, None),
other => panic!("expected Reject, got {other:?}"),
}
}
#[test]
fn transport_and_relaylatency_are_parsed_not_dropped() {
let transport = base_call_builder()
.children([NodeBuilder::new("transport")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.attr("p2p-cand-round", "1")
.attr("transport-message-type", "3")
.children([NodeBuilder::new("net").attr("medium", "2").build()])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&transport)).unwrap().unwrap();
match call.action {
CallAction::Transport {
call_id,
p2p_cand_round,
transport_message_type,
..
} => {
assert_eq!(call_id, "CID");
assert_eq!(p2p_cand_round.as_deref(), Some("1"));
assert_eq!(transport_message_type.as_deref(), Some("3"));
}
other => panic!("expected Transport, got {other:?}"),
}
let relaylatency = base_call_builder()
.children([NodeBuilder::new("relaylatency")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&relaylatency)).unwrap().unwrap();
assert!(matches!(call.action, CallAction::RelayLatency { .. }));
}
#[test]
fn idless_call_stanza_parses() {
let transport = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("t", "1766847151")
.children([NodeBuilder::new("transport")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "CID")
.children([NodeBuilder::new("net").attr("medium", "2").build()])
.build()])
.build();
let call = parse_call_stanza(&as_ref(&transport)).unwrap().unwrap();
assert_eq!(call.stanza_id, "");
assert!(matches!(call.action, CallAction::Transport { .. }));
}
#[test]
fn transport_malformed_call_creator_errors() {
let node = base_call_builder()
.children([NodeBuilder::new("transport")
.attr("call-creator", "@@not-a-jid@@")
.attr("call-id", "CID")
.build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn relaylatency_malformed_call_creator_errors() {
let node = base_call_builder()
.children([NodeBuilder::new("relaylatency")
.attr("call-creator", "@@not-a-jid@@")
.attr("call-id", "CID")
.build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn unknown_action_returns_none() {
let node = base_call_builder()
.children([NodeBuilder::new("surprise").build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).unwrap().is_none());
}
#[test]
fn unknown_action_short_circuits_before_attr_validation() {
let node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "S")
.children([NodeBuilder::new("surprise").build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).unwrap().is_none());
}
#[test]
fn malformed_audio_missing_enc_errors() {
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("audio").attr("rate", "16000").build()])
.build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn malformed_audio_missing_rate_errors() {
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("audio").attr("enc", "opus").build()])
.build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn malformed_audio_rate_overflow_errors() {
let node = base_call_builder()
.children([offer_builder_base()
.children([NodeBuilder::new("audio")
.attr("enc", "opus")
.attr("rate", "4294967296") .build()])
.build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn malformed_missing_t_errors() {
let node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "STANZA-ID-0001")
.children([offer_builder_base().build()])
.build();
assert!(parse_call_stanza(&as_ref(&node)).is_err());
}
#[test]
fn offline_delivery_flag() {
let offline_node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "S")
.attr("t", "1766847151")
.attr("offline", "1")
.children([offer_builder_base().build()])
.build();
assert!(
parse_call_stanza(&as_ref(&offline_node))
.unwrap()
.unwrap()
.offline
);
let online_node = NodeBuilder::new("call")
.attr("from", fake_caller_lid())
.attr("id", "S")
.attr("t", "1766847151")
.attr("e", "1766847151")
.children([offer_builder_base().build()])
.build();
assert!(
!parse_call_stanza(&as_ref(&online_node))
.unwrap()
.unwrap()
.offline
);
}
#[test]
fn build_offer_ack_receipt_matches_wa_web_shape() {
let node = base_call_builder()
.children([offer_builder_base().build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let own = Jid::new("222222222222222", Server::Lid).with_device(42);
let receipt = build_offer_ack_receipt(&call, Some(&own)).unwrap();
assert_eq!(receipt.tag.as_ref(), "receipt");
let mut a = receipt.attrs();
assert_eq!(
a.required_string("to").unwrap(),
fake_caller_lid().to_string()
);
assert_eq!(a.required_string("id").unwrap(), "STANZA-ID-0001");
assert_eq!(a.required_string("from").unwrap(), own.to_string());
let offer = receipt.get_optional_child("offer").unwrap();
let mut oa = offer.attrs();
assert_eq!(oa.required_string("call-id").unwrap(), "CALL-ID-0001");
assert_eq!(
oa.required_string("call-creator").unwrap(),
fake_caller_lid().to_string()
);
}
#[test]
fn build_offer_ack_receipt_returns_none_for_non_offer() {
let node = base_call_builder()
.children([NodeBuilder::new("reject")
.attr("call-creator", fake_caller_lid())
.attr("call-id", "X")
.build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
assert!(build_offer_ack_receipt(&call, None).is_none());
}
#[test]
fn build_offer_ack_receipt_omits_from_when_own_ad_missing() {
let node = base_call_builder()
.children([offer_builder_base().build()])
.build();
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let receipt = build_offer_ack_receipt(&call, None).unwrap();
let mut a = receipt.attrs();
assert!(a.optional_string("from").is_none());
}
#[test]
fn audio_voip_settings_select_native_opus_pt120() {
let settings: serde_json::Value =
serde_json::from_slice(standard_opus_voip_settings(false)).unwrap();
assert_eq!(settings["encode"]["use_mlow_codec_v1"], "false");
assert_eq!(settings["options"]["enable_48khz_rtp_clock"], "false");
}
#[test]
fn audio_voip_settings_select_rfc7587_pt111() {
let settings: serde_json::Value =
serde_json::from_slice(standard_opus_voip_settings(true)).unwrap();
assert_eq!(settings["encode"]["use_mlow_codec_v1"], "false");
assert_eq!(settings["options"]["enable_48khz_rtp_clock"], "true");
}
#[test]
fn native_opus_accept_carries_directional_settings() {
let peer = peer();
let creator = creator();
let settings = standard_opus_voip_settings(false);
let accept = build_accept(&AcceptParams {
call_id: "CID",
to: &peer,
id: "ACCEPT-ID",
call_creator: &creator,
audio_rates: &["16000"],
relay_te: None,
rte: None,
voip_settings: Some(settings),
capability: Some(&CAPABILITY_STANDARD_OPUS_OFFER),
video: false,
peer_abtest_bucket: None,
peer_abtest_bucket_id_list: None,
});
assert_eq!(
child_tags(&accept),
["audio", "net", "encopt", "capability", "voip_settings"]
);
let accept_ref = accept.as_node_ref();
let action = &accept_ref.children().unwrap()[0];
let capability = action.get_optional_child("capability").unwrap();
assert_eq!(
capability.content_bytes(),
Some(CAPABILITY_STANDARD_OPUS_OFFER.as_slice())
);
let voip_settings = action.get_optional_child("voip_settings").unwrap();
assert_eq!(
voip_settings
.attrs()
.optional_string("uncompressed")
.as_deref(),
Some("1")
);
assert_eq!(voip_settings.content_bytes(), Some(settings));
}
fn peer() -> Jid {
Jid::new("111111111111111", Server::Lid)
}
fn creator() -> Jid {
Jid::new("222222222222222", Server::Lid).with_device(19)
}
fn child_tags(call: &Node) -> Vec<String> {
let r: NodeRef<'_> = call.as_node_ref();
let action = &r.children().unwrap()[0];
action
.children()
.unwrap()
.iter()
.map(|c| c.tag.as_ref().to_string())
.collect()
}
#[test]
fn offer_child_order_is_load_bearing() {
let peer = peer();
let creator = creator();
let dk = OfferDeviceKey {
device_jid: peer.clone(),
ciphertext: vec![1, 2, 3],
enc_type: "pkmsg".into(),
};
let call = build_offer(&OfferParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
device_keys: std::slice::from_ref(&dk),
privacy_token: Some(&[0xaa, 0xbb]),
capability: Some(&CAPABILITY_OFFER),
device_identity: Some(&[0xcc]),
id: Some("OFFER-STANZA-ID"),
multi_device: false,
video: false,
audio_rates: DEFAULT_AUDIO_RATES,
});
assert_eq!(
child_tags(&call),
[
"privacy",
"audio",
"audio",
"net",
"capability",
"enc",
"encopt",
"device-identity"
]
);
let r = call.as_node_ref();
assert_eq!(r.tag.as_ref(), "call");
assert_eq!(
r.attrs().optional_string("id").as_deref(),
Some("OFFER-STANZA-ID")
);
let offer = &r.children().unwrap()[0];
assert_eq!(offer.tag.as_ref(), "offer");
assert_eq!(
offer.attrs().optional_string("call-id").as_deref(),
Some("CID")
);
}
#[test]
fn offer_multi_device_uses_destination() {
let peer = peer();
let creator = creator();
let keys = vec![
OfferDeviceKey {
device_jid: peer.clone(),
ciphertext: vec![1],
enc_type: "pkmsg".into(),
},
OfferDeviceKey {
device_jid: creator.clone(),
ciphertext: vec![2],
enc_type: "msg".into(),
},
];
let call = build_offer(&OfferParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
device_keys: &keys,
privacy_token: None,
capability: None,
device_identity: None,
id: None,
multi_device: false,
video: false,
audio_rates: DEFAULT_AUDIO_RATES,
});
let tags = child_tags(&call);
assert!(tags.contains(&"destination".to_string()));
assert!(!tags.contains(&"enc".to_string()));
}
#[test]
fn offer_audio_rates_are_configurable() {
let peer = peer();
let creator = creator();
let dk = OfferDeviceKey {
device_jid: peer.clone(),
ciphertext: vec![1],
enc_type: "msg".into(),
};
let call = build_offer(&OfferParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
device_keys: std::slice::from_ref(&dk),
privacy_token: None,
capability: None,
device_identity: None,
id: None,
multi_device: false,
video: false,
audio_rates: &["8000"],
});
let call_ref = call.as_node_ref();
let offer = &call_ref.children().unwrap()[0];
let rates: Vec<_> = offer
.children()
.unwrap()
.iter()
.filter(|child| child.tag == "audio")
.map(|child| child.attrs().optional_u64("rate"))
.collect();
assert_eq!(rates, [Some(8_000)]);
}
#[test]
fn offer_multi_device_single_survivor_keeps_destination() {
let peer = peer();
let creator = creator();
let keys = vec![OfferDeviceKey {
device_jid: creator.clone(),
ciphertext: vec![2],
enc_type: "msg".into(),
}];
let call = build_offer(&OfferParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
device_keys: &keys,
privacy_token: None,
capability: None,
device_identity: None,
id: None,
multi_device: true,
video: false,
audio_rates: DEFAULT_AUDIO_RATES,
});
let tags = child_tags(&call);
assert!(tags.contains(&"destination".to_string()));
assert!(!tags.contains(&"enc".to_string()));
}
#[test]
fn accept_and_preaccept_shape() {
let peer = peer();
let creator = creator();
let accept = build_accept(&AcceptParams {
call_id: "CID",
to: &peer,
id: "ACCEPT-STANZA-ID",
call_creator: &creator,
audio_rates: &["16000"],
relay_te: Some(&[0u8; 6]),
rte: None,
voip_settings: None,
capability: Some(&CAPABILITY_OFFER),
video: false,
peer_abtest_bucket: None,
peer_abtest_bucket_id_list: None,
});
assert_eq!(
child_tags(&accept),
["audio", "te", "net", "encopt", "capability"]
);
assert_eq!(
accept
.as_node_ref()
.attrs()
.optional_string("id")
.as_deref(),
Some("ACCEPT-STANZA-ID")
);
let pre = build_preaccept(
"CID",
&peer,
&creator,
"abcd1234",
&["8000", "16000"],
false,
);
assert_eq!(child_tags(&pre), ["audio", "audio", "encopt", "capability"]);
assert_eq!(
pre.as_node_ref().attrs().optional_string("id").as_deref(),
Some("abcd1234")
);
let standard_opus = build_preaccept_with_capability(
"CID",
&peer,
&creator,
"abcd1234",
&["8000"],
&CAPABILITY_STANDARD_OPUS_PREACCEPT,
false,
);
let standard_ref = standard_opus.as_node_ref();
let action = &standard_ref.children().unwrap()[0];
let capability = action
.children()
.unwrap()
.iter()
.find(|child| child.tag == "capability")
.unwrap();
assert_eq!(
capability.content_bytes().unwrap(),
&CAPABILITY_STANDARD_OPUS_PREACCEPT
);
assert_eq!(
CAPABILITY_OFFER[5] ^ CAPABILITY_STANDARD_OPUS_OFFER[5],
0x80
);
}
#[test]
fn video_preaccept_advertises_video_and_video_capability() {
let peer = peer();
let creator = creator();
let pre = build_preaccept("CID", &peer, &creator, "abcd1234", &["16000"], true);
assert_eq!(child_tags(&pre), ["audio", "video", "encopt", "capability"]);
let pre_ref = pre.as_node_ref();
let action = &pre_ref.children().unwrap()[0];
let video = action.get_optional_child("video").unwrap();
assert_eq!(
video.attrs().optional_string("dec").as_deref(),
Some("H264")
);
assert_eq!(
video.attrs().optional_string("screen_width").as_deref(),
Some("0")
);
let cap = action.get_optional_child("capability").unwrap();
assert_eq!(cap.content_bytes().unwrap(), &CAPABILITY_OFFER);
let audio_pre = build_preaccept("CID", &peer, &creator, "id", &["16000"], false);
let audio_ref = audio_pre.as_node_ref();
let audio_action = &audio_ref.children().unwrap()[0];
assert_eq!(
audio_action
.get_optional_child("capability")
.unwrap()
.content_bytes()
.unwrap(),
&CAPABILITY_PREACCEPT
);
}
#[test]
fn transport_net_protocol_rule() {
let peer = peer();
let creator = creator();
let t1 = build_transport(&TransportParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
p2p_cand_round: Some("1"),
transport_message_type: Some("1"),
relay_te: Some(&[9u8; 6]),
});
let r = t1.as_node_ref();
let action = &r.children().unwrap()[0];
assert_eq!(
action
.attrs()
.optional_string("transport-message-type")
.as_deref(),
Some("1")
);
let net = action.get_optional_child("net").unwrap();
assert_eq!(
net.attrs().optional_string("protocol").as_deref(),
Some("0")
);
let t9 = build_transport(&TransportParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
p2p_cand_round: None,
transport_message_type: Some("9"),
relay_te: None,
});
let r9 = t9.as_node_ref();
let net9 = r9.children().unwrap()[0].get_optional_child("net").unwrap();
assert!(net9.attrs().optional_string("protocol").is_none());
}
#[test]
fn relaylatency_encoding_and_heartbeat() {
let peer = peer();
let creator = creator();
assert_eq!(encode_latency(45), "33554477");
let rl = build_relay_latency(&RelayLatencyParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
latency_ms: 45,
relay_name: "gru1c02",
address_bytes: &[1, 2, 3, 4, 5, 6],
devices: std::slice::from_ref(&peer),
});
let r = rl.as_node_ref();
let action = &r.children().unwrap()[0];
let te = action.get_optional_child("te").unwrap();
assert_eq!(
te.attrs().optional_string("latency").as_deref(),
Some("33554477")
);
assert_eq!(
te.attrs().optional_string("relay_name").as_deref(),
Some("gru1c02")
);
assert!(action.get_optional_child("destination").is_some());
let hb = build_heartbeat("CALLID", &creator, "DEADBEEF");
assert_eq!(
hb.as_node_ref().attrs().optional_string("to").as_deref(),
Some("CALLID@call")
);
assert_eq!(
hb.as_node_ref().attrs().optional_string("id").as_deref(),
Some("DEADBEEF")
);
}
#[test]
fn terminate_is_per_device_with_id_and_no_destination() {
let dev = peer().with_device(3);
let creator = creator();
let term = build_terminate(&TerminateParams {
call_id: "CID",
to: &dev,
id: Some("term-1"),
call_creator: &creator,
reason: Some("accepted_elsewhere"),
});
let r = term.as_node_ref();
assert_eq!(
r.attrs().optional_string("to").as_deref(),
Some(dev.to_string().as_str()),
"addressed to the device JID, not the bare peer"
);
assert_eq!(r.attrs().optional_string("id").as_deref(), Some("term-1"));
let action = &r.children().unwrap()[0];
assert_eq!(action.tag, "terminate");
assert_eq!(
action.attrs().optional_string("reason").as_deref(),
Some("accepted_elsewhere")
);
assert!(
action.get_optional_child("destination").is_none(),
"terminate must not use a <destination> block"
);
}
#[test]
fn video_state_wire_enum_round_trips() {
let known = [
(0, VideoState::Disabled),
(1, VideoState::Enabled),
(3, VideoState::UpgradeRequest),
(4, VideoState::UpgradeAccept),
(5, VideoState::UpgradeReject),
(6, VideoState::Stopped),
(8, VideoState::UpgradeCancel),
(11, VideoState::UpgradeRequestV2),
];
for (code, state) in known {
assert_eq!(VideoState::from(code), state);
assert_eq!(state.code(), code);
}
assert_eq!(VideoState::from(99), VideoState::Unknown(99));
assert_eq!(VideoState::Unknown(99).code(), 99);
}
fn video_call_node(attrs: &[(&'static str, &str)]) -> Node {
let mut video = NodeBuilder::new("video")
.attr("call-id", "CID")
.attr("call-creator", fake_caller_lid());
for (k, v) in attrs {
video = video.attr(k, v.to_string());
}
base_call_builder().children([video.build()]).build()
}
#[test]
fn parse_video_upgrade_request() {
let node = video_call_node(&[("state", "11"), ("dec", "H264"), ("voip_settings", "video")]);
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
match call.action {
CallAction::VideoState {
state,
dec,
orientation,
..
} => {
assert_eq!(state, VideoState::UpgradeRequestV2);
assert_eq!(dec.as_deref(), Some("H264"));
assert_eq!(orientation, None);
}
other => panic!("expected VideoState, got {other:?}"),
}
}
#[test]
fn generated_call_action_tags_drive_dispatch_and_serialization() {
assert_eq!(
CallActionTag::try_from("group_update").expect("known generated tag"),
CallActionTag::GroupUpdate
);
assert!(CallActionTag::try_from("future_call_action").is_err());
let action = CallAction::RelayLatency {
call_id: "CID".to_string(),
call_creator: fake_caller_lid(),
};
let value = serde_json::to_value(action).expect("serialize call action");
assert_eq!(value["type"], "relaylatency");
let optional = CallAction::Reject {
call_id: "CID".to_string(),
call_creator: fake_caller_lid(),
reason: None,
};
let value = serde_json::to_value(optional).expect("serialize optional fields");
assert!(
value.get("reason").is_none(),
"tagged WireEnum preserves the frozen omission semantics for None"
);
}
#[test]
fn parse_video_accept_downgrade_and_unknown_states() {
let accept = video_call_node(&[("state", "4"), ("dec", "H264,AV1")]);
let call = parse_call_stanza(&as_ref(&accept)).unwrap().unwrap();
assert!(matches!(
call.action,
CallAction::VideoState {
state: VideoState::UpgradeAccept,
..
}
));
let downgrade = video_call_node(&[("state", "6"), ("device_orientation", "2")]);
let call = parse_call_stanza(&as_ref(&downgrade)).unwrap().unwrap();
match call.action {
CallAction::VideoState {
state, orientation, ..
} => {
assert_eq!(state, VideoState::Stopped);
assert_eq!(orientation, Some(2));
}
other => panic!("expected VideoState, got {other:?}"),
}
let malformed = video_call_node(&[("state", "1"), ("device_orientation", "255")]);
let call = parse_call_stanza(&as_ref(&malformed)).unwrap().unwrap();
assert!(matches!(
call.action,
CallAction::VideoState {
orientation: None,
..
}
));
let future = video_call_node(&[("state", "42")]);
let call = parse_call_stanza(&as_ref(&future)).unwrap().unwrap();
assert!(matches!(
call.action,
CallAction::VideoState {
state: VideoState::Unknown(42),
..
}
));
}
#[test]
fn parse_video_missing_or_garbage_state_errors() {
let missing = video_call_node(&[("dec", "H264")]);
assert!(parse_call_stanza(&as_ref(&missing)).is_err());
let garbage = video_call_node(&[("state", "not-a-number")]);
assert!(parse_call_stanza(&as_ref(&garbage)).is_err());
}
#[test]
fn build_video_state_upgrade_carries_marker_downgrade_does_not() {
let peer = peer();
let creator = creator();
let upgrade = build_video_state(&VideoStateParams {
call_id: "CID",
to: &peer,
id: "VIDEO-WRAP-ID",
call_creator: &creator,
state: VideoState::UpgradeRequestV2,
dec: Some("H264"),
device_orientation: None,
});
let r = upgrade.as_node_ref();
assert_eq!(
r.attrs().optional_string("to").as_deref(),
Some(peer.to_string().as_str())
);
assert_eq!(
r.attrs().optional_string("id").as_deref(),
Some("VIDEO-WRAP-ID"),
"the <call> wrapper must carry the id the peer's typed ack correlates to"
);
let action = &r.children().unwrap()[0];
assert_eq!(action.tag, "video");
assert_eq!(
action.attrs().optional_string("state").as_deref(),
Some("11")
);
assert_eq!(
action.attrs().optional_string("dec").as_deref(),
Some("H264")
);
assert_eq!(
action.attrs().optional_string("voip_settings").as_deref(),
Some("video"),
"the upgrade request must carry the marker attr"
);
let downgrade = build_video_state(&VideoStateParams {
call_id: "CID",
to: &peer,
id: "VIDEO-WRAP-ID-2",
call_creator: &creator,
state: VideoState::Stopped,
dec: None,
device_orientation: Some(0),
});
let action = downgrade.as_node_ref().children().unwrap()[0].to_owned();
let ar = action.as_node_ref();
assert_eq!(ar.attrs().optional_string("state").as_deref(), Some("6"));
assert_eq!(
ar.attrs().optional_string("voip_settings"),
None,
"a downgrade must NOT carry the marker (it re-arms the peer's video)"
);
assert_eq!(
ar.attrs().optional_string("device_orientation").as_deref(),
Some("0")
);
let wrapped = base_call_builder()
.children([upgrade.as_node_ref().children().unwrap()[0].to_owned()])
.build();
let call = parse_call_stanza(&as_ref(&wrapped)).unwrap().unwrap();
assert!(matches!(
call.action,
CallAction::VideoState {
state: VideoState::UpgradeRequestV2,
..
}
));
}
#[test]
fn offer_and_accept_video_advertisement() {
let peer = peer();
let creator = creator();
let dk = OfferDeviceKey {
device_jid: peer.clone(),
ciphertext: vec![1, 2, 3],
enc_type: "msg".into(),
};
let base = OfferParams {
call_id: "CID",
to: &peer,
call_creator: &creator,
device_keys: std::slice::from_ref(&dk),
privacy_token: None,
capability: None,
device_identity: None,
id: None,
multi_device: false,
video: true,
audio_rates: DEFAULT_AUDIO_RATES,
};
let offer = build_offer(&base);
assert_eq!(
child_tags(&offer),
["audio", "audio", "video", "net", "enc", "encopt"]
);
let call = parse_call_stanza(&offer.as_node_ref()).ok().flatten();
assert!(
call.is_none(),
"an outbound offer has no from/t; shape-only check above"
);
let no_video = build_offer(&OfferParams {
video: false,
..base
});
assert!(!child_tags(&no_video).contains(&"video".to_string()));
let accept = build_accept(&AcceptParams {
call_id: "CID",
to: &peer,
id: "ACCEPT-ID",
call_creator: &creator,
audio_rates: &["16000"],
relay_te: None,
rte: None,
voip_settings: None,
capability: None,
video: true,
peer_abtest_bucket: None,
peer_abtest_bucket_id_list: None,
});
assert_eq!(child_tags(&accept), ["audio", "video", "net", "encopt"]);
let vnode = accept.as_node_ref().children().unwrap()[0]
.children()
.unwrap()
.iter()
.find(|c| c.tag == "video")
.unwrap()
.to_owned();
let vr = vnode.as_node_ref();
assert_eq!(vr.attrs().optional_string("dec").as_deref(), Some("H264"));
assert_eq!(
vr.attrs().optional_string("enc"),
None,
"accept <video> must not advertise enc"
);
assert_eq!(vr.attrs().optional_string("screen_width"), None);
let ovnode = offer.as_node_ref().children().unwrap()[0]
.children()
.unwrap()
.iter()
.find(|c| c.tag == "video")
.unwrap()
.to_owned();
let ovr = ovnode.as_node_ref();
assert_eq!(ovr.attrs().optional_string("enc").as_deref(), Some("h264"));
assert_eq!(ovr.attrs().optional_string("dec").as_deref(), Some("h264"));
let accept = build_accept(&AcceptParams {
call_id: "CID",
to: &peer,
id: "ACCEPT-ID-METADATA",
call_creator: &creator,
audio_rates: &["16000"],
relay_te: None,
rte: None,
voip_settings: None,
capability: None,
video: true,
peer_abtest_bucket: Some("video_interop_holdout"),
peer_abtest_bucket_id_list: Some("110001,110002"),
});
assert_eq!(
child_tags(&accept),
["audio", "video", "net", "encopt", "metadata"]
);
let accept_ref = accept.as_node_ref();
let metadata = accept_ref.children().unwrap()[0]
.get_optional_child("metadata")
.unwrap();
assert_eq!(
metadata
.attrs()
.optional_string("peer_abtest_bucket")
.as_deref(),
Some("video_interop_holdout")
);
assert_eq!(
metadata
.attrs()
.optional_string("peer_abtest_bucket_id_list")
.as_deref(),
Some("110001,110002")
);
}
#[test]
fn video_ack_is_typed_and_requires_a_stanza_id() {
let node = video_call_node(&[("state", "11"), ("voip_settings", "video")]);
let call = parse_call_stanza(&as_ref(&node)).unwrap().unwrap();
let ack =
build_call_video_ack(&call, &as_ref(&node)).expect("ack for an id-carrying stanza");
let r = ack.as_node_ref();
assert_eq!(r.tag, "ack");
assert_eq!(r.attrs().optional_string("class").as_deref(), Some("call"));
assert_eq!(
r.attrs().optional_string("type").as_deref(),
Some("video"),
"the ack must be typed: an untyped ack makes the requester revert the upgrade"
);
assert_eq!(
r.attrs().optional_string("id").as_deref(),
Some("STANZA-ID-0001")
);
assert_eq!(
r.attrs().optional_string("to").as_deref(),
Some(fake_caller_lid().to_string().as_str())
);
let participant = fake_caller_lid().with_device(3);
let recipient = fake_caller_pn();
let routed = base_call_builder()
.attr("participant", &participant)
.attr("recipient", &recipient)
.children([NodeBuilder::new("video")
.attr("call-id", "CID")
.attr("call-creator", fake_caller_lid())
.attr("state", "11")
.build()])
.build();
let routed_call = parse_call_stanza(&as_ref(&routed)).unwrap().unwrap();
let routed_ack = build_call_video_ack(&routed_call, &as_ref(&routed)).unwrap();
let routed_ref = routed_ack.as_node_ref();
assert_eq!(
routed_ref.attrs().optional_jid("participant"),
Some(participant)
);
assert_eq!(
routed_ref.attrs().optional_jid("recipient"),
Some(recipient)
);
let mut idless = call.clone();
idless.stanza_id = String::new();
assert!(build_call_video_ack(&idless, &as_ref(&node)).is_none());
}
}