diff --git a/.changeset/forty-moons-dance.md b/.changeset/forty-moons-dance.md new file mode 100644 index 000000000..b726620a8 --- /dev/null +++ b/.changeset/forty-moons-dance.md @@ -0,0 +1,7 @@ +--- +'@sw-internal/playground-realtime-api': minor +'@signalwire/core': minor +'@signalwire/realtime-api': minor +--- + +Expose the `Voice.createDialer()` method to simplify dialing devices on a Voice Call. diff --git a/internal/playground-realtime-api/src/voice/index.ts b/internal/playground-realtime-api/src/voice/index.ts index eee9c5f53..2ee76e198 100644 --- a/internal/playground-realtime-api/src/voice/index.ts +++ b/internal/playground-realtime-api/src/voice/index.ts @@ -30,17 +30,19 @@ async function run() { }) try { - const call = await client.dial({ - devices: [ - [ - { - type: 'phone', - to: process.env.TO_NUMBER as string, - from: process.env.FROM_NUMBER as string, - timeout: 30, - }, - ], - ], + // Using createDialer util + // const dialer = Voice.createDialer().addPhone({ + // to: process.env.TO_NUMBER as string, + // from: process.env.FROM_NUMBER as string, + // timeout: 30, + // }) + // const call = await client.dial(dialer) + + // Using dialPhone Alias + const call = await client.dialPhone({ + to: process.env.TO_NUMBER as string, + from: process.env.FROM_NUMBER as string, + timeout: 30, }) console.log('Dial resolved!', call.id) diff --git a/packages/core/src/types/voiceCall.ts b/packages/core/src/types/voiceCall.ts index 1b6a19a4c..4ad3f17fd 100644 --- a/packages/core/src/types/voiceCall.ts +++ b/packages/core/src/types/voiceCall.ts @@ -132,6 +132,8 @@ export interface VoiceCallPhoneParams { timeout?: number } +export type OmitType = Omit + export interface VoiceCallSipParams { type: 'sip' from: string @@ -186,19 +188,19 @@ export interface VoiceCallPlayMethodParams { } export interface VoiceCallPlayAudioMethodParams - extends Omit { + extends OmitType { volume?: number } export interface VoiceCallPlaySilenceMethodParams - extends Omit {} + extends OmitType {} export interface VoiceCallPlayRingtoneMethodParams - extends Omit { + extends OmitType { volume?: number } export interface VoiceCallPlayTTSMethodParams - extends Omit { + extends OmitType { volume?: number } @@ -239,19 +241,19 @@ export type VoiceCallPromptMethodParams = SpeechOrDigits & { partialResults?: boolean } export type VoiceCallPromptAudioMethodParams = SpeechOrDigits & - Omit & { + OmitType & { volume?: number initialTimeout?: number partialResults?: boolean } export type VoiceCallPromptRingtoneMethodParams = SpeechOrDigits & - Omit & { + OmitType & { volume?: number initialTimeout?: number partialResults?: boolean } export type VoiceCallPromptTTSMethodParams = SpeechOrDigits & - Omit & { + OmitType & { volume?: number initialTimeout?: number partialResults?: boolean @@ -299,6 +301,19 @@ export type VoiceCallDisconnectReason = | 'decline' | 'error' +export type VoiceCallDialPhoneMethodParams = OmitType +export type VoiceCallDialSipMethodParams = OmitType +export interface CreateVoiceDialerParams { + region?: string +} + +export interface VoiceDialer extends CreateVoiceDialerParams { + devices: VoiceCallDialMethodParams['devices'] + addPhone(params: VoiceCallDialPhoneMethodParams): this + addSip(params: VoiceCallDialSipMethodParams): this + inParallel(dialer: VoiceDialer): this +} + /** * Public Contract for a VoiceCall */ @@ -451,7 +466,7 @@ export interface VoiceCallContract { direction: 'inbound' | 'outbound' headers?: SipHeader[] - dial(params?: VoiceCallDialMethodParams): Promise + dial(params: VoiceDialer): Promise hangup(reason?: VoiceCallDisconnectReason): Promise answer(): Promise play(params: VoiceCallPlayMethodParams): Promise @@ -758,18 +773,18 @@ export interface CallingCallConnectEvent extends SwEvent { * 'calling.call.send_digits */ - export type CallingCallSendDigitsState = 'finished' - export interface CallingCallSendDigitsEventParams { - node_id: string - call_id: string - control_id: string - state: CallingCallSendDigitsState - } - - export interface CallingCallSendDigitsEvent extends SwEvent { - event_type: ToInternalVoiceEvent - params: CallingCallSendDigitsEventParams - } +export type CallingCallSendDigitsState = 'finished' +export interface CallingCallSendDigitsEventParams { + node_id: string + call_id: string + control_id: string + state: CallingCallSendDigitsState +} + +export interface CallingCallSendDigitsEvent extends SwEvent { + event_type: ToInternalVoiceEvent + params: CallingCallSendDigitsEventParams +} /** * ========== diff --git a/packages/realtime-api/src/voice/Call.ts b/packages/realtime-api/src/voice/Call.ts index 12aaf7227..03c323ba7 100644 --- a/packages/realtime-api/src/voice/Call.ts +++ b/packages/realtime-api/src/voice/Call.ts @@ -7,7 +7,7 @@ import { extendComponent, VoiceCallMethods, VoiceCallContract, - VoiceCallDialMethodParams, + VoiceDialer, VoiceCallDisconnectReason, VoiceCallPlayMethodParams, VoiceCallPlayAudioMethodParams, @@ -299,7 +299,7 @@ export class CallConsumer extends AutoApplyTransformsConsumer { this.runWorker('voiceCallDialWorker', { worker: voiceCallDialWorker, @@ -307,12 +307,13 @@ export class CallConsumer extends AutoApplyTransformsConsumer { reject(e) diff --git a/packages/realtime-api/src/voice/Voice.ts b/packages/realtime-api/src/voice/Voice.ts index 19760f051..4e21aa65e 100644 --- a/packages/realtime-api/src/voice/Voice.ts +++ b/packages/realtime-api/src/voice/Voice.ts @@ -1,21 +1,21 @@ -import { - connect, - BaseComponentOptions, - toExternalJSON, - VoiceCallDialMethodParams, -} from '@signalwire/core' +import { connect, BaseComponentOptions, toExternalJSON } from '@signalwire/core' import type { EmitterContract, EventTransform, CallingCallReceiveEventParams, + VoiceDialer, + VoiceCallDialPhoneMethodParams, + VoiceCallDialSipMethodParams, } from '@signalwire/core' import { RealtimeClient } from '../client/index' import { createCallObject, Call } from './Call' import { voiceCallReceiveWorker } from './workers' +import { createDialer } from './utils' import type { RealTimeCallApiEvents } from '../types' import { AutoApplyTransformsConsumer } from '../AutoApplyTransformsConsumer' export * from './VoiceClient' +export { createDialer } /** * List of events for {@link Voice.Call}. @@ -27,7 +27,9 @@ type EmitterTransformsEvents = 'calling.call.received' export interface Voice extends EmitterContract { /** @internal */ _session: RealtimeClient - dial(params: VoiceCallDialMethodParams): Promise + dial(dialer: VoiceDialer): Promise + dialPhone(params: VoiceCallDialPhoneMethodParams): Promise + dialSip(params: VoiceCallDialSipMethodParams): Promise } /** @internal */ @@ -70,6 +72,20 @@ class VoiceAPI extends AutoApplyTransformsConsumer { ], ]) } + + dialPhone(params: VoiceCallDialPhoneMethodParams) { + const dialer = createDialer().addPhone(params) + // dial is available through the VoiceClient Proxy + // @ts-expect-error + return this.dial(dialer) + } + + dialSip(params: VoiceCallDialSipMethodParams) { + const dialer = createDialer().addSip(params) + // dial is available through the VoiceClient Proxy + // @ts-expect-error + return this.dial(dialer) + } } /** @internal */ diff --git a/packages/realtime-api/src/voice/VoiceClient.ts b/packages/realtime-api/src/voice/VoiceClient.ts index 82c7154c7..52666f651 100644 --- a/packages/realtime-api/src/voice/VoiceClient.ts +++ b/packages/realtime-api/src/voice/VoiceClient.ts @@ -38,7 +38,7 @@ const VoiceClient = function (options?: VoiceClientOptions) { return voice.once(...args) } - const callDial: Call['dial'] = async (...args) => { + const callDial: Call['dial'] = async (dialer) => { await clientConnect(client) const call = createCallObject({ @@ -46,7 +46,7 @@ const VoiceClient = function (options?: VoiceClientOptions) { emitter, }) - await call.dial(...args) + await call.dial(dialer) return call } diff --git a/packages/realtime-api/src/voice/utils.test.ts b/packages/realtime-api/src/voice/utils.test.ts index 424aa8d41..dac99f743 100644 --- a/packages/realtime-api/src/voice/utils.test.ts +++ b/packages/realtime-api/src/voice/utils.test.ts @@ -1,4 +1,4 @@ -import { toInternalDevices } from './utils' +import { toInternalDevices, createDialer } from './utils' describe('toInternalDevices', () => { it('should convert the user facing interface to the internal one', () => { @@ -127,3 +127,89 @@ describe('toInternalDevices', () => { ]) }) }) + +describe('createDialer', () => { + it('should build a list of devices to dial', () => { + const dialer = createDialer() + + dialer + .addPhone({ from: '+1', to: '+2', timeout: 30 }) + .addSip({ + from: 'sip:one', + to: 'sip:two', + headers: [{ name: 'foo', value: 'bar' }], + }) + .inParallel( + createDialer() + .addPhone({ from: '+3', to: '+4' }) + .addSip({ + from: 'sip:three', + to: 'sip:four', + headers: [{ name: 'baz', value: 'qux' }], + }) + .addPhone({ from: '+5', to: '+6' }) + ) + + expect(dialer.devices).toStrictEqual([ + [ + { + type: 'phone', + from: '+1', + to: '+2', + timeout: 30, + }, + ], + [ + { + type: 'sip', + from: 'sip:one', + to: 'sip:two', + headers: [{ name: 'foo', value: 'bar' }], + }, + ], + [ + { + type: 'phone', + from: '+3', + to: '+4', + }, + { + type: 'sip', + from: 'sip:three', + to: 'sip:four', + headers: [{ name: 'baz', value: 'qux' }], + }, + { + type: 'phone', + from: '+5', + to: '+6', + }, + ], + ]) + }) + + it('should build a list of devices to dial including region', () => { + const dialer = createDialer({ region: 'us' }) + dialer.inParallel( + createDialer() + .addPhone({ from: '+3', to: '+4' }) + .addPhone({ from: '+5', to: '+6' }) + ) + + expect(dialer.region).toBe('us') + expect(dialer.devices).toStrictEqual([ + [ + { + type: 'phone', + from: '+3', + to: '+4', + }, + { + type: 'phone', + from: '+5', + to: '+6', + }, + ], + ]) + }) +}) diff --git a/packages/realtime-api/src/voice/utils.ts b/packages/realtime-api/src/voice/utils.ts index 8eff224b0..959388cda 100644 --- a/packages/realtime-api/src/voice/utils.ts +++ b/packages/realtime-api/src/voice/utils.ts @@ -3,6 +3,8 @@ import { VoiceCallDialMethodParams, VoiceCallPlayParams, VoiceCallPlayMethodParams, + CreateVoiceDialerParams, + VoiceDialer, toSnakeCaseKeys, } from '@signalwire/core' @@ -73,3 +75,33 @@ export const toInternalPlayParams = ( }) return result } + +export const createDialer = (params: CreateVoiceDialerParams = {}) => { + const devices: VoiceDialer['devices'] = [] + + const dialer: VoiceDialer = { + ...params, + devices, + addPhone(params) { + devices.push([{ type: 'phone', ...params }]) + return dialer + }, + addSip(params) { + devices.push([{ type: 'sip', ...params }]) + return dialer + }, + inParallel(dialer) { + const parallel = dialer.devices.map((row) => { + if (Array.isArray(row)) { + return row[0] + } + return row + }) + devices.push(parallel) + + return dialer + }, + } + + return dialer +}