Skip to content

Commit 945eec9

Browse files
authored
Voice Additions, DTMF + NCCO (#527)
* added DTMF mode with logic * added DTMF events subscribe and stop
1 parent f4d0994 commit 945eec9

File tree

6 files changed

+174
-2
lines changed

6 files changed

+174
-2
lines changed

src/Voice/Client.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ public function playDTMF(string $callId, string $digits): array
152152
]);
153153
}
154154

155+
public function subscribeToDtmfEventsById(string $id, array $payload): bool
156+
{
157+
$this->api->update($id . '/input/dtmf', [
158+
'eventUrl' => $payload
159+
]);
160+
161+
return true;
162+
}
163+
164+
public function unsubscribeToDtmfEventsById(string $id): bool
165+
{
166+
$this->api->delete($id . '/input/dtmf');
167+
168+
return true;
169+
}
170+
155171
/**
156172
* @throws ClientExceptionInterface
157173
* @throws \Vonage\Client\Exception\Exception

src/Voice/NCCO/Action/Input.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Vonage\Voice\NCCO\Action;
66

7+
use phpDocumentor\Reflection\Types\This;
78
use RuntimeException;
89
use Vonage\Voice\Webhook;
910

@@ -14,6 +15,13 @@
1415

1516
class Input implements ActionInterface
1617
{
18+
public const ASYNCHRONOUS_MODE = 'asynchronous';
19+
public const SYNCHRONOUS_MODE = 'synchronous';
20+
21+
public array $allowedModes = [
22+
self::SYNCHRONOUS_MODE,
23+
self::ASYNCHRONOUS_MODE,
24+
];
1725
protected ?int $dtmfTimeout = null;
1826

1927
protected ?int $dtmfMaxDigits = null;
@@ -26,6 +34,8 @@ class Input implements ActionInterface
2634

2735
protected ?string $speechLanguage = null;
2836

37+
protected ?string $mode = null;
38+
2939
/**
3040
* @var ?array<string>
3141
*/
@@ -70,6 +80,10 @@ public static function factory(array $data): Input
7080
}
7181
}
7282

83+
if (array_key_exists('mode', $data)) {
84+
$action->setMode($data['mode']);
85+
}
86+
7387
if (array_key_exists('speech', $data)) {
7488
$speech = $data['speech'];
7589
$action->setEnableSpeech(true);
@@ -136,7 +150,10 @@ public function toNCCOArray(): array
136150
'action' => 'input',
137151
];
138152

139-
if ($this->getEnableDtmf() === false && $this->getEnableSpeech() === false) {
153+
if (
154+
$this->getEnableDtmf() === false && $this->getEnableSpeech() === false && $this->getMode() !==
155+
self::ASYNCHRONOUS_MODE
156+
) {
140157
throw new RuntimeException('Input NCCO action must have either speech or DTMF enabled');
141158
}
142159

@@ -198,6 +215,10 @@ public function toNCCOArray(): array
198215
$data['eventMethod'] = $eventWebhook->getMethod();
199216
}
200217

218+
if ($this->getMode()) {
219+
$data['mode'] = $this->getMode();
220+
}
221+
201222
return $data;
202223
}
203224

@@ -365,4 +386,25 @@ public function setEnableDtmf(bool $enableDtmf): Input
365386

366387
return $this;
367388
}
389+
390+
public function getMode(): ?string
391+
{
392+
return $this->mode;
393+
}
394+
395+
public function setMode(?string $mode): self
396+
{
397+
if ($this->getEnableDtmf()) {
398+
if ($mode == self::ASYNCHRONOUS_MODE) {
399+
throw new \InvalidArgumentException('Cannot have DTMF input when using Asynchronous mode.');
400+
}
401+
}
402+
403+
if (!in_array($mode, $this->allowedModes)) {
404+
throw new \InvalidArgumentException('Mode not a valid string');
405+
}
406+
407+
$this->mode = $mode;
408+
return $this;
409+
}
368410
}

test/Voice/ClientTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,50 @@ public function testCanPlayTTSIntoCall(): void
570570
$this->assertEquals('Talk started', $response['message']);
571571
}
572572

573+
public function testCanSubscribeToDtmfEvents(): void
574+
{
575+
$id = '63f61863-4a51-4f6b-86e1-46edebcf9356';
576+
577+
$payload = [
578+
'https://example.com/events'
579+
];
580+
581+
$this->vonageClient->send(Argument::that(function (RequestInterface $request) use ($id) {
582+
$uri = $request->getUri();
583+
$uriString = $uri->__toString();
584+
$this->assertEquals(
585+
'https://api.nexmo.com/v1/calls/63f61863-4a51-4f6b-86e1-46edebcf9356/input/dtmf',
586+
$uriString
587+
);
588+
$this->assertEquals('PUT', $request->getMethod());
589+
590+
$this->assertRequestJsonBodyContains('eventUrl', ['https://example.com/events'], $request);
591+
592+
return true;
593+
}))->willReturn($this->getResponse('dtmf-subscribed'));
594+
595+
$this->voiceClient->subscribeToDtmfEventsById($id, $payload);
596+
}
597+
598+
public function testCanUnsubscribeToDtmfEvents(): void
599+
{
600+
$id = '63f61863-4a51-4f6b-86e1-46edebcf9356';
601+
602+
$this->vonageClient->send(Argument::that(function (RequestInterface $request) use ($id) {
603+
$uri = $request->getUri();
604+
$uriString = $uri->__toString();
605+
$this->assertEquals(
606+
'https://api.nexmo.com/v1/calls/63f61863-4a51-4f6b-86e1-46edebcf9356/input/dtmf',
607+
$uriString
608+
);
609+
$this->assertEquals('DELETE', $request->getMethod());
610+
611+
return true;
612+
}))->willReturn($this->getResponse('dtmf-unsubscribed'));
613+
614+
$this->voiceClient->unsubscribeToDtmfEventsById($id);
615+
}
616+
573617
/**
574618
* @throws ClientExceptionInterface
575619
* @throws Client\Exception\Exception

test/Voice/NCCO/Action/InputTest.php

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,76 @@ public function testThrowsRuntimeExceptionIfNoInputDefined(): void
128128
$this->expectException(RuntimeException::class);
129129
$this->expectExceptionMessage('Input NCCO action must have either speech or DTMF enabled');
130130

131-
(new Input())->toNCCOArray();
131+
$input = new Input();
132+
$array = $input->toNCCOArray();
133+
}
134+
135+
public function testCanCreateInputSyncNCCOCorrectly(): void
136+
{
137+
$data = [
138+
'action' => 'input',
139+
'eventUrl' => ['https://test.domain/events'],
140+
'dtmf' => [
141+
'maxDigits' => 4,
142+
],
143+
'mode' => 'synchronous'
144+
];
145+
146+
$action = Input::factory($data);
147+
$ncco = $action->toNCCOArray();
148+
149+
$this->assertSame($data['dtmf']['maxDigits'], $action->getDtmfMaxDigits());
150+
$this->assertSame($data['dtmf']['maxDigits'], $ncco['dtmf']->maxDigits);
151+
$this->assertSame($data['mode'], $ncco['mode']);
152+
$this->assertSame('POST', $action->getEventWebhook()->getMethod());
153+
$this->assertSame($data['mode'], $action->getMode());
154+
}
155+
156+
public function testCanCreateInputAsyncNCCOCorrectly(): void
157+
{
158+
$data = [
159+
'action' => 'input',
160+
'eventUrl' => ['https://test.domain/events'],
161+
'mode' => 'asynchronous'
162+
];
163+
164+
$action = Input::factory($data);
165+
$ncco = $action->toNCCOArray();
166+
167+
$this->assertSame($data['mode'], $ncco['mode']);
168+
$this->assertSame('POST', $action->getEventWebhook()->getMethod());
169+
$this->assertSame($data['mode'], $action->getMode());
170+
}
171+
172+
public function testCannotCreateInputNCCOWithDtmfAndAsyncMode(): void
173+
{
174+
$this->expectException(\InvalidArgumentException::class);
175+
176+
$data = [
177+
'action' => 'input',
178+
'eventUrl' => ['https://test.domain/events'],
179+
'dtmf' => [
180+
'maxDigits' => 4,
181+
],
182+
'mode' => 'asynchronous'
183+
];
184+
185+
$action = Input::factory($data);
186+
}
187+
188+
public function testErrorsOnInvalidInput(): void
189+
{
190+
$this->expectException(\InvalidArgumentException::class);
191+
192+
$data = [
193+
'action' => 'input',
194+
'eventUrl' => ['https://test.domain/events'],
195+
'dtmf' => [
196+
'maxDigits' => 4,
197+
],
198+
'mode' => 'syncronus'
199+
];
200+
201+
$action = Input::factory($data);
132202
}
133203
}

test/Voice/responses/dtmf-subscribed.json

Whitespace-only changes.

test/Voice/responses/dtmf-unsubscribed.json

Whitespace-only changes.

0 commit comments

Comments
 (0)