Skip to content

Commit

Permalink
feat: DTMF listener endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
SMadani committed Oct 17, 2024
1 parent e015876 commit b110232
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 20 deletions.
33 changes: 33 additions & 0 deletions src/main/java/com/vonage/client/voice/AddDtmfListenerRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2024 Vonage
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vonage.client.voice;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vonage.client.JsonableBaseObject;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;

class AddDtmfListenerRequest extends JsonableBaseObject {
@JsonIgnore final String uuid;
@JsonProperty("eventUrl") final Collection<URI> eventUrl;

public AddDtmfListenerRequest(String uuid, URI eventUrl) {
this.uuid = uuid;
this.eventUrl = Collections.singletonList(eventUrl);
}
}
56 changes: 47 additions & 9 deletions src/main/java/com/vonage/client/voice/VoiceClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.vonage.client.*;
import com.vonage.client.auth.JWTAuthMethod;
import com.vonage.client.common.HttpMethod;
import com.vonage.client.voice.ncco.InputMode;
import com.vonage.client.voice.ncco.Ncco;
import com.vonage.jwt.Jwt;
import java.io.IOException;
Expand All @@ -41,6 +42,8 @@ public class VoiceClient {
final RestEndpoint<TalkPayload, TalkResponse> startTalk;
final RestEndpoint<String, TalkResponse> stopTalk;
final RestEndpoint<DtmfPayload, DtmfResponse> sendDtmf;
final RestEndpoint<AddDtmfListenerRequest, Void> addDtmfListener;
final RestEndpoint<String, Void> removeDtmfListener;
final RestEndpoint<String, byte[]> downloadRecording;

/**
Expand All @@ -58,15 +61,10 @@ class Endpoint<T, R> extends DynamicEndpoint<T, R> {
.requestMethod(method).wrapper(wrapper).pathGetter((de, req) -> {
String base = de.getHttpWrapper().getHttpConfig().getVersionedApiBaseUri("v1");
String path = pathGetter.apply(req);
if (path.isEmpty()) {
return base + "/calls";
}
else if (path.startsWith("http") && method == HttpMethod.GET) {
if (path.startsWith("http") && method == HttpMethod.GET) {
return path;
}
else {
return base + "/calls/" + pathGetter.apply(req);
}
return base + "/calls" + (path.isEmpty() ? "" : "/" + path);
})
);
}
Expand All @@ -81,6 +79,8 @@ else if (path.startsWith("http") && method == HttpMethod.GET) {
startTalk = new Endpoint<>(req -> req.uuid + "/talk", HttpMethod.PUT);
stopTalk = new Endpoint<>(uuid -> uuid + "/talk", HttpMethod.DELETE);
sendDtmf = new Endpoint<>(req -> req.uuid + "/dtmf", HttpMethod.PUT);
addDtmfListener = new Endpoint<>(req -> req.uuid + "/input/dtmf", HttpMethod.PUT);
removeDtmfListener = new Endpoint<>(uuid -> uuid + "/input/dtmf", HttpMethod.DELETE);
downloadRecording = new Endpoint<>(Function.identity(), HttpMethod.GET);
}

Expand Down Expand Up @@ -425,8 +425,8 @@ public TalkResponse startTalk(String uuid, String text, int loop) throws VonageR
/**
* Send a synthesized speech message to an ongoing call.
*
* @param uuid The UUID of the call, obtained from the object returned by {@link #createCall(Call)}. This value
* can be obtained with {@link CallEvent#getUuid()}.
* @param uuid The UUID of the call, obtained from the object returned by {@link #createCall(Call)}.
* This value can be obtained with {@link CallEvent#getUuid()}.
* @param text The message to be spoken to the call participants.
* @param language The language to use for the text-to-speech.
* @param style The language style to use for the text-to-speech.
Expand Down Expand Up @@ -481,6 +481,44 @@ public TalkResponse stopTalk(String uuid) throws VonageResponseParseException, V
return stopTalk.execute(validateUuid(uuid));
}

/**
* Add a listener for asynchronous DTMF events sent by a caller to an
* {@linkplain com.vonage.client.voice.ncco.InputAction} NCCO action, when the
* {@linkplain com.vonage.client.voice.ncco.InputAction.Builder#mode(InputMode)} is
* {@link com.vonage.client.voice.ncco.InputMode#ASYNCHRONOUS}.
*
* @param uuid The UUID of the call, obtained from the object returned by {@link #createCall(Call)}.
* This value can be obtained with {@link CallEvent#getUuid()}.
*
* @param eventUrl The URL to send asynchronous DTMF user input events to.
*
* @throws VoiceResponseException If the call does not exist or the listener could not be added,
* for example if the call's state or input mode are incompatible.
*
* @since 8.12.0
*/
public void addDtmfListener(String uuid, String eventUrl) throws VoiceResponseException {
addDtmfListener.execute(new AddDtmfListenerRequest(validateUuid(uuid), URI.create(validateUrl(eventUrl))));
}

/**
* Remove the listener for asynchronous DTMF events sent by a caller to an
* {@linkplain com.vonage.client.voice.ncco.InputAction} NCCO, when the
* {@linkplain com.vonage.client.voice.ncco.InputAction.Builder#mode(InputMode)} is
* {@link com.vonage.client.voice.ncco.InputMode#ASYNCHRONOUS}. Calling this method
* stops sending DTMF events to the event URL set in {@link #addDtmfListener(String, String)}.
*
* @param uuid The UUID of the call, obtained from the object returned by {@link #createCall(Call)}.
* This value can be obtained with {@link CallEvent#getUuid()}.
*
* @throws VoiceResponseException If the call does not exist or have a listener attached.
*
* @since 8.12.0
*/
public void removeDtmfListener(String uuid) throws VoiceResponseException {
removeDtmfListener.execute(validateUuid(uuid));
}

/**
* Download a recording.
*
Expand Down
103 changes: 92 additions & 11 deletions src/test/java/com/vonage/client/voice/VoiceClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
import com.vonage.client.AbstractClientTest;
import com.vonage.client.RestEndpoint;
import com.vonage.client.TestUtils;
import static com.vonage.client.TestUtils.testJsonableBaseObject;
import com.vonage.client.common.HttpMethod;
import com.vonage.client.voice.ncco.Ncco;
import com.vonage.client.voice.ncco.TalkAction;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.function.Executable;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -58,13 +60,12 @@ public void testVerifySignature() {

@Test
public void testCreateCall() throws Exception {
stubResponse(200,
"{\n" + " \"conversation_uuid\": \"63f61863-4a51-4f6b-86e1-46edebio0391\",\n"
+ " \"uuid\": \"" + SAMPLE_CALL_ID + "\",\n"
+ " \"status\": \"started\",\n" + " \"direction\": \"outbound\"\n" + "}"
);
String expectedJson = "{\n" + " \"conversation_uuid\": \"63f61863-4a51-4f6b-86e1-46edebio0391\",\n"
+ " \"uuid\": \"" + SAMPLE_CALL_ID + "\",\n"
+ " \"status\": \"started\",\n" + " \"direction\": \"outbound\"\n" + "}";
stubResponse(200, expectedJson);
CallEvent evt = client.createCall(new Call("447700900903", "447700900904", "http://api.example.com/answer"));
TestUtils.testJsonableBaseObject(evt);
assertEquals(CallEvent.fromJson(expectedJson), evt);
assertEquals("63f61863-4a51-4f6b-86e1-46edebio0391", evt.getConversationUuid());
assertEquals(SAMPLE_CALL_ID, evt.getUuid());
assertEquals(CallDirection.OUTBOUND, evt.getDirection());
Expand All @@ -82,7 +83,7 @@ public void testListCallsNoFilter() throws Exception {
+ " }\n" + "}\n"
);
CallInfoPage page = client.listCalls();
TestUtils.testJsonableBaseObject(page);
testJsonableBaseObject(page);
assertEquals(0, page.getCount());
assert401Response(client::listCalls);
}
Expand Down Expand Up @@ -119,7 +120,7 @@ public void testGetCallDetails() throws Exception {
+ " }\n" + " }\n"
);
CallInfo call = client.getCallDetails("93137ee3-580e-45f7-a61a-e0b5716000ef");
TestUtils.testJsonableBaseObject(call, true);
testJsonableBaseObject(call, true);
assertEquals("93137ee3-580e-45f7-a61a-e0b5716000ef", call.getUuid());
assert401Response(() -> client.getCallDetails(SAMPLE_CALL_ID));
}
Expand All @@ -132,7 +133,7 @@ public void testSendDtmf() throws Exception {
);

DtmfResponse response = client.sendDtmf("944dd293-ca13-4a58-bc37-6252e11474be", "332393");
TestUtils.testJsonableBaseObject(response);
testJsonableBaseObject(response);
assertEquals("944dd293-ca13-4a58-bc37-6252e11474be", response.getUuid());
assertEquals("DTMF sent", response.getMessage());
assertThrows(IllegalArgumentException.class, () ->
Expand Down Expand Up @@ -244,7 +245,7 @@ public void testStopStream() throws Exception {
);

StreamResponse response = client.stopStream("944dd293-ca13-4a58-bc37-6252e11474be");
TestUtils.testJsonableBaseObject(response);
testJsonableBaseObject(response);
assertEquals("Stream stopped", response.getMessage());
assertEquals("944dd293-ca13-4a58-bc37-6252e11474be", response.getUuid());
assert401Response(() -> client.stopStream(SAMPLE_CALL_ID));
Expand All @@ -262,7 +263,7 @@ public void testStartTalkAllParamsModern() throws Exception {
.style(1).level(-0.5).loop(3).premium(false)
.language(TextToSpeechLanguage.FRENCH).build()
);
TestUtils.testJsonableBaseObject(response);
testJsonableBaseObject(response);
assertEquals("Talk started", response.getMessage());
assertEquals("944dd293-ca13-4a58-bc37-6252e11474be", response.getUuid());
assertThrows(NullPointerException.class, () ->
Expand Down Expand Up @@ -384,6 +385,24 @@ public void testStopTalk() throws Exception {
assert401Response(() -> client.stopTalk(SAMPLE_CALL_ID));
}

@Test
public void testAddDtmfListener() throws Exception {
stubResponse(200);
var url = "https://example.app/webhooks/answer";
client.addDtmfListener(SAMPLE_CALL_ID, url);
assertThrows(NullPointerException.class, () -> client.addDtmfListener(null, url));
assertThrows(IllegalArgumentException.class, () -> client.addDtmfListener(SAMPLE_CALL_ID, null));
assert401Response(() -> client.addDtmfListener(SAMPLE_CALL_ID, url));
}

@Test
public void removeDtmfListener() throws Exception {
stubResponse(204);
client.removeDtmfListener(SAMPLE_CALL_ID);
assertThrows(NullPointerException.class, () -> client.removeDtmfListener(null));
assert401Response(() -> client.removeDtmfListener(SAMPLE_CALL_ID));
}

@Test
public void testDownloadRecording() throws Exception {
String recordingId = UUID.randomUUID().toString();
Expand Down Expand Up @@ -426,6 +445,68 @@ public void testDownloadRecording() throws Exception {

// ENDPOINT TESTS

@Test
public void testAddDtmfListenerEndpoint() throws Exception {
new VoiceEndpointTestSpec<AddDtmfListenerRequest, Void>() {
private final String uuid = SAMPLE_CALL_ID;
private final String url = "https://example.org/webhooks/answer";

@Override
protected RestEndpoint<AddDtmfListenerRequest, Void> endpoint() {
return client.addDtmfListener;
}

@Override
protected HttpMethod expectedHttpMethod() {
return HttpMethod.PUT;
}

@Override
protected String expectedEndpointUri(AddDtmfListenerRequest request) {
return "/v1/calls/" + request.uuid + "/input/dtmf";
}

@Override
protected AddDtmfListenerRequest sampleRequest() {
return new AddDtmfListenerRequest(uuid, URI.create(url));
}

@Override
protected String sampleRequestBodyString() {
return "{\"eventUrl\":[\""+url+"\"]}";
}
}
.runTests();
}

@Test
public void testRemoveDtmfListenerEndpoint() throws Exception {
new VoiceEndpointTestSpec<String, Void>() {
private final String uuid = SAMPLE_CALL_ID;

@Override
protected RestEndpoint<String, Void> endpoint() {
return client.removeDtmfListener;
}

@Override
protected HttpMethod expectedHttpMethod() {
return HttpMethod.DELETE;
}

@Override
protected String expectedEndpointUri(String request) {
return "/v1/calls/" + request + "/input/dtmf";
}

@Override
protected String sampleRequest() {
return uuid;
}
}
.runTests();
}

@Test
public void testDownloadRecordingEndpoint() throws Exception {
new VoiceEndpointTestSpec<String, byte[]>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,11 @@ public void testEventMethodField() {
String expectedJson = "[{\"type\":[\"dtmf\"],\"eventMethod\":\"POST\",\"action\":\"input\"}]";
assertEquals(expectedJson, new Ncco(input).toJson());
}

@Test
public void testInputModeDeserialization() {
assertEquals(InputMode.SYNCHRONOUS, InputMode.fromString("synchronous"));
assertEquals(InputMode.ASYNCHRONOUS, InputMode.fromString("asynchronous"));
assertNull(InputMode.fromString("invalid"));
}
}

0 comments on commit b110232

Please sign in to comment.