From 673502475e30b13dc49e39e34612ae01d47a4e9d Mon Sep 17 00:00:00 2001 From: Chuck Reeves Date: Tue, 8 Oct 2024 14:10:52 -0400 Subject: [PATCH] feat: added user-to-user headers for sip endpoint (#965) --- .../voice/__tests__/__dataSets__/calls.ts | 315 +++++++++--------- .../voice/__tests__/__dataSets__/create.ts | 94 ++++++ .../voice/lib/classes/Endpoint/SIPEndpoint.ts | 6 +- packages/voice/lib/enums/TTSLanguages.ts | 5 + .../voice/lib/types/Endpoint/SIPEndpoint.ts | 17 +- packages/voice/lib/voice.ts | 20 ++ 6 files changed, 298 insertions(+), 159 deletions(-) diff --git a/packages/voice/__tests__/__dataSets__/calls.ts b/packages/voice/__tests__/__dataSets__/calls.ts index 64930a2d..eac18dc3 100644 --- a/packages/voice/__tests__/__dataSets__/calls.ts +++ b/packages/voice/__tests__/__dataSets__/calls.ts @@ -1,5 +1,10 @@ import { Client } from '@vonage/server-client'; -import { CallDetailResponse, CallPageResponse } from '../../lib'; +import { + CallDetailResponse, + CallPageResponse, + CallStatus, + CallListFilter, +} from '../../lib'; import { callSip, callPhone, callWebsocket } from '../common'; import { BASE_URL } from '../common'; @@ -143,158 +148,158 @@ export default [ }, ], }, -// { -// label: 'get a page of calls', -// requests: [['/v1/calls?', 'GET']], -// responses: [ -// [ -// 200, -// { -// count: 3, -// page_size: 1, -// record_index: 0, -// _embedded: { -// calls: [ -// { -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, -// }, -// }, -// }, -// ], -// }, -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/`, -// }, -// }, -// } as CallPageResponse, -// ], -// ], -// clientMethod: 'getCallsPage', -// parameters: [], -// generator: false, -// error: false, -// expected: { -// count: 3, -// page_size: 1, -// record_index: 0, -// _embedded: { -// calls: [ -// { -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, -// }, -// }, -// }, -// ], -// }, -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/`, -// }, -// }, -// }, -// }, -// { -// label: 'search', -// requests: [ -// [ -// `/v1/calls?status=${CallStatus.ANSWERED}&date_start=453168000&date_end=1302552660&page_size=1&record_index=0&order=asc&conversation_uuid=${callPhone.conversationUUID}`, -// 'GET', -// ], -// ], -// responses: [ -// [ -// 200, -// { -// count: 3, -// page_size: 1, -// record_index: 0, -// _embedded: { -// calls: [ -// { -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, -// }, -// }, -// }, -// ], -// }, -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/`, -// }, -// }, -// } as CallPageResponse, -// ], -// ], -// clientMethod: 'search', -// parameters: [ -// { -// status: CallStatus.ANSWERED, -// date_start: '453168000', -// date_end: '1302552660', -// page_size: '1', -// record_index: '0', -// order: 'asc', -// conversation_uuid: callPhone.conversationUUID, -// } as CallListFilter, -// ], -// generator: false, -// error: false, -// expected: { -// count: 3, -// page_size: 1, -// record_index: 0, -// _embedded: { -// calls: [ -// { -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, -// }, -// }, -// }, -// ], -// }, -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/`, -// }, -// }, -// }, -// }, -// { -// label: 'get call', -// requests: [[`/v1/calls/${callPhone.uuid}`, 'GET']], -// responses: [ -// [ -// 200, -// { -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// _links: { -// self: { -// href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, -// }, -// }, -// } as CallDetailResponse, -// ], -// ], -// clientMethod: 'getCall', -// parameters: [callPhone.uuid], -// generator: false, -// error: false, -// expected: { -// ...callPhone, -// ...Client.transformers.snakeCaseObjectKeys(callPhone), -// }, -// }, + { + label: 'get a page of calls', + requests: [['/v1/calls?', 'GET']], + responses: [ + [ + 200, + { + count: 3, + page_size: 1, + record_index: 0, + _embedded: { + calls: [ + { + ...Client.transformers.snakeCaseObjectKeys(callPhone), + _links: { + self: { + href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, + }, + }, + }, + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/calls/`, + }, + }, + } as CallPageResponse, + ], + ], + clientMethod: 'getCallsPage', + parameters: [], + generator: false, + error: false, + expected: { + count: 3, + page_size: 1, + record_index: 0, + _embedded: { + calls: [ + { + ...Client.transformers.snakeCaseObjectKeys(callPhone), + _links: { + self: { + href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, + }, + }, + }, + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/calls/`, + }, + }, + }, + }, + { + label: 'search', + requests: [ + [ + `/v1/calls?status=${CallStatus.ANSWERED}&date_start=453168000&date_end=1302552660&page_size=1&record_index=0&order=asc&conversation_uuid=${callPhone.conversationUUID}`, + 'GET', + ], + ], + responses: [ + [ + 200, + { + count: 3, + page_size: 1, + record_index: 0, + _embedded: { + calls: [ + { + ...Client.transformers.snakeCaseObjectKeys(callPhone), + _links: { + self: { + href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, + }, + }, + }, + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/calls/`, + }, + }, + } as CallPageResponse, + ], + ], + clientMethod: 'search', + parameters: [ + { + status: CallStatus.ANSWERED, + date_start: '453168000', + date_end: '1302552660', + page_size: '1', + record_index: '0', + order: 'asc', + conversation_uuid: callPhone.conversationUUID, + } as CallListFilter, + ], + generator: false, + error: false, + expected: { + count: 3, + page_size: 1, + record_index: 0, + _embedded: { + calls: [ + { + ...Client.transformers.snakeCaseObjectKeys(callPhone), + _links: { + self: { + href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, + }, + }, + }, + ], + }, + _links: { + self: { + href: `${BASE_URL}/v1/calls/`, + }, + }, + }, + }, + { + label: 'get call', + requests: [[`/v1/calls/${callPhone.uuid}`, 'GET']], + responses: [ + [ + 200, + { + ...Client.transformers.snakeCaseObjectKeys(callPhone), + _links: { + self: { + href: `${BASE_URL}/v1/calls/${callPhone.uuid}`, + }, + }, + } as CallDetailResponse, + ], + ], + clientMethod: 'getCall', + parameters: [callPhone.uuid], + generator: false, + error: false, + expected: { + ...callPhone, + ...Client.transformers.snakeCaseObjectKeys(callPhone), + }, + }, ]; diff --git a/packages/voice/__tests__/__dataSets__/create.ts b/packages/voice/__tests__/__dataSets__/create.ts index 4f6074a9..7baab6ef 100644 --- a/packages/voice/__tests__/__dataSets__/create.ts +++ b/packages/voice/__tests__/__dataSets__/create.ts @@ -207,4 +207,98 @@ export default [ conversationUUID: callPhone.conversationUUID, } as CallResult, }, + { + label: 'create a call with sip endpoint', + requests: [ + [ + '/v1/calls?', + 'POST', + { + to: [ + { + type: 'sip', + uri: 'sip://sip.example.com', + headers: { + 'x-foo': 'bar', + }, + standard_headers: { + 'User-to-User': '42', + } + }, + ], + from: { + type: 'phone', + number: '14152739164', + }, + answer_url: ['https://example.com/answer'], + answer_method: HttpMethod.GET, + random_from_number: false, + event_url: ['example.com'], + event_method: HttpMethod.GET, + machine_detection: MachineDetectionBehavior.CONTINUE, + advanced_machine_detection: { + behavior: MachineDetectionBehavior.HANGUP, + mode: AdvancedMachineDetectionMode.DETECT, + beep_timeout: 42, + }, + length_timer: 84, + ringing_timer: 126, + }, + ], + ], + responses: [ + [ + 201, + { + uuid: callPhone.uuid, + status: CallStatus.STARTED, + direction: CallDirection.OUTBOUND, + conversation_uuid: callPhone.conversationUUID, + } as CreateCallResponse, + ], + ], + clientMethod: 'createOutboundCall', + parameters: [ + { + answerUrl: ['https://example.com/answer'], + answerMethod: HttpMethod.GET, + to: [ + { + type: 'sip', + uri: 'sip://sip.example.com', + headers: { + 'x-foo': 'bar', + }, + standardHeaders: { + userToUser: '42', + } + }, + ], + from: { + type: 'phone', + number: '14152739164', + }, + randomFromNumber: false, + eventUrl: ['example.com'], + eventMethod: HttpMethod.GET, + machineDetection: MachineDetectionBehavior.CONTINUE, + advancedMachineDetection: { + behavior: MachineDetectionBehavior.HANGUP, + mode: AdvancedMachineDetectionMode.DETECT, + beepTimeout: 42, + }, + lengthTimer: 84, + ringingTimer: 126, + } as CallWithAnswerURL, + ], + generator: false, + error: false, + expected: { + uuid: callPhone.uuid, + status: CallStatus.STARTED, + direction: CallDirection.OUTBOUND, + conversation_uuid: callPhone.conversationUUID, + conversationUUID: callPhone.conversationUUID, + } as CallResult, + }, ]; diff --git a/packages/voice/lib/classes/Endpoint/SIPEndpoint.ts b/packages/voice/lib/classes/Endpoint/SIPEndpoint.ts index 89e74140..b5908010 100644 --- a/packages/voice/lib/classes/Endpoint/SIPEndpoint.ts +++ b/packages/voice/lib/classes/Endpoint/SIPEndpoint.ts @@ -2,7 +2,7 @@ import { SIPEndpoint as SIPEndpointType } from '../../types/Endpoint/SIPEndpoint import debug from 'debug'; debug('@vonage/voice')( - + 'This class is deprecated. Please update to use the SIPEndpointType type instead', ); @@ -28,7 +28,7 @@ export class SIPEndpoint implements SIPEndpointType { * * @param {Array>} headers - Optional custom headers as an array of key-value pairs. */ - headers?: Array>; + headers?: Record; /** * Create a new SIPEndpoint instance. @@ -41,7 +41,7 @@ export class SIPEndpoint implements SIPEndpointType { this.uri = uri; if (headers) { - this.headers = headers; + this.headers = headers[0]; } } } diff --git a/packages/voice/lib/enums/TTSLanguages.ts b/packages/voice/lib/enums/TTSLanguages.ts index d158e089..6cb4647c 100644 --- a/packages/voice/lib/enums/TTSLanguages.ts +++ b/packages/voice/lib/enums/TTSLanguages.ts @@ -62,6 +62,11 @@ export enum TTSLanguages { */ DE_DE = 'de-DE', + /** + * Ethiopia German (de-ET) - Supported Text-to-Speech (TTS) language. + */ + DE_ET = 'de-ET', + /** * Greek (el-GR) - Supported Text-to-Speech (TTS) language. */ diff --git a/packages/voice/lib/types/Endpoint/SIPEndpoint.ts b/packages/voice/lib/types/Endpoint/SIPEndpoint.ts index 2020da70..1bc11069 100644 --- a/packages/voice/lib/types/Endpoint/SIPEndpoint.ts +++ b/packages/voice/lib/types/Endpoint/SIPEndpoint.ts @@ -15,5 +15,20 @@ export type SIPEndpoint = { /** * An optional array of headers as key-value pairs. These headers can be included in the SIP request. */ - headers?: Array>; + headers?: Record; + + /** + * Standard SIP INVITE headers. Unlike the headers property, these are not + * prepended with X-. + */ + standardHeaders?: { + /** + * Transmit user-to-user information if supported by the CC / PBX vendor, + * as per RFC 7433. + * + * @link https://tools.ietf.org/html/rfc7433 + */ + userToUser: string; + } }; + diff --git a/packages/voice/lib/voice.ts b/packages/voice/lib/voice.ts index abaa32e0..63b8e514 100644 --- a/packages/voice/lib/voice.ts +++ b/packages/voice/lib/voice.ts @@ -238,19 +238,39 @@ export class Voice extends Client { */ async createOutboundCall(call: OutboundCall): Promise { const callRequest = Client.transformers.snakeCaseObjectKeys(call, true); + if ((call as CallWithNCCO).ncco) { callRequest.ncco = (call as CallWithNCCO).ncco; } + const to = call.to.map((endpoint) => { + switch (endpoint.type) { + case 'sip': + return { + type: 'sip', + uri: endpoint.uri, + headers: endpoint.headers, + standard_headers: { + 'User-to-User': endpoint.standardHeaders?.userToUser, + } + }; + } + + return endpoint; + }); + + callRequest.to = to; const resp = await this.sendPostRequest( `${this.config.apiHost}/v1/calls`, callRequest, ); + const result = Client.transformers.camelCaseObjectKeys( resp.data, true, true, ); + delete result.conversationUuid; result.conversationUUID = resp.data.conversation_uuid; return result as CallResult;