use bytes::Bytes;
pub fn sdp(s: impl Into<String>) -> (String, Bytes) {
let body: String = s.into();
(
"application/sdp".to_string(),
Bytes::from(body.into_bytes()),
)
}
pub fn dtmf_relay(signal: char, duration_ms: u32) -> (String, Bytes) {
let body = format!("Signal={}\r\nDuration={}\r\n", signal, duration_ms);
(
"application/dtmf-relay".to_string(),
Bytes::from(body.into_bytes()),
)
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct Presence {
pub entity: String,
pub basic: String,
pub note: Option<String>,
}
impl Presence {
pub fn new(entity: impl Into<String>, basic: impl Into<String>) -> Self {
Self {
entity: entity.into(),
basic: basic.into(),
note: None,
}
}
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
}
pub fn pidf_xml(presence: &Presence) -> (String, Bytes) {
let note_block = match presence.note.as_ref() {
Some(n) => format!(" <note>{}</note>\n", xml_escape(n)),
None => String::new(),
};
let body = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<presence xmlns=\"urn:ietf:params:xml:ns:pidf\" entity=\"{}\">\n\
\x20 <tuple id=\"t1\">\n\
\x20 <status><basic>{}</basic></status>\n\
{}\
\x20 </tuple>\n\
</presence>\n",
xml_escape(&presence.entity),
xml_escape(&presence.basic),
note_block,
);
(
"application/pidf+xml".to_string(),
Bytes::from(body.into_bytes()),
)
}
pub fn simple_message_summary(
account_uri: impl Into<String>,
messages_waiting: bool,
new_count: u32,
old_count: u32,
) -> (String, Bytes) {
let body = format!(
"Messages-Waiting: {}\r\n\
Message-Account: {}\r\n\
Voice-Message: {}/{}\r\n",
if messages_waiting { "yes" } else { "no" },
account_uri.into(),
new_count,
old_count,
);
(
"application/simple-message-summary".to_string(),
Bytes::from(body.into_bytes()),
)
}
pub fn isup_l3(bytes: impl Into<Bytes>) -> (String, Bytes) {
("application/isup".to_string(), bytes.into())
}
fn xml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdp_returns_application_sdp() {
let (ct, body) = sdp("v=0\r\no=- 1 1 IN IP4 1.2.3.4\r\n");
assert_eq!(ct, "application/sdp");
assert!(body.starts_with(b"v=0"));
}
#[test]
fn dtmf_relay_emits_signal_and_duration() {
let (ct, body) = dtmf_relay('5', 200);
assert_eq!(ct, "application/dtmf-relay");
let rendered = std::str::from_utf8(&body).unwrap();
assert!(rendered.contains("Signal=5"));
assert!(rendered.contains("Duration=200"));
}
#[test]
fn pidf_xml_escapes_entity_and_note() {
let presence =
Presence::new("sip:alice@example.com", "open").with_note("In a <meeting> & away");
let (ct, body) = pidf_xml(&presence);
assert_eq!(ct, "application/pidf+xml");
let rendered = std::str::from_utf8(&body).unwrap();
assert!(rendered.contains("entity=\"sip:alice@example.com\""));
assert!(rendered.contains("In a <meeting> & away"));
}
#[test]
fn simple_message_summary_emits_required_headers() {
let (ct, body) = simple_message_summary("sip:vm@example.com", true, 3, 5);
assert_eq!(ct, "application/simple-message-summary");
let rendered = std::str::from_utf8(&body).unwrap();
assert!(rendered.contains("Messages-Waiting: yes"));
assert!(rendered.contains("Voice-Message: 3/5"));
}
#[test]
fn isup_l3_preserves_bytes() {
let raw = Bytes::from(&b"\x01\x02\x03"[..]);
let (ct, body) = isup_l3(raw.clone());
assert_eq!(ct, "application/isup");
assert_eq!(body, raw);
}
}