Skip to main content

videosdk/resources/sip/
webhooks.rs

1//! Webhooks delivering SIP call-event notifications.
2//!
3//! This manages webhook *configuration*. It is unrelated to verifying the
4//! signature of an inbound webhook.
5
6use std::sync::Arc;
7
8use futures_util::Stream;
9use reqwest::Method;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12
13use crate::client::{CallOptions, Client};
14use crate::common::MessageResponse;
15use crate::error::{Error, Result};
16use crate::pagination::{auto_page, paginate, ItemMapper, ListParams, Page, PageFetcher};
17use crate::query::QueryBuilder;
18use crate::resources::escape;
19use crate::resources::sip::SipWebhookEvent;
20
21const PATH: &str = "/v2/sip/webhooks";
22
23/// A webhook delivering SIP call-event notifications.
24#[derive(Debug, Clone, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct SipWebhook {
27    /// The webhook id.
28    ///
29    /// The list endpoint keys this `webhookId`; the SDK surfaces it here either way.
30    #[serde(default, deserialize_with = "crate::common::null_to_default")]
31    pub id: String,
32    /// Where events are delivered.
33    pub url: Option<String>,
34    /// The subscribed events.
35    #[serde(default, deserialize_with = "crate::common::null_to_default")]
36    pub events: Vec<SipWebhookEvent>,
37    /// When the webhook was created.
38    pub created_at: Option<String>,
39    /// When the webhook was last updated.
40    pub updated_at: Option<String>,
41    /// Any fields the server returned that this SDK does not model yet.
42    #[serde(flatten)]
43    pub extra: Map<String, Value>,
44}
45
46/// The parameters for creating or updating a SIP webhook.
47#[derive(Debug, Clone, Default, Serialize)]
48pub struct CreateSipWebhookParams {
49    /// Where to deliver events.
50    pub url: String,
51    /// The events to subscribe to.
52    pub events: Vec<SipWebhookEvent>,
53}
54
55/// The query parameters for [`SipWebhooksResource::list`].
56#[derive(Debug, Clone, Default)]
57pub struct SipWebhookListParams {
58    /// The 1-based page number.
59    pub page: Option<u32>,
60    /// Items per page.
61    pub per_page: Option<u32>,
62    /// An opaque cursor from a previous page.
63    pub cursor: Option<String>,
64    /// Matches on the webhook's URL.
65    pub search: Option<String>,
66}
67
68impl SipWebhookListParams {
69    fn pagination(&self) -> ListParams {
70        ListParams {
71            page: self.page,
72            per_page: self.per_page,
73            cursor: self.cursor.clone(),
74        }
75    }
76}
77
78/// The list returns raw documents, which key the id as `webhookId`. Surface it
79/// as `id`, so a listed webhook has the same shape as a fetched one.
80fn webhook_item_mapper() -> ItemMapper<SipWebhook> {
81    Arc::new(|value: Value| {
82        let mut webhook: SipWebhook =
83            serde_json::from_value(value).map_err(|e| Error::decode("SIP webhook list item", e))?;
84        if webhook.id.is_empty() {
85            if let Some(webhook_id) = webhook.extra.get("webhookId").and_then(Value::as_str) {
86                webhook.id = webhook_id.to_string();
87            }
88        }
89        Ok(webhook)
90    })
91}
92
93/// SIP webhooks. Reached via [`SipResource::webhooks`](crate::SipResource::webhooks).
94#[derive(Debug, Clone, Copy)]
95pub struct SipWebhooksResource<'a> {
96    client: &'a Client,
97}
98
99impl<'a> SipWebhooksResource<'a> {
100    pub(crate) fn new(client: &'a Client) -> Self {
101        Self { client }
102    }
103
104    /// Creates a webhook.
105    pub async fn create(&self, params: CreateSipWebhookParams) -> Result<SipWebhook> {
106        self.client
107            .json(Method::POST, PATH, CallOptions::json(&params)?)
108            .await
109    }
110
111    /// Lists webhooks, one page at a time.
112    pub async fn list(&self, params: SipWebhookListParams) -> Result<Page<SipWebhook>> {
113        paginate(
114            self.fetcher(&params),
115            &params.pagination(),
116            "data",
117            Some(webhook_item_mapper()),
118        )
119        .await
120    }
121
122    /// Lists webhooks, transparently fetching every page.
123    pub fn list_stream(
124        &self,
125        params: SipWebhookListParams,
126    ) -> impl Stream<Item = Result<SipWebhook>> + Send {
127        auto_page(
128            self.fetcher(&params),
129            params.pagination(),
130            "data",
131            Some(webhook_item_mapper()),
132        )
133    }
134
135    /// Fetches a webhook by id.
136    pub async fn get(&self, webhook_id: &str) -> Result<SipWebhook> {
137        let path = format!("{PATH}/{}", escape(webhook_id));
138        self.client
139            .json(Method::GET, &path, CallOptions::new())
140            .await
141    }
142
143    /// Replaces a webhook's URL and events.
144    pub async fn update(
145        &self,
146        webhook_id: &str,
147        params: CreateSipWebhookParams,
148    ) -> Result<SipWebhook> {
149        let path = format!("{PATH}/{}", escape(webhook_id));
150        self.client
151            .json(Method::PATCH, &path, CallOptions::json(&params)?)
152            .await
153    }
154
155    /// Deletes a webhook.
156    pub async fn delete(&self, webhook_id: &str) -> Result<MessageResponse> {
157        let path = format!("{PATH}/{}", escape(webhook_id));
158        self.client
159            .json(Method::DELETE, &path, CallOptions::new())
160            .await
161    }
162
163    fn fetcher(&self, params: &SipWebhookListParams) -> PageFetcher {
164        let client = self.client.clone();
165        let search = params.search.clone();
166        Arc::new(move |page, per_page| {
167            let client = client.clone();
168            let search = search.clone();
169            Box::pin(async move {
170                let query = QueryBuilder::new()
171                    .opt("page", page)
172                    .opt("perPage", per_page)
173                    .opt_str("search", search.as_deref())
174                    .into_pairs();
175                client
176                    .json::<Value>(Method::GET, PATH, CallOptions::new().query(query))
177                    .await
178            })
179        })
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use serde_json::json;
187
188    #[test]
189    fn the_list_mapper_surfaces_webhook_id_as_id() {
190        let mapper = webhook_item_mapper();
191        let webhook = mapper(json!({"webhookId": "w-1", "url": "https://x"})).unwrap();
192        assert_eq!(webhook.id, "w-1");
193
194        let webhook = mapper(json!({"id": "real", "webhookId": "w-1"})).unwrap();
195        assert_eq!(webhook.id, "real");
196    }
197
198    #[test]
199    fn create_params_serialize_url_and_events() {
200        let body = serde_json::to_value(CreateSipWebhookParams {
201            url: "https://example.com/sip".into(),
202            events: vec![SipWebhookEvent::CALL_STARTED, SipWebhookEvent::CALL_HANGUP],
203        })
204        .unwrap();
205        assert_eq!(
206            body,
207            json!({"url": "https://example.com/sip", "events": ["call-started", "call-hangup"]})
208        );
209    }
210}