Skip to main content

videosdk/resources/sip/
trunks.rs

1//! Inbound and outbound SIP trunks.
2
3use 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::common::MessageResponse;
13use crate::error::Result;
14use crate::pagination::{auto_page, paginate, ListParams, Page, PageFetcher};
15use crate::query::QueryBuilder;
16use crate::resources::escape;
17use crate::resources::sip::{SipAuth, SipMediaEncryption, SipRegion, SipTransport};
18
19const INBOUND_PATH: &str = "/v2/sip/inbound-gateways";
20const OUTBOUND_PATH: &str = "/v2/sip/outbound-gateways";
21
22/// An inbound or outbound SIP trunk.
23#[derive(Debug, Clone, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SipTrunk {
26    /// The trunk id.
27    pub id: String,
28    /// The trunk's display name.
29    pub name: Option<String>,
30    /// The SIP host, for outbound trunks.
31    pub address: Option<String>,
32    /// The E.164 numbers this trunk handles.
33    #[serde(default, deserialize_with = "crate::common::null_to_default")]
34    pub numbers: Vec<String>,
35    /// The allowed source IPs and CIDRs.
36    #[serde(default, deserialize_with = "crate::common::null_to_default")]
37    pub allowed_addresses: Vec<String>,
38    /// The allowed caller-number patterns.
39    #[serde(default, deserialize_with = "crate::common::null_to_default")]
40    pub allowed_numbers: Vec<String>,
41    /// Whether media is encrypted.
42    pub media_encryption: Option<SipMediaEncryption>,
43    /// The SIP transport.
44    pub transport: Option<SipTransport>,
45    /// Whether calls on this trunk are recorded.
46    pub record: Option<bool>,
47    /// Free-form tags.
48    #[serde(default, deserialize_with = "crate::common::null_to_default")]
49    pub tags: Vec<String>,
50    /// The SIP credentials.
51    pub auth: Option<SipAuth>,
52    /// The SIP region.
53    pub geo_region: Option<SipRegion>,
54    /// Free-form metadata.
55    #[serde(default, deserialize_with = "crate::common::null_to_default")]
56    pub metadata: HashMap<String, String>,
57    /// Whether noise cancellation is applied.
58    pub noise_cancellation: Option<bool>,
59    /// Whether DTMF is enabled.
60    pub enable_dtmf: Option<bool>,
61    /// Any fields the server returned that this SDK does not model yet.
62    #[serde(flatten)]
63    pub extra: Map<String, Value>,
64}
65
66/// The parameters for [`InboundTrunkResource::create`].
67#[derive(Debug, Clone, Default, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct CreateInboundTrunkParams {
70    /// The display name. Required.
71    pub name: String,
72    /// The E.164 numbers this trunk handles. Required, non-empty.
73    pub numbers: Vec<String>,
74    /// The SIP credentials.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub auth: Option<SipAuth>,
77    /// The allowed source IPs and CIDRs.
78    #[serde(skip_serializing_if = "Vec::is_empty")]
79    pub allowed_addresses: Vec<String>,
80    /// The allowed caller-number patterns, e.g. `+91*`.
81    #[serde(skip_serializing_if = "Vec::is_empty")]
82    pub allowed_numbers: Vec<String>,
83    /// Whether to encrypt media.
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub media_encryption: Option<SipMediaEncryption>,
86    /// Whether to record calls on this trunk.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub record: Option<bool>,
89    /// Whether to apply noise cancellation.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub noise_cancellation: Option<bool>,
92    /// Whether to enable DTMF.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub enable_dtmf: Option<bool>,
95    /// Free-form metadata.
96    #[serde(skip_serializing_if = "HashMap::is_empty")]
97    pub metadata: HashMap<String, String>,
98    /// Free-form tags.
99    #[serde(skip_serializing_if = "Vec::is_empty")]
100    pub tags: Vec<String>,
101    /// The SIP region.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub geo_region: Option<SipRegion>,
104}
105
106/// The parameters for [`InboundTrunkResource::update`].
107///
108/// Omitted fields keep their current value, **except** `record`,
109/// `noise_cancellation` and `enable_dtmf`, which reset to `false` when omitted,
110/// and `geo_region`, which is cleared. Send the complete desired state.
111#[derive(Debug, Clone, Default, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct UpdateInboundTrunkParams {
114    /// The display name.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub name: Option<String>,
117    /// The E.164 numbers this trunk handles.
118    #[serde(skip_serializing_if = "Vec::is_empty")]
119    pub numbers: Vec<String>,
120    /// The SIP credentials.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub auth: Option<SipAuth>,
123    /// The allowed source IPs and CIDRs.
124    #[serde(skip_serializing_if = "Vec::is_empty")]
125    pub allowed_addresses: Vec<String>,
126    /// The allowed caller-number patterns.
127    #[serde(skip_serializing_if = "Vec::is_empty")]
128    pub allowed_numbers: Vec<String>,
129    /// Whether to encrypt media.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub media_encryption: Option<SipMediaEncryption>,
132    /// Whether to record calls. Resets to `false` when omitted.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub record: Option<bool>,
135    /// Whether to apply noise cancellation. Resets to `false` when omitted.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub noise_cancellation: Option<bool>,
138    /// Whether to enable DTMF. Resets to `false` when omitted.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub enable_dtmf: Option<bool>,
141    /// Free-form metadata.
142    #[serde(skip_serializing_if = "HashMap::is_empty")]
143    pub metadata: HashMap<String, String>,
144    /// Free-form tags.
145    #[serde(skip_serializing_if = "Vec::is_empty")]
146    pub tags: Vec<String>,
147    /// The SIP region. Cleared when omitted.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub geo_region: Option<SipRegion>,
150}
151
152/// The parameters for [`OutboundTrunkResource::create`].
153#[derive(Debug, Clone, Default, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct CreateOutboundTrunkParams {
156    /// The display name. Required.
157    pub name: String,
158    /// The SIP host, e.g. `sip.telnyx.com:5061`. Required.
159    pub address: String,
160    /// The E.164 numbers for this trunk. The server rejects an outbound trunk
161    /// without numbers.
162    #[serde(skip_serializing_if = "Vec::is_empty")]
163    pub numbers: Vec<String>,
164    /// The SIP transport. Defaults to TLS.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub transport: Option<SipTransport>,
167    /// The SIP credentials.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub auth: Option<SipAuth>,
170    /// Whether to encrypt media.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub media_encryption: Option<SipMediaEncryption>,
173    /// Whether to record calls on this trunk.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub record: Option<bool>,
176    /// Whether to apply noise cancellation.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub noise_cancellation: Option<bool>,
179    /// Whether to enable DTMF.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub enable_dtmf: Option<bool>,
182    /// Free-form metadata.
183    #[serde(skip_serializing_if = "HashMap::is_empty")]
184    pub metadata: HashMap<String, String>,
185    /// Free-form tags.
186    #[serde(skip_serializing_if = "Vec::is_empty")]
187    pub tags: Vec<String>,
188    /// The SIP region.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub geo_region: Option<SipRegion>,
191}
192
193/// The parameters for [`OutboundTrunkResource::update`]. The same reset
194/// semantics as [`UpdateInboundTrunkParams`] apply.
195#[derive(Debug, Clone, Default, Serialize)]
196#[serde(rename_all = "camelCase")]
197pub struct UpdateOutboundTrunkParams {
198    /// The display name.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub name: Option<String>,
201    /// The E.164 numbers for this trunk.
202    #[serde(skip_serializing_if = "Vec::is_empty")]
203    pub numbers: Vec<String>,
204    /// The SIP host.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub address: Option<String>,
207    /// The SIP transport.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub transport: Option<SipTransport>,
210    /// The SIP credentials.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub auth: Option<SipAuth>,
213    /// Whether to encrypt media.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub media_encryption: Option<SipMediaEncryption>,
216    /// Whether to record calls. Resets to `false` when omitted.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub record: Option<bool>,
219    /// Whether to apply noise cancellation. Resets to `false` when omitted.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub noise_cancellation: Option<bool>,
222    /// Whether to enable DTMF. Resets to `false` when omitted.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub enable_dtmf: Option<bool>,
225    /// Free-form metadata.
226    #[serde(skip_serializing_if = "HashMap::is_empty")]
227    pub metadata: HashMap<String, String>,
228    /// Free-form tags.
229    #[serde(skip_serializing_if = "Vec::is_empty")]
230    pub tags: Vec<String>,
231    /// The SIP region. Cleared when omitted.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub geo_region: Option<SipRegion>,
234    /// The allowed caller-number patterns. Outbound trunks accept this on update.
235    #[serde(skip_serializing_if = "Vec::is_empty")]
236    pub allowed_numbers: Vec<String>,
237}
238
239/// The query parameters for listing trunks.
240#[derive(Debug, Clone, Default)]
241pub struct SipTrunkListParams {
242    /// The 1-based page number.
243    pub page: Option<u32>,
244    /// Items per page.
245    pub per_page: Option<u32>,
246    /// An opaque cursor from a previous page.
247    pub cursor: Option<String>,
248    /// Filters by trunk id.
249    pub id: Option<String>,
250    /// A case-insensitive search on the trunk's name.
251    pub search: Option<String>,
252}
253
254impl SipTrunkListParams {
255    fn pagination(&self) -> ListParams {
256        ListParams {
257            page: self.page,
258            per_page: self.per_page,
259            cursor: self.cursor.clone(),
260        }
261    }
262}
263
264fn trunk_fetcher(client: &Client, path: &'static str, params: &SipTrunkListParams) -> PageFetcher {
265    let client = client.clone();
266    let params = params.clone();
267    Arc::new(move |page, per_page| {
268        let client = client.clone();
269        let params = params.clone();
270        Box::pin(async move {
271            let query = QueryBuilder::new()
272                .opt("page", page)
273                .opt("perPage", per_page)
274                .opt_str("id", params.id.as_deref())
275                .opt_str("search", params.search.as_deref())
276                .into_pairs();
277            client
278                .json::<Value>(Method::GET, path, CallOptions::new().query(query))
279                .await
280        })
281    })
282}
283
284/// Inbound SIP trunks: connections that receive calls for a set of numbers.
285/// Reached via [`SipTrunksResource::inbound`].
286#[derive(Debug, Clone, Copy)]
287pub struct InboundTrunkResource<'a> {
288    client: &'a Client,
289}
290
291impl<'a> InboundTrunkResource<'a> {
292    /// Creates an inbound trunk.
293    pub async fn create(&self, params: CreateInboundTrunkParams) -> Result<SipTrunk> {
294        self.client
295            .json(Method::POST, INBOUND_PATH, CallOptions::json(&params)?)
296            .await
297    }
298
299    /// Lists inbound trunks, one page at a time.
300    pub async fn list(&self, params: SipTrunkListParams) -> Result<Page<SipTrunk>> {
301        let fetcher = trunk_fetcher(self.client, INBOUND_PATH, &params);
302        paginate(fetcher, &params.pagination(), "data", None).await
303    }
304
305    /// Lists inbound trunks, transparently fetching every page.
306    pub fn list_stream(
307        &self,
308        params: SipTrunkListParams,
309    ) -> impl Stream<Item = Result<SipTrunk>> + Send {
310        let fetcher = trunk_fetcher(self.client, INBOUND_PATH, &params);
311        auto_page(fetcher, params.pagination(), "data", None)
312    }
313
314    /// Fetches an inbound trunk by id.
315    pub async fn get(&self, trunk_id: &str) -> Result<SipTrunk> {
316        let path = format!("{INBOUND_PATH}/{}", escape(trunk_id));
317        self.client
318            .json(Method::GET, &path, CallOptions::new())
319            .await
320    }
321
322    /// Partially updates an inbound trunk. See [`UpdateInboundTrunkParams`] for
323    /// the fields that reset when omitted.
324    pub async fn update(
325        &self,
326        trunk_id: &str,
327        params: UpdateInboundTrunkParams,
328    ) -> Result<SipTrunk> {
329        let path = format!("{INBOUND_PATH}/{}", escape(trunk_id));
330        self.client
331            .json(Method::PATCH, &path, CallOptions::json(&params)?)
332            .await
333    }
334
335    /// Deletes an inbound trunk.
336    pub async fn delete(&self, trunk_id: &str) -> Result<MessageResponse> {
337        let path = format!("{INBOUND_PATH}/{}", escape(trunk_id));
338        self.client
339            .json(Method::DELETE, &path, CallOptions::new())
340            .await
341    }
342}
343
344/// Outbound SIP trunks: connections used to place calls to a SIP host. Reached
345/// via [`SipTrunksResource::outbound`].
346#[derive(Debug, Clone, Copy)]
347pub struct OutboundTrunkResource<'a> {
348    client: &'a Client,
349}
350
351impl<'a> OutboundTrunkResource<'a> {
352    /// Creates an outbound trunk.
353    pub async fn create(&self, params: CreateOutboundTrunkParams) -> Result<SipTrunk> {
354        self.client
355            .json(Method::POST, OUTBOUND_PATH, CallOptions::json(&params)?)
356            .await
357    }
358
359    /// Lists outbound trunks, one page at a time.
360    pub async fn list(&self, params: SipTrunkListParams) -> Result<Page<SipTrunk>> {
361        let fetcher = trunk_fetcher(self.client, OUTBOUND_PATH, &params);
362        paginate(fetcher, &params.pagination(), "data", None).await
363    }
364
365    /// Lists outbound trunks, transparently fetching every page.
366    pub fn list_stream(
367        &self,
368        params: SipTrunkListParams,
369    ) -> impl Stream<Item = Result<SipTrunk>> + Send {
370        let fetcher = trunk_fetcher(self.client, OUTBOUND_PATH, &params);
371        auto_page(fetcher, params.pagination(), "data", None)
372    }
373
374    /// Fetches an outbound trunk by id.
375    pub async fn get(&self, trunk_id: &str) -> Result<SipTrunk> {
376        let path = format!("{OUTBOUND_PATH}/{}", escape(trunk_id));
377        self.client
378            .json(Method::GET, &path, CallOptions::new())
379            .await
380    }
381
382    /// Partially updates an outbound trunk. The same reset semantics as the
383    /// inbound update apply.
384    pub async fn update(
385        &self,
386        trunk_id: &str,
387        params: UpdateOutboundTrunkParams,
388    ) -> Result<SipTrunk> {
389        let path = format!("{OUTBOUND_PATH}/{}", escape(trunk_id));
390        self.client
391            .json(Method::PATCH, &path, CallOptions::json(&params)?)
392            .await
393    }
394
395    /// Deletes an outbound trunk.
396    pub async fn delete(&self, trunk_id: &str) -> Result<MessageResponse> {
397        let path = format!("{OUTBOUND_PATH}/{}", escape(trunk_id));
398        self.client
399            .json(Method::DELETE, &path, CallOptions::new())
400            .await
401    }
402}
403
404/// Groups the inbound and outbound trunk sub-resources. Reached via
405/// [`SipResource::trunks`](crate::SipResource::trunks).
406#[derive(Debug, Clone, Copy)]
407pub struct SipTrunksResource<'a> {
408    client: &'a Client,
409}
410
411impl<'a> SipTrunksResource<'a> {
412    pub(crate) fn new(client: &'a Client) -> Self {
413        Self { client }
414    }
415
416    /// Inbound trunks: connections that receive calls for a set of numbers.
417    pub fn inbound(&self) -> InboundTrunkResource<'a> {
418        InboundTrunkResource {
419            client: self.client,
420        }
421    }
422
423    /// Outbound trunks: connections used to place calls to a SIP host.
424    pub fn outbound(&self) -> OutboundTrunkResource<'a> {
425        OutboundTrunkResource {
426            client: self.client,
427        }
428    }
429}