Skip to content

Commit faf71a2

Browse files
committed
feat(connector): initial service impl
1 parent d98893d commit faf71a2

File tree

6 files changed

+4365
-441
lines changed

6 files changed

+4365
-441
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
15+
use livekit_protocol as proto;
16+
use std::collections::HashMap;
17+
18+
use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
19+
use crate::{access_token::VideoGrants, get_env_keys, services::twirp_client::TwirpClient};
20+
21+
const SVC: &str = "Connector";
22+
23+
/// Options for dialing a WhatsApp call
24+
#[derive(Default, Clone, Debug)]
25+
pub struct DialWhatsAppCallOptions {
26+
/// Optional - An arbitrary string useful for tracking and logging purposes
27+
pub biz_opaque_callback_data: Option<String>,
28+
/// Optional - What LiveKit room should this participant be connected to
29+
pub room_name: Option<String>,
30+
/// Optional - Agents to dispatch the call to
31+
pub agents: Option<Vec<proto::RoomAgentDispatch>>,
32+
/// Optional - Identity of the participant in LiveKit room
33+
pub participant_identity: Option<String>,
34+
/// Optional - Name of the participant in LiveKit room
35+
pub participant_name: Option<String>,
36+
/// Optional - User-defined metadata attached to the participant in the room
37+
pub participant_metadata: Option<String>,
38+
/// Optional - User-defined attributes attached to the participant in the room
39+
pub participant_attributes: Option<HashMap<String, String>>,
40+
/// Optional - Country where the call terminates as ISO 3166-1 alpha-2
41+
pub destination_country: Option<String>,
42+
}
43+
44+
/// Options for accepting a WhatsApp call
45+
#[derive(Default, Clone, Debug)]
46+
pub struct AcceptWhatsAppCallOptions {
47+
/// Optional - An arbitrary string useful for tracking and logging purposes
48+
pub biz_opaque_callback_data: Option<String>,
49+
/// Optional - What LiveKit room should this participant be connected to
50+
pub room_name: Option<String>,
51+
/// Optional - Agents to dispatch the call to
52+
pub agents: Option<Vec<proto::RoomAgentDispatch>>,
53+
/// Optional - Identity of the participant in LiveKit room
54+
pub participant_identity: Option<String>,
55+
/// Optional - Name of the participant in LiveKit room
56+
pub participant_name: Option<String>,
57+
/// Optional - User-defined metadata attached to the participant in the room
58+
pub participant_metadata: Option<String>,
59+
/// Optional - User-defined attributes attached to the participant in the room
60+
pub participant_attributes: Option<HashMap<String, String>>,
61+
/// Optional - Country where the call terminates as ISO 3166-1 alpha-2
62+
pub destination_country: Option<String>,
63+
}
64+
65+
/// Options for connecting a Twilio call
66+
#[derive(Default, Clone, Debug)]
67+
pub struct ConnectTwilioCallOptions {
68+
/// Optional - Agents to dispatch the call to
69+
pub agents: Option<Vec<proto::RoomAgentDispatch>>,
70+
/// Optional - Identity of the participant in LiveKit room
71+
pub participant_identity: Option<String>,
72+
/// Optional - Name of the participant in LiveKit room
73+
pub participant_name: Option<String>,
74+
/// Optional - User-defined metadata attached to the participant in the room
75+
pub participant_metadata: Option<String>,
76+
/// Optional - User-defined attributes attached to the participant in the room
77+
pub participant_attributes: Option<HashMap<String, String>>,
78+
/// Optional - Country where the call terminates as ISO 3166-1 alpha-2
79+
pub destination_country: Option<String>,
80+
}
81+
82+
#[derive(Debug)]
83+
pub struct ConnectorClient {
84+
base: ServiceBase,
85+
client: TwirpClient,
86+
}
87+
88+
impl ConnectorClient {
89+
pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
90+
Self {
91+
base: ServiceBase::with_api_key(api_key, api_secret),
92+
client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
93+
}
94+
}
95+
96+
pub fn new(host: &str) -> ServiceResult<Self> {
97+
let (api_key, api_secret) = get_env_keys()?;
98+
Ok(Self::with_api_key(host, &api_key, &api_secret))
99+
}
100+
101+
/// Dials a WhatsApp call
102+
///
103+
/// # Arguments
104+
/// * `phone_number_id` - The number of the business initiating the call
105+
/// * `to_phone_number` - The number of the user that should receive the call
106+
/// * `api_key` - The API key of the business initiating the call
107+
/// * `cloud_api_version` - WhatsApp Cloud API version (e.g., "23.0", "24.0")
108+
/// * `options` - Additional options for the call
109+
///
110+
/// # Returns
111+
/// Information about the dialed call including the WhatsApp call ID and room name
112+
pub async fn dial_whatsapp_call(
113+
&self,
114+
phone_number_id: impl Into<String>,
115+
to_phone_number: impl Into<String>,
116+
api_key: impl Into<String>,
117+
cloud_api_version: impl Into<String>,
118+
options: DialWhatsAppCallOptions,
119+
) -> ServiceResult<proto::DialWhatsAppCallResponse> {
120+
self.client
121+
.request(
122+
SVC,
123+
"DialWhatsAppCall",
124+
proto::DialWhatsAppCallRequest {
125+
whatsapp_phone_number_id: phone_number_id.into(),
126+
whatsapp_to_phone_number: to_phone_number.into(),
127+
whatsapp_api_key: api_key.into(),
128+
whatsapp_cloud_api_version: cloud_api_version.into(),
129+
whatsapp_biz_opaque_callback_data: options.biz_opaque_callback_data.unwrap_or_default(),
130+
room_name: options.room_name.unwrap_or_default(),
131+
agents: options.agents.unwrap_or_default(),
132+
participant_identity: options.participant_identity.unwrap_or_default(),
133+
participant_name: options.participant_name.unwrap_or_default(),
134+
participant_metadata: options.participant_metadata.unwrap_or_default(),
135+
participant_attributes: options.participant_attributes.unwrap_or_default(),
136+
destination_country: options.destination_country.unwrap_or_default(),
137+
},
138+
self.base
139+
.auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
140+
)
141+
.await
142+
.map_err(Into::into)
143+
}
144+
145+
/// Disconnects a WhatsApp call
146+
///
147+
/// # Arguments
148+
/// * `call_id` - Call ID sent by Meta
149+
/// * `api_key` - The API key of the business disconnecting the call
150+
///
151+
/// # Returns
152+
/// Empty response on success
153+
pub async fn disconnect_whatsapp_call(
154+
&self,
155+
call_id: impl Into<String>,
156+
api_key: impl Into<String>,
157+
) -> ServiceResult<proto::DisconnectWhatsAppCallResponse> {
158+
self.client
159+
.request(
160+
SVC,
161+
"DisconnectWhatsAppCall",
162+
proto::DisconnectWhatsAppCallRequest {
163+
whatsapp_call_id: call_id.into(),
164+
whatsapp_api_key: api_key.into(),
165+
},
166+
self.base
167+
.auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
168+
)
169+
.await
170+
.map_err(Into::into)
171+
}
172+
173+
/// Connects a WhatsApp call (handles the SDP exchange)
174+
///
175+
/// # Arguments
176+
/// * `call_id` - Call ID sent by Meta
177+
/// * `sdp` - The SDP from Meta (answer SDP for business-initiated call)
178+
///
179+
/// # Returns
180+
/// Empty response on success
181+
pub async fn connect_whatsapp_call(
182+
&self,
183+
call_id: impl Into<String>,
184+
sdp: proto::SessionDescription,
185+
) -> ServiceResult<proto::ConnectWhatsAppCallResponse> {
186+
self.client
187+
.request(
188+
SVC,
189+
"ConnectWhatsAppCall",
190+
proto::ConnectWhatsAppCallRequest {
191+
whatsapp_call_id: call_id.into(),
192+
sdp: Some(sdp),
193+
},
194+
self.base
195+
.auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
196+
)
197+
.await
198+
.map_err(Into::into)
199+
}
200+
201+
/// Accepts an incoming WhatsApp call
202+
///
203+
/// # Arguments
204+
/// * `phone_number_id` - The number of the business connecting the call
205+
/// * `api_key` - The API key of the business connecting the call
206+
/// * `cloud_api_version` - WhatsApp Cloud API version (e.g., "23.0", "24.0")
207+
/// * `call_id` - Call ID sent by Meta
208+
/// * `sdp` - The SDP from Meta (for user-initiated call)
209+
/// * `options` - Additional options for the call
210+
///
211+
/// # Returns
212+
/// Information about the accepted call including the room name
213+
pub async fn accept_whatsapp_call(
214+
&self,
215+
phone_number_id: impl Into<String>,
216+
api_key: impl Into<String>,
217+
cloud_api_version: impl Into<String>,
218+
call_id: impl Into<String>,
219+
sdp: proto::SessionDescription,
220+
options: AcceptWhatsAppCallOptions,
221+
) -> ServiceResult<proto::AcceptWhatsAppCallResponse> {
222+
self.client
223+
.request(
224+
SVC,
225+
"AcceptWhatsAppCall",
226+
proto::AcceptWhatsAppCallRequest {
227+
whatsapp_phone_number_id: phone_number_id.into(),
228+
whatsapp_api_key: api_key.into(),
229+
whatsapp_cloud_api_version: cloud_api_version.into(),
230+
whatsapp_call_id: call_id.into(),
231+
whatsapp_biz_opaque_callback_data: options.biz_opaque_callback_data.unwrap_or_default(),
232+
sdp: Some(sdp),
233+
room_name: options.room_name.unwrap_or_default(),
234+
agents: options.agents.unwrap_or_default(),
235+
participant_identity: options.participant_identity.unwrap_or_default(),
236+
participant_name: options.participant_name.unwrap_or_default(),
237+
participant_metadata: options.participant_metadata.unwrap_or_default(),
238+
participant_attributes: options.participant_attributes.unwrap_or_default(),
239+
destination_country: options.destination_country.unwrap_or_default(),
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 Twilio call
249+
///
250+
/// # Arguments
251+
/// * `direction` - The direction of the call (inbound or outbound)
252+
/// * `room_name` - What LiveKit room should this call be connected to
253+
/// * `options` - Additional options for the call
254+
///
255+
/// # Returns
256+
/// The WebSocket URL which Twilio media stream should connect to
257+
pub async fn connect_twilio_call(
258+
&self,
259+
direction: proto::connect_twilio_call_request::TwilioCallDirection,
260+
room_name: impl Into<String>,
261+
options: ConnectTwilioCallOptions,
262+
) -> ServiceResult<proto::ConnectTwilioCallResponse> {
263+
self.client
264+
.request(
265+
SVC,
266+
"ConnectTwilioCall",
267+
proto::ConnectTwilioCallRequest {
268+
twilio_call_direction: direction as i32,
269+
room_name: room_name.into(),
270+
agents: options.agents.unwrap_or_default(),
271+
participant_identity: options.participant_identity.unwrap_or_default(),
272+
participant_name: options.participant_name.unwrap_or_default(),
273+
participant_metadata: options.participant_metadata.unwrap_or_default(),
274+
participant_attributes: options.participant_attributes.unwrap_or_default(),
275+
destination_country: options.destination_country.unwrap_or_default(),
276+
},
277+
self.base
278+
.auth_header(VideoGrants { room_create: true, ..Default::default() }, None)?,
279+
)
280+
.await
281+
.map_err(Into::into)
282+
}
283+
}

livekit-api/src/services/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::access_token::{AccessToken, AccessTokenError, SIPGrants, VideoGrants}
2222
pub use twirp_client::{TwirpError, TwirpErrorCode, TwirpResult};
2323

2424
pub mod agent_dispatch;
25+
pub mod connector;
2526
pub mod egress;
2627
pub mod ingress;
2728
pub mod room;

livekit-protocol/generate_proto.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ protoc \
3131
$PROTOCOL/livekit_room.proto \
3232
$PROTOCOL/livekit_webhook.proto \
3333
$PROTOCOL/livekit_sip.proto \
34-
$PROTOCOL/livekit_models.proto
34+
$PROTOCOL/livekit_models.proto \
35+
$PROTOCOL/livekit_connector.proto \
36+
$PROTOCOL/livekit_connector_whatsapp.proto \
37+
$PROTOCOL/livekit_connector_twilio.proto

livekit-protocol/protocol

Submodule protocol updated 145 files

0 commit comments

Comments
 (0)