1use std::collections::HashMap;
4use std::sync::Arc;
5
6use futures_util::Stream;
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::client::{CallOptions, Client};
12use crate::error::{Error, Result};
13use crate::pagination::{auto_page, paginate, ItemMapper, ListParams, Page, PageFetcher};
14use crate::query::QueryBuilder;
15use crate::resources::escape;
16use crate::resources::sip::{SipCallStatus, SipDirection, SipIncludeHeaders, SipMediaEncryption};
17
18const PATH: &str = "/v2/sip/call";
19
20#[derive(Debug, Clone, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct SipCall {
24 #[serde(default, deserialize_with = "crate::common::null_to_default")]
28 pub id: String,
29 #[serde(rename = "type")]
31 pub direction: Option<SipDirection>,
32 pub transport: Option<String>,
34 pub provider: Option<String>,
36 pub gateway_id: Option<String>,
38 pub rule_id: Option<String>,
40 pub room_id: Option<String>,
42 pub to: Option<String>,
44 pub from: Option<String>,
46 pub transfer: Option<Value>,
48 pub session_id: Option<String>,
50 pub status: Option<SipCallStatus>,
52 pub metadata: Option<Map<String, Value>>,
54 pub start: Option<String>,
56 pub end: Option<String>,
58 #[serde(default, deserialize_with = "crate::common::null_to_default")]
60 pub timelog: Vec<Value>,
61 pub region: Option<String>,
63 pub additional_details: Option<Value>,
65 #[serde(flatten)]
67 pub extra: Map<String, Value>,
68}
69
70#[derive(Debug, Clone, Default)]
79pub struct CreateSipCallParams {
80 pub call_to: String,
82 pub call_from: Option<String>,
84 pub room_id: Option<String>,
86 pub participant_id: Option<String>,
90 pub gateway_id: Option<String>,
92 pub routing_rule_id: Option<String>,
94 pub participant: Option<Map<String, Value>>,
96 pub metadata: Option<Map<String, Value>>,
98 pub record_audio: Option<bool>,
100 pub wait_until_answered: Option<bool>,
102 pub ringing_timeout: Option<u32>,
104 pub dtmf: Option<bool>,
106 pub max_call_duration: Option<u32>,
108 pub media_encryption: Option<SipMediaEncryption>,
110 pub hide_phone_number: Option<bool>,
112 pub headers: HashMap<String, String>,
114 pub include_headers: Option<SipIncludeHeaders>,
116}
117
118#[derive(Debug, Serialize)]
121#[serde(rename_all = "camelCase")]
122struct CreateSipCallWire {
123 sip_call_to: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 sip_call_from: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 destination_room_id: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 participant: Option<Map<String, Value>>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 gateway_id: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 routing_rule_id: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 metadata: Option<Map<String, Value>>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 record_audio: Option<bool>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 wait_until_answered: Option<bool>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 ringing_timeout: Option<u32>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 dtmf: Option<bool>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 max_call_duration: Option<u32>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 media_encryption: Option<SipMediaEncryption>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 hide_phone_number: Option<bool>,
150 #[serde(skip_serializing_if = "HashMap::is_empty")]
151 headers: HashMap<String, String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 include_headers: Option<SipIncludeHeaders>,
154}
155
156impl From<CreateSipCallParams> for CreateSipCallWire {
157 fn from(params: CreateSipCallParams) -> Self {
158 let participant = params.participant.or_else(|| {
160 params.participant_id.map(|id| {
161 let mut map = Map::new();
162 map.insert("id".to_string(), Value::String(id));
163 map
164 })
165 });
166
167 Self {
168 sip_call_to: params.call_to,
169 sip_call_from: params.call_from,
170 destination_room_id: params.room_id,
171 participant,
172 gateway_id: params.gateway_id,
173 routing_rule_id: params.routing_rule_id,
174 metadata: params.metadata,
175 record_audio: params.record_audio,
176 wait_until_answered: params.wait_until_answered,
177 ringing_timeout: params.ringing_timeout,
178 dtmf: params.dtmf,
179 max_call_duration: params.max_call_duration,
180 media_encryption: params.media_encryption,
181 hide_phone_number: params.hide_phone_number,
182 headers: params.headers,
183 include_headers: params.include_headers,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Default)]
190pub struct TransferSipCallParams {
191 pub to: Option<String>,
193 pub participant: Option<Map<String, Value>>,
195 pub play_dialtone: Option<bool>,
197}
198
199#[derive(Debug, Serialize)]
201#[serde(rename_all = "camelCase")]
202struct TransferWire<'a> {
203 call_id: &'a str,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 transfer_to: Option<&'a str>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 participant: Option<&'a Map<String, Value>>,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 play_dialtone: Option<bool>,
210}
211
212#[derive(Debug, Clone, Default)]
214pub struct SwitchRoomParams {
215 pub room_id: String,
217 pub participant_id: Option<String>,
219 pub token: Option<String>,
221}
222
223#[derive(Debug, Serialize)]
224#[serde(rename_all = "camelCase")]
225struct SwitchRoomWire<'a> {
226 call_id: &'a str,
227 room_id: &'a str,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 participant_id: Option<&'a str>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 token: Option<&'a str>,
232}
233
234#[derive(Debug, Clone, Default)]
236pub struct SipCallListParams {
237 pub page: Option<u32>,
239 pub per_page: Option<u32>,
241 pub cursor: Option<String>,
243 pub room_id: Option<String>,
245 pub session_id: Option<String>,
247 pub id: Option<String>,
249 pub gateway_id: Option<String>,
251 pub rule_id: Option<String>,
253 pub direction: Option<SipDirection>,
255 pub search: Option<String>,
257 pub start_date: Option<i64>,
259 pub end_date: Option<i64>,
261}
262
263impl SipCallListParams {
264 fn pagination(&self) -> ListParams {
265 ListParams {
266 page: self.page,
267 per_page: self.per_page,
268 cursor: self.cursor.clone(),
269 }
270 }
271}
272
273fn call_item_mapper() -> ItemMapper<SipCall> {
276 Arc::new(|value: Value| {
277 let mut call: SipCall =
278 serde_json::from_value(value).map_err(|e| Error::decode("SIP call list item", e))?;
279 if call.id.is_empty() {
280 if let Some(call_id) = call.extra.get("callId").and_then(Value::as_str) {
281 call.id = call_id.to_string();
282 }
283 }
284 Ok(call)
285 })
286}
287
288#[derive(Debug, Clone, Copy)]
290pub struct SipCallsResource<'a> {
291 client: &'a Client,
292}
293
294impl<'a> SipCallsResource<'a> {
295 pub(crate) fn new(client: &'a Client) -> Self {
296 Self { client }
297 }
298
299 pub async fn create(&self, params: CreateSipCallParams) -> Result<SipCall> {
301 if params.call_to.is_empty() {
302 return Err(Error::validation(
303 "sip.calls().create() requires call_to, the destination number",
304 ));
305 }
306 let body = CreateSipCallWire::from(params);
307 self.client
308 .data(Method::POST, PATH, CallOptions::json(&body)?)
309 .await
310 }
311
312 pub async fn list(&self, params: SipCallListParams) -> Result<Page<SipCall>> {
314 paginate(
315 self.fetcher(¶ms),
316 ¶ms.pagination(),
317 "data",
318 Some(call_item_mapper()),
319 )
320 .await
321 }
322
323 pub fn list_stream(
325 &self,
326 params: SipCallListParams,
327 ) -> impl Stream<Item = Result<SipCall>> + Send {
328 auto_page(
329 self.fetcher(¶ms),
330 params.pagination(),
331 "data",
332 Some(call_item_mapper()),
333 )
334 }
335
336 pub async fn get(&self, call_id: &str) -> Result<SipCall> {
338 let path = format!("{PATH}/{}", escape(call_id));
339 self.client
340 .data(Method::GET, &path, CallOptions::new())
341 .await
342 }
343
344 pub async fn transfer(&self, call_id: &str, params: TransferSipCallParams) -> Result<SipCall> {
346 let body = TransferWire {
347 call_id,
348 transfer_to: params.to.as_deref(),
349 participant: params.participant.as_ref(),
350 play_dialtone: params.play_dialtone,
351 };
352 let path = format!("{PATH}/transfer");
353 self.client
354 .data(Method::POST, &path, CallOptions::json(&body)?)
355 .await
356 }
357
358 pub async fn end(&self, call_id: &str) -> Result<SipCall> {
360 let body = serde_json::json!({ "callId": call_id });
361 let path = format!("{PATH}/end");
362 self.client
363 .data(Method::POST, &path, CallOptions::json(&body)?)
364 .await
365 }
366
367 pub async fn switch_room(&self, call_id: &str, params: SwitchRoomParams) -> Result<SipCall> {
369 let body = SwitchRoomWire {
370 call_id,
371 room_id: ¶ms.room_id,
372 participant_id: params.participant_id.as_deref(),
373 token: params.token.as_deref(),
374 };
375 let path = format!("{PATH}/switch-room");
376 self.client
377 .data(Method::POST, &path, CallOptions::json(&body)?)
378 .await
379 }
380
381 fn fetcher(&self, params: &SipCallListParams) -> PageFetcher {
382 let client = self.client.clone();
383 let params = params.clone();
384 Arc::new(move |page, per_page| {
385 let client = client.clone();
386 let params = params.clone();
387 Box::pin(async move {
388 let query = QueryBuilder::new()
389 .opt("page", page)
390 .opt("perPage", per_page)
391 .opt_str("roomId", params.room_id.as_deref())
392 .opt_str("sessionId", params.session_id.as_deref())
393 .opt_str("id", params.id.as_deref())
394 .opt_str("gatewayId", params.gateway_id.as_deref())
395 .opt_str("ruleId", params.rule_id.as_deref())
396 .opt_str("type", params.direction.as_ref().map(SipDirection::as_str))
397 .opt_str("search", params.search.as_deref())
398 .opt("startDate", params.start_date)
399 .opt("endDate", params.end_date)
400 .into_pairs();
401 client
402 .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
403 .await
404 })
405 })
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use serde_json::json;
413
414 fn wire(params: CreateSipCallParams) -> Value {
415 serde_json::to_value(CreateSipCallWire::from(params)).unwrap()
416 }
417
418 #[test]
419 fn create_renames_call_to_and_call_from() {
420 let body = wire(CreateSipCallParams {
421 call_to: "+15550001".into(),
422 call_from: Some("+15550002".into()),
423 room_id: Some("r-1".into()),
424 ..Default::default()
425 });
426 assert_eq!(
427 body,
428 json!({
429 "sipCallTo": "+15550001",
430 "sipCallFrom": "+15550002",
431 "destinationRoomId": "r-1",
432 })
433 );
434 }
435
436 #[test]
437 fn participant_id_is_sugar_for_a_participant_object() {
438 let body = wire(CreateSipCallParams {
439 call_to: "+1".into(),
440 participant_id: Some("p-1".into()),
441 ..Default::default()
442 });
443 assert_eq!(body["participant"], json!({"id": "p-1"}));
444 }
445
446 #[test]
447 fn an_explicit_participant_wins_over_participant_id() {
448 let mut participant = Map::new();
449 participant.insert("id".into(), json!("explicit"));
450 participant.insert("name".into(), json!("Ada"));
451
452 let body = wire(CreateSipCallParams {
453 call_to: "+1".into(),
454 participant_id: Some("sugar".into()),
455 participant: Some(participant),
456 ..Default::default()
457 });
458 assert_eq!(body["participant"]["id"], json!("explicit"));
459 assert_eq!(body["participant"]["name"], json!("Ada"));
460 }
461
462 #[test]
463 fn absent_options_are_omitted() {
464 let body = wire(CreateSipCallParams {
465 call_to: "+1".into(),
466 ..Default::default()
467 });
468 assert_eq!(body, json!({"sipCallTo": "+1"}));
469 }
470
471 #[test]
472 fn the_list_mapper_surfaces_call_id_as_id() {
473 let mapper = call_item_mapper();
474 let call = mapper(json!({"callId": "c-1", "to": "+1"})).unwrap();
475 assert_eq!(call.id, "c-1");
476
477 let call = mapper(json!({"id": "real", "callId": "c-1"})).unwrap();
479 assert_eq!(call.id, "real");
480 }
481
482 #[test]
483 fn transfer_renames_to_as_transfer_to() {
484 let body = serde_json::to_value(TransferWire {
485 call_id: "c-1",
486 transfer_to: Some("+15550003"),
487 participant: None,
488 play_dialtone: Some(true),
489 })
490 .unwrap();
491 assert_eq!(
492 body,
493 json!({"callId": "c-1", "transferTo": "+15550003", "playDialtone": true})
494 );
495 }
496}