diff --git a/.changeset/red-squids-double.md b/.changeset/red-squids-double.md new file mode 100644 index 00000000..2372f90a --- /dev/null +++ b/.changeset/red-squids-double.md @@ -0,0 +1,5 @@ +--- +"livekit-server-sdk": minor +--- + +feat(connector): initial client impl diff --git a/packages/livekit-server-sdk/package.json b/packages/livekit-server-sdk/package.json index f202478f..3881a3ac 100644 --- a/packages/livekit-server-sdk/package.json +++ b/packages/livekit-server-sdk/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^1.10.1", - "@livekit/protocol": "^1.42.0", + "@livekit/protocol": "^1.43.1", "camelcase-keys": "^9.0.0", "jose": "^5.1.2" }, diff --git a/packages/livekit-server-sdk/src/ConnectorClient.ts b/packages/livekit-server-sdk/src/ConnectorClient.ts new file mode 100644 index 00000000..0500aab4 --- /dev/null +++ b/packages/livekit-server-sdk/src/ConnectorClient.ts @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { + ConnectTwilioCallRequest_TwilioCallDirection, + RoomAgentDispatch, + SessionDescription, +} from '@livekit/protocol'; +import { + AcceptWhatsAppCallRequest, + AcceptWhatsAppCallResponse, + ConnectTwilioCallRequest, + ConnectTwilioCallResponse, + ConnectWhatsAppCallRequest, + ConnectWhatsAppCallResponse, + DialWhatsAppCallRequest, + DialWhatsAppCallResponse, + DisconnectWhatsAppCallRequest, + DisconnectWhatsAppCallResponse, +} from '@livekit/protocol'; +import type { ClientOptions } from './ClientOptions.js'; +import { ServiceBase } from './ServiceBase.js'; +import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js'; + +const svc = 'Connector'; + +// WhatsApp types +export interface DialWhatsAppCallOptions { + /** Required - The identifier of the WhatsApp phone number that is initiating the call */ + whatsappPhoneNumberId: string; + /** Required - The number of the user that is supposed to receive the call */ + whatsappToPhoneNumber: string; + /** Required - The API key of the business that is initiating the call */ + whatsappApiKey: string; + /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */ + whatsappCloudApiVersion: string; + /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */ + whatsappBizOpaqueCallbackData?: string; + /** Optional - What LiveKit room should this participant be connected to */ + roomName?: string; + /** Optional - Agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional - Identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional - Name of the participant in LiveKit room */ + participantName?: string; + /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +export interface AcceptWhatsAppCallOptions { + /** Required - The identifier of the WhatsApp phone number that is connecting the call */ + whatsappPhoneNumberId: string; + /** Required - The API key of the business that is connecting the call */ + whatsappApiKey: string; + /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */ + whatsappCloudApiVersion: string; + /** Required - Call ID sent by Meta */ + whatsappCallId: string; + /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */ + whatsappBizOpaqueCallbackData?: string; + /** Required - The call accept webhook comes with SDP from Meta */ + sdp: SessionDescription; + /** Optional - What LiveKit room should this participant be connected to */ + roomName?: string; + /** Optional - Agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional - Identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional - Name of the participant in LiveKit room */ + participantName?: string; + /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +// Twilio types +export interface ConnectTwilioCallOptions { + /** The direction of the call */ + twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection; + /** What LiveKit room should this call be connected to */ + roomName: string; + /** Optional agents to dispatch the call to */ + agents?: RoomAgentDispatch[]; + /** Optional identity of the participant in LiveKit room */ + participantIdentity?: string; + /** Optional name of the participant in LiveKit room */ + participantName?: string; + /** Optional user-defined metadata. Will be attached to a created Participant in the room. */ + participantMetadata?: string; + /** Optional user-defined attributes. Will be attached to a created Participant in the room. */ + participantAttributes?: { [key: string]: string }; + /** Country where the call terminates as ISO 3166-1 alpha-2 */ + destinationCountry?: string; +} + +/** + * Client to access Connector APIs for WhatsApp and Twilio integrations + */ +export class ConnectorClient extends ServiceBase { + private readonly rpc: Rpc; + + /** + * @param host - hostname including protocol. i.e. 'https://.livekit.cloud' + * @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY + * @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET + * @param options - client options + */ + constructor(host: string, apiKey?: string, secret?: string, options?: ClientOptions) { + super(apiKey, secret); + const rpcOptions = options?.requestTimeout + ? { requestTimeout: options.requestTimeout } + : undefined; + this.rpc = new TwirpRpc(host, livekitPackage, rpcOptions); + } + + /** + * Initiate an outbound WhatsApp call + * + * @param options - WhatsApp call options + * @returns Promise containing the WhatsApp call ID and room name + */ + async dialWhatsAppCall(options: DialWhatsAppCallOptions): Promise { + const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || ''; + const roomName = options.roomName || ''; + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new DialWhatsAppCallRequest({ + whatsappPhoneNumberId: options.whatsappPhoneNumberId, + whatsappToPhoneNumber: options.whatsappToPhoneNumber, + whatsappApiKey: options.whatsappApiKey, + whatsappCloudApiVersion: options.whatsappCloudApiVersion, + whatsappBizOpaqueCallbackData, + roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'DialWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return DialWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Accept an inbound WhatsApp call + * + * @param options - WhatsApp call accept options + * @returns Promise containing the room name + */ + async acceptWhatsAppCall( + options: AcceptWhatsAppCallOptions, + ): Promise { + const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || ''; + const roomName = options.roomName || ''; + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new AcceptWhatsAppCallRequest({ + whatsappPhoneNumberId: options.whatsappPhoneNumberId, + whatsappApiKey: options.whatsappApiKey, + whatsappCloudApiVersion: options.whatsappCloudApiVersion, + whatsappCallId: options.whatsappCallId, + whatsappBizOpaqueCallbackData, + sdp: options.sdp, + roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'AcceptWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return AcceptWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Connect an established WhatsApp call (used for business-initiated calls) + * + * @param whatsappCallId - Call ID sent by Meta + * @param sdp - Session description from Meta + */ + async connectWhatsAppCall( + whatsappCallId: string, + sdp: SessionDescription, + ): Promise { + const req = new ConnectWhatsAppCallRequest({ + whatsappCallId, + sdp, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'ConnectWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return ConnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Disconnect an active WhatsApp call + * + * @param whatsappCallId - Call ID sent by Meta + * @param whatsappApiKey - The API key of the business that is disconnecting the call + */ + async disconnectWhatsAppCall( + whatsappCallId: string, + whatsappApiKey: string, + ): Promise { + const req = new DisconnectWhatsAppCallRequest({ + whatsappCallId, + whatsappApiKey, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'DisconnectWhatsAppCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return DisconnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } + + /** + * Connect a Twilio call to a LiveKit room + * + * @param options - Twilio call connection options + * @returns Promise containing the WebSocket connect URL for Twilio media stream + */ + async connectTwilioCall(options: ConnectTwilioCallOptions): Promise { + const participantIdentity = options.participantIdentity || ''; + const participantName = options.participantName || ''; + const participantMetadata = options.participantMetadata || ''; + const destinationCountry = options.destinationCountry || ''; + + const req = new ConnectTwilioCallRequest({ + twilioCallDirection: options.twilioCallDirection, + roomName: options.roomName, + agents: options.agents, + participantIdentity, + participantName, + participantMetadata, + participantAttributes: options.participantAttributes, + destinationCountry, + }).toJson(); + + const data = await this.rpc.request( + svc, + 'ConnectTwilioCall', + req, + await this.authHeader({ roomCreate: true }), + ); + return ConnectTwilioCallResponse.fromJson(data, { ignoreUnknownFields: true }); + } +} diff --git a/packages/livekit-server-sdk/src/index.ts b/packages/livekit-server-sdk/src/index.ts index e9ba43c2..3fdf1c1d 100644 --- a/packages/livekit-server-sdk/src/index.ts +++ b/packages/livekit-server-sdk/src/index.ts @@ -3,14 +3,20 @@ // SPDX-License-Identifier: Apache-2.0 export { + AcceptWhatsAppCallResponse, AliOSSUpload, AgentDispatch, AudioCodec, AutoParticipantEgress, AutoTrackEgress, AzureBlobUpload, + ConnectTwilioCallRequest_TwilioCallDirection, + ConnectTwilioCallResponse, + ConnectWhatsAppCallResponse, DataPacket_Kind, + DialWhatsAppCallResponse, DirectFileOutput, + DisconnectWhatsAppCallResponse, EgressInfo, EgressStatus, EncodedFileOutput, @@ -40,6 +46,7 @@ export { RoomConfiguration, RoomEgress, S3Upload, + SessionDescription, SIPDispatchRule, SIPDispatchRuleInfo, SIPDispatchRuleDirect, @@ -64,6 +71,7 @@ export { } from '@livekit/protocol'; export * from './AccessToken.js'; export * from './AgentDispatchClient.js'; +export * from './ConnectorClient.js'; export * from './EgressClient.js'; export * from './grants.js'; export * from './IngressClient.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b2a5da8..c42e706e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,8 +270,8 @@ importers: specifier: ^1.10.1 version: 1.10.1 '@livekit/protocol': - specifier: ^1.42.0 - version: 1.42.0 + specifier: ^1.43.1 + version: 1.43.1 camelcase-keys: specifier: ^9.0.0 version: 9.1.3 @@ -995,8 +995,8 @@ packages: '@livekit/mutex@1.1.1': resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} - '@livekit/protocol@1.42.0': - resolution: {integrity: sha512-42sYSCay2PZrn5yHHt+O3RQpTElcTrA7bqg7iYbflUApeerA5tUCJDr8Z4abHsYHVKjqVUbkBq/TPmT3X6aYOQ==} + '@livekit/protocol@1.43.1': + resolution: {integrity: sha512-0NiinrCw9PruS9rFqr/F4UhLn0t09DDjSRgMnU3uu2iHT7uW4wgXPBlTp9HZ/nSShsSd0YCcG2HLX3ltwgkVcw==} '@livekit/typed-emitter@3.0.0': resolution: {integrity: sha512-9bl0k4MgBPZu3Qu3R3xy12rmbW17e3bE9yf4YY85gJIQ3ezLEj/uzpKHWBsLaDoL5Mozz8QCgggwIBudYQWeQg==} @@ -4417,7 +4417,7 @@ snapshots: '@livekit/mutex@1.1.1': {} - '@livekit/protocol@1.42.0': + '@livekit/protocol@1.43.1': dependencies: '@bufbuild/protobuf': 1.10.1