From 140b2a2fdf088e9a7aad4fd74f65ca9d7d379cbb Mon Sep 17 00:00:00 2001 From: sehmer Date: Fri, 14 Jan 2022 14:25:35 +0100 Subject: [PATCH 1/4] feat: implement playAudioAndHangup Co-authored-by: Michel Schwarz --- lib/webhook/audioUtils.ts | 1 + lib/webhook/webhook.ts | 38 ++++++++++++++++++++++++++++++++++++ lib/webhook/webhook.types.ts | 2 ++ 3 files changed, 41 insertions(+) diff --git a/lib/webhook/audioUtils.ts b/lib/webhook/audioUtils.ts index a42aeda..4365ee9 100644 --- a/lib/webhook/audioUtils.ts +++ b/lib/webhook/audioUtils.ts @@ -7,6 +7,7 @@ export interface ValidateOptions { bitsPerSample?: number; sampleRate?: number; numberOfChannels?: number; + duration?:number; } interface ValidateResult { diff --git a/lib/webhook/webhook.ts b/lib/webhook/webhook.ts index 91c3646..ff559e3 100644 --- a/lib/webhook/webhook.ts +++ b/lib/webhook/webhook.ts @@ -26,6 +26,7 @@ import { isSipgateSignature } from './signatureVerifier'; import { js2xml } from 'xml-js'; import { parse } from 'qs'; import { validateAnnouncementAudio } from './audioUtils'; +import { createRTCMModule, SipgateIOClient } from '..'; interface WebhookApiResponse { _declaration: { @@ -330,6 +331,43 @@ export const WebhookResponse: WebhookResponseInterface = { return { Play: { Url: playOptions.announcement } }; }, + playAudioAndHangUp: async ( + playOptions: PlayOptions, + client: SipgateIOClient, + callId: string, + timeout?: number + ): Promise => { + const validationResult = await validateAnnouncementAudio( + playOptions.announcement + ); + + if (!validationResult.isValid) { + throw new Error( + `\n\n${ + WebhookErrorMessage.AUDIO_FORMAT_ERROR + }\nYour format was: ${JSON.stringify(validationResult.metadata)}\n` + ); + } + + let duration = validationResult.metadata.duration + ? validationResult.metadata.duration * 1000 + : 0; + + duration += timeout ? timeout : 0; + + setTimeout(() => { + const rtcm = createRTCMModule(client); + try { + rtcm.hangUp({ callId }); + } catch (error) { + console.log(error) + return; + } + }, duration); + + return { Play: { Url: playOptions.announcement } }; + }, + redirectCall: (redirectOptions: RedirectOptions): RedirectObject => { return { Dial: { diff --git a/lib/webhook/webhook.types.ts b/lib/webhook/webhook.types.ts index 5eca80a..8563fc4 100644 --- a/lib/webhook/webhook.types.ts +++ b/lib/webhook/webhook.types.ts @@ -1,4 +1,5 @@ import { Server } from 'http'; +import { SipgateIOClient } from '..'; export enum EventType { NEW_CALL = 'newCall', @@ -50,6 +51,7 @@ export interface WebhookResponseInterface { redirectCall: (redirectOptions: RedirectOptions) => RedirectObject; gatherDTMF: (gatherOptions: GatherOptions) => Promise; playAudio: (playOptions: PlayOptions) => Promise; + playAudioAndHangUp: (playOptions: PlayOptions, client: SipgateIOClient, callId: string, timeout?: number) => Promise; rejectCall: (rejectOptions: RejectOptions) => RejectObject; hangUpCall: () => HangUpObject; sendToVoicemail: () => VoicemailObject; From 8c77a8cc6efa541e401029b4b6d67e8670cc311c Mon Sep 17 00:00:00 2001 From: mschwarz Date: Fri, 14 Jan 2022 15:37:48 +0100 Subject: [PATCH 2/4] test: add test for playAudioAndHangUp -e -n -e -n Co-authored-by: Malte Sehmer -e -n Co-authored-by: Philip Kiparra --- lib/webhook/webhook.test.node.ts | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lib/webhook/webhook.test.node.ts b/lib/webhook/webhook.test.node.ts index 8a18cc3..9be53bf 100644 --- a/lib/webhook/webhook.test.node.ts +++ b/lib/webhook/webhook.test.node.ts @@ -5,6 +5,7 @@ import qs from 'qs'; import * as audioUtils from './audioUtils'; import { WebhookErrorMessage } from './webhook.errors'; +import { SipgateIOClient } from '../core/sipgateIOClient'; const mockedGetAudioMetadata = jest.spyOn(audioUtils, 'getAudioMetadata'); @@ -113,6 +114,15 @@ describe('create webhook module', () => { }); describe('create webhook-"Response" module', () => { + + let mockClient: SipgateIOClient; + jest.spyOn(global, 'setTimeout'); + + beforeEach(() => { + mockClient = {} as SipgateIOClient; + jest.useFakeTimers(); + }); + it('should return a gather object without play tag', async () => { const gatherOptions = { maxDigits: 1, timeout: 2000 }; const gatherObject = { @@ -227,6 +237,84 @@ describe('create webhook-"Response" module', () => { expect(result).toEqual(playObject); }); + it('should return a play audio object for a valid audio file with hangUp and timeOut', async () => { + + const duration = 7140; + const timeout = 1000; + + mockedGetAudioMetadata.mockReturnValue( + new Promise((resolve) => + resolve({ + container: 'WAVE', + codec: 'PCM', + bitsPerSample: 16, + sampleRate: 8000, + numberOfChannels: 1, + duration: duration/1000 + }) + ) + ); + + mockClient.delete = jest + .fn() + .mockImplementation(() => Promise.resolve()); + + const testUrl = 'www.testurl.com'; + const callId = '1234567890'; + + const playOptions = { + announcement: testUrl, + }; + const result = await WebhookResponse.playAudioAndHangUp(playOptions, mockClient, callId, timeout); + const playObject = { Play: { Url: testUrl } }; + + expect(result).toEqual(playObject); + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), duration+timeout); + jest.runAllTimers(); + expect(mockClient.delete).toHaveBeenCalledTimes(1); + expect(mockClient.delete).toHaveBeenCalledWith(`/calls/${callId}`); + }); + + it('should return a play audio object for a valid audio file with hangUp and without timeOut', async () => { + + const duration = 7140; + const timeout = 0; + + mockedGetAudioMetadata.mockReturnValue( + new Promise((resolve) => + resolve({ + container: 'WAVE', + codec: 'PCM', + bitsPerSample: 16, + sampleRate: 8000, + numberOfChannels: 1, + duration: duration/1000 + }) + ) + ); + + mockClient.delete = jest + .fn() + .mockImplementation(() => Promise.resolve()); + + const testUrl = 'www.testurl.com'; + const callId = '1234567890'; + + const playOptions = { + announcement: testUrl, + }; + const result = await WebhookResponse.playAudioAndHangUp(playOptions, mockClient, callId); + const playObject = { Play: { Url: testUrl } }; + + expect(result).toEqual(playObject); + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), duration+timeout); + jest.runAllTimers(); + expect(mockClient.delete).toHaveBeenCalledTimes(1); + expect(mockClient.delete).toHaveBeenCalledWith(`/calls/${callId}`); + }); + it('should throw an exception for an invalid audio file in play audio', async () => { mockedGetAudioMetadata.mockReturnValue( new Promise((resolve) => From de5e1b14ee6615ad2e98702c4674d1c940929589 Mon Sep 17 00:00:00 2001 From: mschwarz Date: Fri, 14 Jan 2022 15:53:27 +0100 Subject: [PATCH 3/4] docs: add description for playAudioAndHangUp -e -n -e -n Co-authored-by: Malte Sehmer -e -n Co-authored-by: Philip Kiparra --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98dd5c8..ef19c61 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,7 @@ interface WebhookResponseInterface { sendToVoicemail: () => VoicemailObject; rejectCall: (rejectOptions: RejectOptions) => RejectObject; playAudio: (playOptions: PlayOptions) => PlayObject; + playAudioAndHangUp: (playOptions: PlayOptions, client: SipgateIOClient, callId: string, timeout?: number) => Promise; gatherDTMF: (gatherOptions: GatherOptions) => GatherObject; hangUpCall: () => HangUpObject; } @@ -530,7 +531,14 @@ Linux users might want to use mpg123 to convert the file: mpg123 --rate 8000 --mono -w output.wav input.mp3 ``` -**Note:** If you want to hang up your call immediately after playing the audio file, you have to use the `gatherDTMF` function with `timeout:0` and `maxDigits:1`. +##### Play audio and hang up + +The `playAudioAndHangUp` method accepts an options object of type `PlayOptions` with a single field, the URL to a sound file to be played. +In addition, this also requires a `sipgateIOClient`, a unique `callId` from an current active call and a `timeout` which is optional. + +After the audio file has been played and the additional timeout has expired, the call is terminated based on the `callId`. + +**Note:** For any information about the audio file please look at [play audio](#play-audio). ##### Gather DTMF tones From 4498250fa52b1428009d674ce42b6f5556408ccb Mon Sep 17 00:00:00 2001 From: mschwarz Date: Fri, 14 Jan 2022 16:22:49 +0100 Subject: [PATCH 4/4] chore: increase library version to v2.7.0 Co-authored-by: Malte Sehmer --- lib/version.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/version.json b/lib/version.json index 5ed6603..aacbf41 100644 --- a/lib/version.json +++ b/lib/version.json @@ -1 +1 @@ -{ "version": "2.6.3" } +{ "version": "2.7.0" } diff --git a/package.json b/package.json index 0ef7231..cd77191 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sipgateio", - "version": "2.6.3", + "version": "2.7.0", "description": "The official Node.js library for sipgate.io", "main": "./dist/index.js", "browser": "./dist/browser.js",