use std::collections::HashMap;
use std::sync::Arc;
use futures_util::Stream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::client::{CallOptions, Client};
use crate::error::{Error, Result};
use crate::pagination::{auto_page, paginate, ItemMapper, ListParams, Page, PageFetcher};
use crate::query::QueryBuilder;
use crate::resources::escape;
use crate::resources::sip::{SipCallStatus, SipDirection, SipIncludeHeaders, SipMediaEncryption};
const PATH: &str = "/v2/sip/call";
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SipCall {
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub id: String,
#[serde(rename = "type")]
pub direction: Option<SipDirection>,
pub transport: Option<String>,
pub provider: Option<String>,
pub gateway_id: Option<String>,
pub rule_id: Option<String>,
pub room_id: Option<String>,
pub to: Option<String>,
pub from: Option<String>,
pub transfer: Option<Value>,
pub session_id: Option<String>,
pub status: Option<SipCallStatus>,
pub metadata: Option<Map<String, Value>>,
pub start: Option<String>,
pub end: Option<String>,
#[serde(default, deserialize_with = "crate::common::null_to_default")]
pub timelog: Vec<Value>,
pub region: Option<String>,
pub additional_details: Option<Value>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateSipCallParams {
pub call_to: String,
pub call_from: Option<String>,
pub room_id: Option<String>,
pub participant_id: Option<String>,
pub gateway_id: Option<String>,
pub routing_rule_id: Option<String>,
pub participant: Option<Map<String, Value>>,
pub metadata: Option<Map<String, Value>>,
pub record_audio: Option<bool>,
pub wait_until_answered: Option<bool>,
pub ringing_timeout: Option<u32>,
pub dtmf: Option<bool>,
pub max_call_duration: Option<u32>,
pub media_encryption: Option<SipMediaEncryption>,
pub hide_phone_number: Option<bool>,
pub headers: HashMap<String, String>,
pub include_headers: Option<SipIncludeHeaders>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateSipCallWire {
sip_call_to: String,
#[serde(skip_serializing_if = "Option::is_none")]
sip_call_from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
destination_room_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
participant: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
gateway_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
routing_rule_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
record_audio: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
wait_until_answered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
ringing_timeout: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
dtmf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
max_call_duration: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
media_encryption: Option<SipMediaEncryption>,
#[serde(skip_serializing_if = "Option::is_none")]
hide_phone_number: Option<bool>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
include_headers: Option<SipIncludeHeaders>,
}
impl From<CreateSipCallParams> for CreateSipCallWire {
fn from(params: CreateSipCallParams) -> Self {
let participant = params.participant.or_else(|| {
params.participant_id.map(|id| {
let mut map = Map::new();
map.insert("id".to_string(), Value::String(id));
map
})
});
Self {
sip_call_to: params.call_to,
sip_call_from: params.call_from,
destination_room_id: params.room_id,
participant,
gateway_id: params.gateway_id,
routing_rule_id: params.routing_rule_id,
metadata: params.metadata,
record_audio: params.record_audio,
wait_until_answered: params.wait_until_answered,
ringing_timeout: params.ringing_timeout,
dtmf: params.dtmf,
max_call_duration: params.max_call_duration,
media_encryption: params.media_encryption,
hide_phone_number: params.hide_phone_number,
headers: params.headers,
include_headers: params.include_headers,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TransferSipCallParams {
pub to: Option<String>,
pub participant: Option<Map<String, Value>>,
pub play_dialtone: Option<bool>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct TransferWire<'a> {
call_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
transfer_to: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
participant: Option<&'a Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
play_dialtone: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct SwitchRoomParams {
pub room_id: String,
pub participant_id: Option<String>,
pub token: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SwitchRoomWire<'a> {
call_id: &'a str,
room_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
participant_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<&'a str>,
}
#[derive(Debug, Clone, Default)]
pub struct SipCallListParams {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub cursor: Option<String>,
pub room_id: Option<String>,
pub session_id: Option<String>,
pub id: Option<String>,
pub gateway_id: Option<String>,
pub rule_id: Option<String>,
pub direction: Option<SipDirection>,
pub search: Option<String>,
pub start_date: Option<i64>,
pub end_date: Option<i64>,
}
impl SipCallListParams {
fn pagination(&self) -> ListParams {
ListParams {
page: self.page,
per_page: self.per_page,
cursor: self.cursor.clone(),
}
}
}
fn call_item_mapper() -> ItemMapper<SipCall> {
Arc::new(|value: Value| {
let mut call: SipCall =
serde_json::from_value(value).map_err(|e| Error::decode("SIP call list item", e))?;
if call.id.is_empty() {
if let Some(call_id) = call.extra.get("callId").and_then(Value::as_str) {
call.id = call_id.to_string();
}
}
Ok(call)
})
}
#[derive(Debug, Clone, Copy)]
pub struct SipCallsResource<'a> {
client: &'a Client,
}
impl<'a> SipCallsResource<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, params: CreateSipCallParams) -> Result<SipCall> {
if params.call_to.is_empty() {
return Err(Error::validation(
"sip.calls().create() requires call_to, the destination number",
));
}
let body = CreateSipCallWire::from(params);
self.client
.data(Method::POST, PATH, CallOptions::json(&body)?)
.await
}
pub async fn list(&self, params: SipCallListParams) -> Result<Page<SipCall>> {
paginate(
self.fetcher(¶ms),
¶ms.pagination(),
"data",
Some(call_item_mapper()),
)
.await
}
pub fn list_stream(
&self,
params: SipCallListParams,
) -> impl Stream<Item = Result<SipCall>> + Send {
auto_page(
self.fetcher(¶ms),
params.pagination(),
"data",
Some(call_item_mapper()),
)
}
pub async fn get(&self, call_id: &str) -> Result<SipCall> {
let path = format!("{PATH}/{}", escape(call_id));
self.client
.data(Method::GET, &path, CallOptions::new())
.await
}
pub async fn transfer(&self, call_id: &str, params: TransferSipCallParams) -> Result<SipCall> {
let body = TransferWire {
call_id,
transfer_to: params.to.as_deref(),
participant: params.participant.as_ref(),
play_dialtone: params.play_dialtone,
};
let path = format!("{PATH}/transfer");
self.client
.data(Method::POST, &path, CallOptions::json(&body)?)
.await
}
pub async fn end(&self, call_id: &str) -> Result<SipCall> {
let body = serde_json::json!({ "callId": call_id });
let path = format!("{PATH}/end");
self.client
.data(Method::POST, &path, CallOptions::json(&body)?)
.await
}
pub async fn switch_room(&self, call_id: &str, params: SwitchRoomParams) -> Result<SipCall> {
let body = SwitchRoomWire {
call_id,
room_id: ¶ms.room_id,
participant_id: params.participant_id.as_deref(),
token: params.token.as_deref(),
};
let path = format!("{PATH}/switch-room");
self.client
.data(Method::POST, &path, CallOptions::json(&body)?)
.await
}
fn fetcher(&self, params: &SipCallListParams) -> PageFetcher {
let client = self.client.clone();
let params = params.clone();
Arc::new(move |page, per_page| {
let client = client.clone();
let params = params.clone();
Box::pin(async move {
let query = QueryBuilder::new()
.opt("page", page)
.opt("perPage", per_page)
.opt_str("roomId", params.room_id.as_deref())
.opt_str("sessionId", params.session_id.as_deref())
.opt_str("id", params.id.as_deref())
.opt_str("gatewayId", params.gateway_id.as_deref())
.opt_str("ruleId", params.rule_id.as_deref())
.opt_str("type", params.direction.as_ref().map(SipDirection::as_str))
.opt_str("search", params.search.as_deref())
.opt("startDate", params.start_date)
.opt("endDate", params.end_date)
.into_pairs();
client
.json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
.await
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn wire(params: CreateSipCallParams) -> Value {
serde_json::to_value(CreateSipCallWire::from(params)).unwrap()
}
#[test]
fn create_renames_call_to_and_call_from() {
let body = wire(CreateSipCallParams {
call_to: "+15550001".into(),
call_from: Some("+15550002".into()),
room_id: Some("r-1".into()),
..Default::default()
});
assert_eq!(
body,
json!({
"sipCallTo": "+15550001",
"sipCallFrom": "+15550002",
"destinationRoomId": "r-1",
})
);
}
#[test]
fn participant_id_is_sugar_for_a_participant_object() {
let body = wire(CreateSipCallParams {
call_to: "+1".into(),
participant_id: Some("p-1".into()),
..Default::default()
});
assert_eq!(body["participant"], json!({"id": "p-1"}));
}
#[test]
fn an_explicit_participant_wins_over_participant_id() {
let mut participant = Map::new();
participant.insert("id".into(), json!("explicit"));
participant.insert("name".into(), json!("Ada"));
let body = wire(CreateSipCallParams {
call_to: "+1".into(),
participant_id: Some("sugar".into()),
participant: Some(participant),
..Default::default()
});
assert_eq!(body["participant"]["id"], json!("explicit"));
assert_eq!(body["participant"]["name"], json!("Ada"));
}
#[test]
fn absent_options_are_omitted() {
let body = wire(CreateSipCallParams {
call_to: "+1".into(),
..Default::default()
});
assert_eq!(body, json!({"sipCallTo": "+1"}));
}
#[test]
fn the_list_mapper_surfaces_call_id_as_id() {
let mapper = call_item_mapper();
let call = mapper(json!({"callId": "c-1", "to": "+1"})).unwrap();
assert_eq!(call.id, "c-1");
let call = mapper(json!({"id": "real", "callId": "c-1"})).unwrap();
assert_eq!(call.id, "real");
}
#[test]
fn transfer_renames_to_as_transfer_to() {
let body = serde_json::to_value(TransferWire {
call_id: "c-1",
transfer_to: Some("+15550003"),
participant: None,
play_dialtone: Some(true),
})
.unwrap();
assert_eq!(
body,
json!({"callId": "c-1", "transferTo": "+15550003", "playDialtone": true})
);
}
}