diff --git a/livekit-api/src/services/mod.rs b/livekit-api/src/services/mod.rs index f5f6e01a1..a84f7d953 100644 --- a/livekit-api/src/services/mod.rs +++ b/livekit-api/src/services/mod.rs @@ -22,6 +22,7 @@ use crate::access_token::{AccessToken, AccessTokenError, VideoGrants}; pub mod egress; pub mod ingress; pub mod room; +pub mod sip; mod twirp_client; diff --git a/livekit-api/src/services/sip.rs b/livekit-api/src/services/sip.rs new file mode 100644 index 000000000..b9c247ed8 --- /dev/null +++ b/livekit-api/src/services/sip.rs @@ -0,0 +1,229 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ptr::null; +use livekit_protocol as proto; + +use crate::access_token::VideoGrants; +use crate::get_env_keys; +use crate::services::{LIVEKIT_PACKAGE, ServiceBase, ServiceResult}; +use crate::services::ingress::{CreateIngressOptions, IngressListFilter}; +use crate::services::twirp_client::TwirpClient; + +const SVC: &str = "SIP"; + +#[derive(Debug)] +pub struct SIPClient { + base: ServiceBase, + client: TwirpClient, +} + +#[derive(Default, Clone, Debug)] +pub struct CreateSIPTrunkOptions { + /// CIDR or IPs that traffic is accepted from + /// An empty list means all inbound traffic is accepted. + pub inbound_addresses: Vec, + /// Accepted `To` values. This Trunk will only accept a call made to + /// these numbers. This allows you to have distinct Trunks for different phone + /// numbers at the same provider. + pub inbound_numbers: Vec, + /// Username and password used to authenticate inbound SIP invites + /// May be empty to have no Authentication + pub inbound_username: String, + pub inbound_password: String, + + /// IP that SIP INVITE is sent too + pub outbound_address: String, + /// Username and password used to authenticate outbound SIP invites + /// May be empty to have no Authentication + pub outbound_username: String, + pub outbound_password: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ListSIPTrunkFilter { + All, +} + +#[derive(Default, Clone, Debug)] +pub struct CreateSIPDispatchRuleOptions { + /// What trunks are accepted for this dispatch rule + /// If empty all trunks will match this dispatch rule + pub trunk_ids: Vec, + pub hide_phone_number: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ListSIPDispatchRuleFilter { + All, +} + +#[derive(Default, Clone, Debug)] +pub struct CreateSIPParticipantOptions { + /// Optional identity of the participant in LiveKit room + pub participant_identity: String, + /// Optionally send following DTMF digits (extension codes) when making a call. + /// Character 'w' can be used to add a 0.5 sec delay. + pub dtmf: String, + /// Optionally play ringtone in the room as an audible indicator for existing participants + pub play_ringtone: bool, +} + +impl SIPClient { + pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self { + Self { + base: ServiceBase::with_api_key(api_key, api_secret), + client: TwirpClient::new(host, LIVEKIT_PACKAGE, None), + } + } + + pub fn new(host: &str) -> ServiceResult { + let (api_key, api_secret) = get_env_keys()?; + Ok(Self::with_api_key(host, &api_key, &api_secret)) + } + + pub async fn create_sip_trunk( + &self, + number: String, + options: CreateSIPTrunkOptions, + ) -> ServiceResult { + self.client + .request( + SVC, + "CreateSIPTrunk", + proto::CreateSipTrunkRequest { + outbound_number: number.to_owned(), + outbound_address: options.outbound_address.to_owned(), + outbound_username: options.outbound_username.to_owned(), + outbound_password: options.outbound_password.to_owned(), + + inbound_numbers: options.inbound_numbers.to_owned(), + inbound_numbers_regex: Vec::new(), + inbound_addresses: options.inbound_addresses.to_owned(), + inbound_username: options.inbound_username.to_owned(), + inbound_password: options.inbound_password.to_owned(), + }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await + .map_err(Into::into) + } + + pub async fn list_sip_trunk( + &self, + filter: ListSIPTrunkFilter, + ) -> ServiceResult> { + + let resp: proto::ListSipTrunkResponse = self + .client + .request( + SVC, + "ListSIPTrunk", + proto::ListSipTrunkRequest { }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await?; + + Ok(resp.items) + } + + pub async fn delete_sip_trunk(&self, sip_trunk_id: &str) -> ServiceResult { + self.client + .request( + SVC, + "DeleteSIPTrunk", + proto::DeleteSipTrunkRequest { sip_trunk_id: sip_trunk_id.to_owned() }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await + .map_err(Into::into) + } + + pub async fn create_sip_dispatch_rule( + &self, + rule: proto::sip_dispatch_rule::Rule, + options: CreateSIPDispatchRuleOptions, + ) -> ServiceResult { + self.client + .request( + SVC, + "CreateSIPDispatchRule", + proto::CreateSipDispatchRuleRequest { + trunk_ids: options.trunk_ids.to_owned(), + hide_phone_number: options.hide_phone_number, + rule: Some(proto::SipDispatchRule{ + rule: Some(rule.to_owned()), + }), + }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await + .map_err(Into::into) + } + + pub async fn list_sip_dispatch_rule( + &self, + filter: ListSIPDispatchRuleFilter, + ) -> ServiceResult> { + + let resp: proto::ListSipDispatchRuleResponse = self + .client + .request( + SVC, + "ListSIPDispatchRule", + proto::ListSipDispatchRuleRequest { }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await?; + + Ok(resp.items) + } + + pub async fn delete_sip_dispatch_rule(&self, sip_dispatch_rule_id: &str) -> ServiceResult { + self.client + .request( + SVC, + "DeleteSIPDispatchRule", + proto::DeleteSipDispatchRuleRequest { sip_dispatch_rule_id: sip_dispatch_rule_id.to_owned() }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await + .map_err(Into::into) + } + + pub async fn create_sip_participant( + &self, + sip_trunk_id: String, + call_to: String, + room_name: String, + options: CreateSIPParticipantOptions, + ) -> ServiceResult { + self.client + .request( + SVC, + "CreateSIPParticipant", + proto::CreateSipParticipantRequest { + sip_trunk_id: sip_trunk_id.to_owned(), + sip_call_to: call_to.to_owned(), + room_name: room_name.to_owned(), + participant_identity: options.participant_identity.to_owned(), + dtmf: options.dtmf.to_owned(), + play_ringtone: options.play_ringtone, + }, + self.base.auth_header(VideoGrants { ..Default::default() })?, + ) + .await + .map_err(Into::into) + } +} \ No newline at end of file diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index b48a3fd7a..760db090b 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -3559,6 +3559,9 @@ pub struct CreateSipParticipantRequest { /// Character 'w' can be used to add a 0.5 sec delay. #[prost(string, tag="5")] pub dtmf: ::prost::alloc::string::String, + /// Optionally play ringtone in the room as an audible indicator for existing participants + #[prost(bool, tag="6")] + pub play_ringtone: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index d5c79e163..93b2e848d 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -2969,6 +2969,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if !self.dtmf.is_empty() { len += 1; } + if self.play_ringtone { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.CreateSIPParticipantRequest", len)?; if !self.sip_trunk_id.is_empty() { struct_ser.serialize_field("sipTrunkId", &self.sip_trunk_id)?; @@ -2985,6 +2988,9 @@ impl serde::Serialize for CreateSipParticipantRequest { if !self.dtmf.is_empty() { struct_ser.serialize_field("dtmf", &self.dtmf)?; } + if self.play_ringtone { + struct_ser.serialize_field("playRingtone", &self.play_ringtone)?; + } struct_ser.end() } } @@ -3004,6 +3010,8 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "participant_identity", "participantIdentity", "dtmf", + "play_ringtone", + "playRingtone", ]; #[allow(clippy::enum_variant_names)] @@ -3013,6 +3021,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { RoomName, ParticipantIdentity, Dtmf, + PlayRingtone, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -3040,6 +3049,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { "roomName" | "room_name" => Ok(GeneratedField::RoomName), "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), "dtmf" => Ok(GeneratedField::Dtmf), + "playRingtone" | "play_ringtone" => Ok(GeneratedField::PlayRingtone), _ => Ok(GeneratedField::__SkipField__), } } @@ -3064,6 +3074,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { let mut room_name__ = None; let mut participant_identity__ = None; let mut dtmf__ = None; + let mut play_ringtone__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::SipTrunkId => { @@ -3096,6 +3107,12 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { } dtmf__ = Some(map_.next_value()?); } + GeneratedField::PlayRingtone => { + if play_ringtone__.is_some() { + return Err(serde::de::Error::duplicate_field("playRingtone")); + } + play_ringtone__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -3107,6 +3124,7 @@ impl<'de> serde::Deserialize<'de> for CreateSipParticipantRequest { room_name: room_name__.unwrap_or_default(), participant_identity: participant_identity__.unwrap_or_default(), dtmf: dtmf__.unwrap_or_default(), + play_ringtone: play_ringtone__.unwrap_or_default(), }) } }