Skip to main content

livekit_api/services/
connector.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use livekit_protocol as proto;
16use std::collections::HashMap;
17use std::time::Duration;
18
19use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
20use crate::services::dial_timeout::DEFAULT_RINGING_TIMEOUT;
21use crate::{access_token::VideoGrants, get_env_keys, services::twirp_client::TwirpClient};
22
23const SVC: &str = "Connector";
24
25/// Options for dialing a WhatsApp call
26#[derive(Default, Clone, Debug)]
27pub struct DialWhatsAppCallOptions {
28    /// Optional - An arbitrary string useful for tracking and logging purposes
29    pub biz_opaque_callback_data: Option<String>,
30    /// Optional - What LiveKit room should this participant be connected to
31    pub room_name: Option<String>,
32    /// Optional - Agents to dispatch the call to
33    pub agents: Option<Vec<proto::RoomAgentDispatch>>,
34    /// Optional - Identity of the participant in LiveKit room
35    pub participant_identity: Option<String>,
36    /// Optional - Name of the participant in LiveKit room
37    pub participant_name: Option<String>,
38    /// Optional - User-defined metadata attached to the participant in the room
39    pub participant_metadata: Option<String>,
40    /// Optional - User-defined attributes attached to the participant in the room
41    pub participant_attributes: Option<HashMap<String, String>>,
42    /// Optional - Country where the call terminates as ISO 3166-1 alpha-2
43    pub destination_country: Option<String>,
44}
45
46/// Options for accepting a WhatsApp call
47#[derive(Default, Clone, Debug)]
48pub struct AcceptWhatsAppCallOptions {
49    /// Optional - An arbitrary string useful for tracking and logging purposes
50    pub biz_opaque_callback_data: Option<String>,
51    /// Optional - What LiveKit room should this participant be connected to
52    pub room_name: Option<String>,
53    /// Optional - Agents to dispatch the call to
54    pub agents: Option<Vec<proto::RoomAgentDispatch>>,
55    /// Optional - Identity of the participant in LiveKit room
56    pub participant_identity: Option<String>,
57    /// Optional - Name of the participant in LiveKit room
58    pub participant_name: Option<String>,
59    /// Optional - User-defined metadata attached to the participant in the room
60    pub participant_metadata: Option<String>,
61    /// Optional - User-defined attributes attached to the participant in the room
62    pub participant_attributes: Option<HashMap<String, String>>,
63    /// Optional - Country where the call terminates as ISO 3166-1 alpha-2
64    pub destination_country: Option<String>,
65    /// Optional - Wait until the inbound party joins before returning.
66    pub wait_until_answered: Option<bool>,
67    /// Optional - Per-request timeout override. When `wait_until_answered` is set
68    /// it defaults to the standard ring window; otherwise the client default applies.
69    pub timeout: Option<Duration>,
70}
71
72/// Options for connecting a Twilio call
73#[derive(Default, Clone, Debug)]
74pub struct ConnectTwilioCallOptions {
75    /// Optional - Agents to dispatch the call to
76    pub agents: Option<Vec<proto::RoomAgentDispatch>>,
77    /// Optional - Identity of the participant in LiveKit room
78    pub participant_identity: Option<String>,
79    /// Optional - Name of the participant in LiveKit room
80    pub participant_name: Option<String>,
81    /// Optional - User-defined metadata attached to the participant in the room
82    pub participant_metadata: Option<String>,
83    /// Optional - User-defined attributes attached to the participant in the room
84    pub participant_attributes: Option<HashMap<String, String>>,
85    /// Optional - Country where the call terminates as ISO 3166-1 alpha-2
86    pub destination_country: Option<String>,
87}
88
89#[derive(Debug)]
90pub struct ConnectorClient {
91    base: ServiceBase,
92    client: TwirpClient,
93}
94
95impl ConnectorClient {
96    /// Authenticates with an API key and secret, signing a short-lived token per request.
97    pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
98        Self::build(
99            host,
100            ServiceBase::with_api_key(api_key, api_secret),
101            crate::http_client::Client::new(),
102        )
103    }
104
105    /// Authenticates with a pre-signed token, sent verbatim on every request.
106    pub fn with_token(host: &str, token: &str) -> Self {
107        Self::build(host, ServiceBase::with_token(token), crate::http_client::Client::new())
108    }
109
110    /// Builds the client from an already-constructed HTTP client so the unified
111    /// [`LiveKitApi`](super::LiveKitApi) can share one connection pool across services.
112    pub(crate) fn build(host: &str, base: ServiceBase, client: crate::http_client::Client) -> Self {
113        Self { base, client: TwirpClient::with_client(host, LIVEKIT_PACKAGE, None, client) }
114    }
115
116    #[cfg(test)]
117    pub(crate) fn with_default_headers(mut self, headers: http::HeaderMap) -> Self {
118        self.client = self.client.with_default_headers(headers);
119        self
120    }
121
122    /// Reads the API key and secret from the `LIVEKIT_API_KEY` and
123    /// `LIVEKIT_API_SECRET` environment variables.
124    pub fn new(host: &str) -> ServiceResult<Self> {
125        let (api_key, api_secret) = get_env_keys()?;
126        Ok(Self::with_api_key(host, &api_key, &api_secret))
127    }
128
129    /// Enables or disables region failover (enabled by default). Failover only
130    /// engages for LiveKit Cloud hosts.
131    pub fn with_failover(mut self, enabled: bool) -> Self {
132        self.client = self.client.with_failover(enabled);
133        self
134    }
135
136    /// Overrides the default per-request timeout (10s) for calls on this client.
137    pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
138        self.client = self.client.with_request_timeout(timeout);
139        self
140    }
141
142    /// Dials a WhatsApp call
143    ///
144    /// # Arguments
145    /// * `phone_number_id` - The identifier of the number for business initiating the call
146    /// * `to_phone_number` - The number of the user that should receive the call
147    /// * `api_key` - The API key of the business initiating the call
148    /// * `cloud_api_version` - WhatsApp Cloud API version (e.g., "23.0", "24.0")
149    /// * `options` - Additional options for the call
150    ///
151    /// # Returns
152    /// Information about the dialed call including the WhatsApp call ID and room name
153    pub async fn dial_whatsapp_call(
154        &self,
155        phone_number_id: impl Into<String>,
156        to_phone_number: impl Into<String>,
157        api_key: impl Into<String>,
158        cloud_api_version: impl Into<String>,
159        options: DialWhatsAppCallOptions,
160    ) -> ServiceResult<proto::DialWhatsAppCallResponse> {
161        self.client
162            .request(
163                SVC,
164                "DialWhatsAppCall",
165                proto::DialWhatsAppCallRequest {
166                    whatsapp_phone_number_id: phone_number_id.into(),
167                    whatsapp_to_phone_number: to_phone_number.into(),
168                    whatsapp_api_key: api_key.into(),
169                    whatsapp_cloud_api_version: cloud_api_version.into(),
170                    whatsapp_biz_opaque_callback_data: options
171                        .biz_opaque_callback_data
172                        .unwrap_or_default(),
173                    room_name: options.room_name.unwrap_or_default(),
174                    agents: options.agents.unwrap_or_default(),
175                    participant_identity: options.participant_identity.unwrap_or_default(),
176                    participant_name: options.participant_name.unwrap_or_default(),
177                    participant_metadata: options.participant_metadata.unwrap_or_default(),
178                    participant_attributes: options.participant_attributes.unwrap_or_default(),
179                    destination_country: options.destination_country.unwrap_or_default(),
180                    ringing_timeout: Default::default(),
181                },
182                self.base
183                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
184            )
185            .await
186            .map_err(Into::into)
187    }
188
189    /// Disconnects a WhatsApp call initiated by the business.
190    ///
191    /// This is the `BusinessInitiated` case; use [`disconnect_whatsapp_call_with_reason`]
192    /// to disconnect with a different [`DisconnectReason`].
193    ///
194    /// [`disconnect_whatsapp_call_with_reason`]: Self::disconnect_whatsapp_call_with_reason
195    /// [`DisconnectReason`]: proto::disconnect_whats_app_call_request::DisconnectReason
196    ///
197    /// # Arguments
198    /// * `call_id` - Call ID sent by Meta
199    /// * `api_key` - The API key of the business disconnecting the call
200    ///
201    /// # Returns
202    /// Empty response on success
203    pub async fn disconnect_whatsapp_call(
204        &self,
205        call_id: impl Into<String>,
206        api_key: impl Into<String>,
207    ) -> ServiceResult<proto::DisconnectWhatsAppCallResponse> {
208        self.disconnect_whatsapp_call_with_reason(
209            call_id,
210            api_key,
211            proto::disconnect_whats_app_call_request::DisconnectReason::BusinessInitiated,
212        )
213        .await
214    }
215
216    /// Disconnects a WhatsApp call, specifying why it is being disconnected.
217    ///
218    /// # Arguments
219    /// * `call_id` - Call ID sent by Meta
220    /// * `api_key` - The API key of the business disconnecting the call. Required
221    ///   when `reason` is `BusinessInitiated`; optional for `UserInitiated`.
222    /// * `reason` - Why the call is being disconnected
223    ///
224    /// # Returns
225    /// Empty response on success
226    pub async fn disconnect_whatsapp_call_with_reason(
227        &self,
228        call_id: impl Into<String>,
229        api_key: impl Into<String>,
230        reason: proto::disconnect_whats_app_call_request::DisconnectReason,
231    ) -> ServiceResult<proto::DisconnectWhatsAppCallResponse> {
232        self.client
233            .request(
234                SVC,
235                "DisconnectWhatsAppCall",
236                proto::DisconnectWhatsAppCallRequest {
237                    whatsapp_call_id: call_id.into(),
238                    whatsapp_api_key: api_key.into(),
239                    disconnect_reason: reason as i32,
240                },
241                self.base
242                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
243            )
244            .await
245            .map_err(Into::into)
246    }
247
248    /// Connects a WhatsApp call (handles the SDP exchange)
249    ///
250    /// # Arguments
251    /// * `call_id` - Call ID sent by Meta
252    /// * `sdp` - The SDP from Meta (answer SDP for business-initiated call)
253    ///
254    /// # Returns
255    /// Empty response on success
256    pub async fn connect_whatsapp_call(
257        &self,
258        call_id: impl Into<String>,
259        sdp: proto::SessionDescription,
260    ) -> ServiceResult<proto::ConnectWhatsAppCallResponse> {
261        self.client
262            .request(
263                SVC,
264                "ConnectWhatsAppCall",
265                proto::ConnectWhatsAppCallRequest {
266                    whatsapp_call_id: call_id.into(),
267                    sdp: Some(sdp),
268                },
269                self.base
270                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
271            )
272            .await
273            .map_err(Into::into)
274    }
275
276    /// Accepts an incoming WhatsApp call
277    ///
278    /// # Arguments
279    /// * `phone_number_id` - The identifier of the number for business initiating the call
280    /// * `api_key` - The API key of the business connecting the call
281    /// * `cloud_api_version` - WhatsApp Cloud API version (e.g., "23.0", "24.0")
282    /// * `call_id` - Call ID sent by Meta
283    /// * `sdp` - The SDP from Meta (for user-initiated call)
284    /// * `options` - Additional options for the call
285    ///
286    /// # Returns
287    /// Information about the accepted call including the room name
288    pub async fn accept_whatsapp_call(
289        &self,
290        phone_number_id: impl Into<String>,
291        api_key: impl Into<String>,
292        cloud_api_version: impl Into<String>,
293        call_id: impl Into<String>,
294        sdp: proto::SessionDescription,
295        options: AcceptWhatsAppCallOptions,
296    ) -> ServiceResult<proto::AcceptWhatsAppCallResponse> {
297        let wait_until_answered = options.wait_until_answered.unwrap_or(false);
298        let request = proto::AcceptWhatsAppCallRequest {
299            whatsapp_phone_number_id: phone_number_id.into(),
300            whatsapp_api_key: api_key.into(),
301            whatsapp_cloud_api_version: cloud_api_version.into(),
302            whatsapp_call_id: call_id.into(),
303            whatsapp_biz_opaque_callback_data: options.biz_opaque_callback_data.unwrap_or_default(),
304            sdp: Some(sdp),
305            room_name: options.room_name.unwrap_or_default(),
306            agents: options.agents.unwrap_or_default(),
307            participant_identity: options.participant_identity.unwrap_or_default(),
308            participant_name: options.participant_name.unwrap_or_default(),
309            participant_metadata: options.participant_metadata.unwrap_or_default(),
310            participant_attributes: options.participant_attributes.unwrap_or_default(),
311            destination_country: options.destination_country.unwrap_or_default(),
312            ringing_timeout: None,
313            wait_until_answered,
314        };
315        let headers =
316            self.base.auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?;
317
318        // When waiting for the inbound party to join, the request can block, so
319        // default its timeout to the standard ring window; otherwise the client
320        // default applies.
321        let timeout = if wait_until_answered {
322            Some(options.timeout.unwrap_or(DEFAULT_RINGING_TIMEOUT))
323        } else {
324            options.timeout
325        };
326        match timeout {
327            Some(timeout) => self
328                .client
329                .request_with_timeout(SVC, "AcceptWhatsAppCall", request, headers, timeout)
330                .await
331                .map_err(Into::into),
332            None => self
333                .client
334                .request(SVC, "AcceptWhatsAppCall", request, headers)
335                .await
336                .map_err(Into::into),
337        }
338    }
339
340    /// Connects a Twilio call
341    ///
342    /// # Arguments
343    /// * `direction` - The direction of the call (inbound or outbound)
344    /// * `room_name` - What LiveKit room should this call be connected to
345    /// * `options` - Additional options for the call
346    ///
347    /// # Returns
348    /// The WebSocket URL which Twilio media stream should connect to
349    pub async fn connect_twilio_call(
350        &self,
351        direction: proto::connect_twilio_call_request::TwilioCallDirection,
352        room_name: impl Into<String>,
353        options: ConnectTwilioCallOptions,
354    ) -> ServiceResult<proto::ConnectTwilioCallResponse> {
355        self.client
356            .request(
357                SVC,
358                "ConnectTwilioCall",
359                proto::ConnectTwilioCallRequest {
360                    twilio_call_direction: direction as i32,
361                    room_name: room_name.into(),
362                    agents: options.agents.unwrap_or_default(),
363                    participant_identity: options.participant_identity.unwrap_or_default(),
364                    participant_name: options.participant_name.unwrap_or_default(),
365                    participant_metadata: options.participant_metadata.unwrap_or_default(),
366                    participant_attributes: options.participant_attributes.unwrap_or_default(),
367                    destination_country: options.destination_country.unwrap_or_default(),
368                },
369                self.base
370                    .auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
371            )
372            .await
373            .map_err(Into::into)
374    }
375}