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