diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4cdcfbe6f..f2c6a0d67 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = False -current_version = v8.3.0 +current_version = v8.4.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}-{release}{build} diff --git a/CHANGELOG.md b/CHANGELOG.md index dab371152..ff9065744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [8.4.0] - 2024-04-05 +- Added Conversation API implementation +- Type inference for User channels (`com.vonage.client.users.channels`) + - Added `com.vonage.client.common.ChannelType` enum + - Added `@JsonSubTypes` for improved deserialisation support + - Added `type` field to `Channel` + - Methods `setTypeField()` and `removeTypeField()` for automatically setting / unsetting based on the class +- New class `com.vonage.client.common.HalFilterRequest` + - Added `com.vonage.client.common.SortOrder` + - Used for grouping common "List*Request" fields +- Bumped Jackson version to 2.17.0 +- Bumped JWT library version to 1.1.1 + # [8.3.0] - 2024-02-12 - Made `from` parameter mandatory in Verify v2 WhatsApp workflows - Added `com.vonage.client.sms.MessageEvent` for SMS webhooks diff --git a/README.md b/README.md index 7b3d755d8..82b66efb4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ need a Vonage account. You'll need to have [created a Vonage account](https://da - [Account](https://developer.vonage.com/en/account/overview) - [Application](https://developer.vonage.com/en/application/overview) +- [Conversation](https://developer.vonage.com/conversation/overview) - [Conversion](https://developer.vonage.com/messaging/conversion-api/overview) - [Meetings](https://developer.vonage.com/en/meetings/overview) - [Messages](https://developer.vonage.com/en/messages/overview) @@ -51,9 +52,9 @@ See all of our SDKs and integrations on the [Vonage Developer portal](https://de ## Installation -Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/server-sdk/8.3.0/snippets). +Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/server-sdk/8.4.0/snippets). Instructions for your build system can be found in the snippets section. -They're also available from [here](https://mvnrepository.com/artifact/com.vonage/server-sdk/8.3.0). +They're also available from [here](https://mvnrepository.com/artifact/com.vonage/server-sdk/8.4.0). Release notes can be found in the [changelog](CHANGELOG.md). ### Build It Yourself diff --git a/build.gradle b/build.gradle index 8b5852225..2c8b7a079 100644 --- a/build.gradle +++ b/build.gradle @@ -9,30 +9,16 @@ plugins { group = "com.vonage" archivesBaseName = "server-sdk" -version = "8.3.0" +version = "8.4.0" ext.githubPath = 'Vonage/vonage-java-sdk' -tasks.withType(Javadoc).configureEach { - options.addStringOption('Xdoclint:all', '-quiet') -} - repositories { mavenCentral() } -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = sourceCompatibility -} - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = sourceCompatibility -} - dependencies { - def jacksonVersion = '2.16.1' + def jacksonVersion = '2.17.0' def httpclientVersion = '4.5.14' def junitVersion = '5.10.2' @@ -42,14 +28,29 @@ dependencies { implementation "org.apache.httpcomponents:httpmime:$httpclientVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" - implementation "com.vonage:jwt:1.1.0" + implementation "com.vonage:jwt:1.1.1" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testImplementation "org.mockito:mockito-core:5.10.0" + testImplementation "org.mockito:mockito-core:5.11.0" testImplementation "jakarta.servlet:jakarta.servlet-api:4.0.4" } +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = sourceCompatibility +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = sourceCompatibility + options.compilerArgs += '--enable-preview' +} + +tasks.withType(Javadoc).configureEach { + options.addStringOption('Xdoclint:all', '-quiet') +} + test { useJUnitPlatform() dependsOn 'cleanTest' @@ -57,6 +58,7 @@ test { events "skipped", "failed" exceptionFormat "full" } + jvmArgs "--enable-preview" } javadoc { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba771..e6441136f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5c..b82aa23a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 6689b85be..7101f8e46 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/main/java/com/vonage/client/DynamicEndpoint.java b/src/main/java/com/vonage/client/DynamicEndpoint.java index bc91cd21e..b9e406d64 100644 --- a/src/main/java/com/vonage/client/DynamicEndpoint.java +++ b/src/main/java/com/vonage/client/DynamicEndpoint.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @@ -198,7 +197,7 @@ else if (requestBody instanceof BinaryRequest) @Override public final RequestBuilder makeRequest(T requestBody) { - if (requestBody instanceof Jsonable && requestBody.getClass().equals(responseType)) { + if (requestBody instanceof Jsonable && responseType.isAssignableFrom(requestBody.getClass())) { cachedRequestBody = requestBody; } RequestBuilder rqb = createRequestBuilderFromRequestMethod(requestMethod); @@ -292,31 +291,11 @@ else if (byte[].class.equals(responseType)) { return (R) cachedRequestBody; } - for (java.lang.reflect.Method method : responseType.getDeclaredMethods()) { - boolean matching = Modifier.isStatic(method.getModifiers()) && - method.getName().equals("fromJson") && - responseType.isAssignableFrom(method.getReturnType()); - if (matching) { - Class[] params = method.getParameterTypes(); - if (params.length == 1 && params[0].equals(String.class)) { - if (!method.isAccessible()) { - method.setAccessible(true); - } - return (R) method.invoke(responseType, deser); - } - } - } - if (Jsonable.class.isAssignableFrom(responseType)) { - Constructor constructor = responseType.getDeclaredConstructor(); - if (!constructor.isAccessible()) { - constructor.setAccessible(true); - } - R responseBody = constructor.newInstance(); - ((Jsonable) responseBody).updateFromJson(deser); - return responseBody; + return (R) Jsonable.fromJson(deser, (Class) responseType); } - else if (Collection.class.isAssignableFrom(responseType)) { + else if (Collection.class.isAssignableFrom(responseType) || + (responseType.isArray() && Jsonable.class.isAssignableFrom(responseType.getComponentType()))) { return Jsonable.createDefaultObjectMapper().readValue(deser, responseType); } else { @@ -335,17 +314,9 @@ private R parseResponseFailure(HttpResponse response) throws IOException, Reflec String exMessage = EntityUtils.toString(response.getEntity()); if (responseExceptionType != null) { if (VonageApiResponseException.class.isAssignableFrom(responseExceptionType)) { - Constructor constructor = responseExceptionType.getDeclaredConstructor(); - if (!constructor.isAccessible()) { - constructor.setAccessible(true); - } - VonageApiResponseException varex = (VonageApiResponseException) constructor.newInstance(); - try { - varex.updateFromJson(exMessage); - } - catch (VonageResponseParseException ex) { - throw new VonageUnexpectedException(exMessage); - } + VonageApiResponseException varex = Jsonable.fromJson(exMessage, + (Class) responseExceptionType + ); if (varex.title == null) { varex.title = response.getStatusLine().getReasonPhrase(); } diff --git a/src/main/java/com/vonage/client/HttpWrapper.java b/src/main/java/com/vonage/client/HttpWrapper.java index 51be16af0..942adb816 100644 --- a/src/main/java/com/vonage/client/HttpWrapper.java +++ b/src/main/java/com/vonage/client/HttpWrapper.java @@ -30,7 +30,7 @@ */ public class HttpWrapper { private static final String CLIENT_NAME = "vonage-java-sdk"; - private static final String CLIENT_VERSION = "8.3.0"; + private static final String CLIENT_VERSION = "8.4.0"; private static final String JAVA_VERSION = System.getProperty("java.version"); private static final String USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION); diff --git a/src/main/java/com/vonage/client/Jsonable.java b/src/main/java/com/vonage/client/Jsonable.java index 4e40f11e0..02e6126fc 100644 --- a/src/main/java/com/vonage/client/Jsonable.java +++ b/src/main/java/com/vonage/client/Jsonable.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; /** * Indicates that a class can be serialized to and parsed from JSON. @@ -35,10 +36,9 @@ public interface Jsonable { * @return A new ObjectMapper with appropriate configuration. */ static ObjectMapper createDefaultObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - return mapper; + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); } /** @@ -63,14 +63,12 @@ default String toJson() { * @throws VonageResponseParseException If the JSON was invalid or this class couldn't be updated. */ default void updateFromJson(String json) { - if (json == null || json.trim().isEmpty()) { - return; - } + if (json == null || json.trim().isEmpty()) return; try { createDefaultObjectMapper().readerForUpdating(this).readValue(json); } catch (IOException ex) { - throw new VonageResponseParseException("Failed to produce "+getClass().getSimpleName()+" from json.", ex); + throw new VonageResponseParseException("Failed to produce "+getClass().getSimpleName()+" from JSON.", ex); } } @@ -108,6 +106,9 @@ static J fromJson(String json, J... type) { */ static J fromJson(String json, Class jsonable) { try { + if (Modifier.isAbstract(jsonable.getModifiers())) { + return createDefaultObjectMapper().readValue(json, jsonable); + } Constructor constructor = jsonable.getDeclaredConstructor(); if (!(constructor.isAccessible())) { constructor.setAccessible(true); @@ -116,8 +117,8 @@ static J fromJson(String json, Class jsonable) instance.updateFromJson(json); return instance; } - catch (ReflectiveOperationException ex) { + catch (ReflectiveOperationException | JsonProcessingException ex) { throw new VonageUnexpectedException(ex); } - } + } } diff --git a/src/main/java/com/vonage/client/VonageClient.java b/src/main/java/com/vonage/client/VonageClient.java index fe32882ad..2571c98b8 100644 --- a/src/main/java/com/vonage/client/VonageClient.java +++ b/src/main/java/com/vonage/client/VonageClient.java @@ -19,6 +19,7 @@ import com.vonage.client.application.ApplicationClient; import com.vonage.client.auth.*; import com.vonage.client.auth.hashutils.HashUtil; +import com.vonage.client.conversations.ConversationsClient; import com.vonage.client.conversion.ConversionClient; import com.vonage.client.insight.InsightClient; import com.vonage.client.meetings.MeetingsClient; @@ -67,6 +68,7 @@ public class VonageClient { private final UsersClient users; private final VideoClient video; private final NumberInsight2Client numberInsight2; + private final ConversationsClient conversations; private VonageClient(Builder builder) { httpWrapper = new HttpWrapper(builder.httpConfig, builder.authCollection); @@ -89,6 +91,7 @@ private VonageClient(Builder builder) { users = new UsersClient(httpWrapper); video = new VideoClient(httpWrapper); numberInsight2 = new NumberInsight2Client(httpWrapper); + conversations = new ConversationsClient(httpWrapper); } public AccountClient getAccountClient() { @@ -203,6 +206,16 @@ public NumberInsight2Client getNumberInsight2Client() { return numberInsight2; } + /** + * Returns the Conversations v1 client. + * + * @return The Conversation client. + * @since 8.4.0 + */ + public ConversationsClient getConversationsClient() { + return conversations; + } + /** * Generate a JWT for the application the client has been configured with. * diff --git a/src/main/java/com/vonage/client/application/ListApplicationRequest.java b/src/main/java/com/vonage/client/application/ListApplicationRequest.java index a7a196acf..d2a999941 100644 --- a/src/main/java/com/vonage/client/application/ListApplicationRequest.java +++ b/src/main/java/com/vonage/client/application/ListApplicationRequest.java @@ -15,39 +15,25 @@ */ package com.vonage.client.application; -import com.vonage.client.QueryParamsRequest; -import java.util.LinkedHashMap; -import java.util.Map; +import com.vonage.client.common.HalFilterRequest; /** * Query parameters for {@link ApplicationClient#listApplications(ListApplicationRequest)}. */ -public class ListApplicationRequest implements QueryParamsRequest { - private final int pageSize, page; +public class ListApplicationRequest extends HalFilterRequest { private ListApplicationRequest(Builder builder) { - pageSize = builder.pageSize; - page = builder.page; + super(builder); } - public int getPageSize() { - return pageSize; - } - - public int getPage() { - return page; + @Override + public Integer getPage() { + return super.getPage(); } @Override - public Map makeParams() { - LinkedHashMap params = new LinkedHashMap<>(4); - if (page > 0) { - params.put("page", String.valueOf(page)); - } - if (pageSize > 0) { - params.put("page_size", String.valueOf(pageSize)); - } - return params; + public Integer getPageSize() { + return super.getPageSize(); } /** @@ -59,8 +45,11 @@ public static Builder builder() { return new Builder(); } - public static class Builder { - private int pageSize, page; + public static class Builder extends HalFilterRequest.Builder { + + @Deprecated + public Builder() { + } /** * @param pageSize The number of applications per page. @@ -68,8 +57,7 @@ public static class Builder { * @return This builder. */ public Builder pageSize(long pageSize) { - this.pageSize = (int) pageSize; - return this; + return super.pageSize((int) pageSize); } /** @@ -78,8 +66,7 @@ public Builder pageSize(long pageSize) { * @return This builder. */ public Builder page(long page) { - this.page = (int) page; - return this; + return super.page((int) page); } /** diff --git a/src/main/java/com/vonage/client/common/ChannelType.java b/src/main/java/com/vonage/client/common/ChannelType.java new file mode 100644 index 000000000..cda910e10 --- /dev/null +++ b/src/main/java/com/vonage/client/common/ChannelType.java @@ -0,0 +1,45 @@ +/* + * 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.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the various available channel types. + * + * @since 8.4.0 + */ +public enum ChannelType { + APP, PHONE, SMS, MMS, SIP, VBC, WEBSOCKET, VIBER, MESSENGER, WHATSAPP, WHATSAPP_VOICE; + + @JsonCreator + public static ChannelType fromString(String name) { + try { + String normal = name.toUpperCase().replace('-', '_'); + return "PSTN".equals(normal) ? PHONE : valueOf(normal); + } + catch (NullPointerException | IllegalArgumentException ex) { + return null; + } + } + + @JsonValue + @Override + public String toString() { + return name().toLowerCase().replace('_', '-'); + } +} diff --git a/src/main/java/com/vonage/client/common/HalFilterRequest.java b/src/main/java/com/vonage/client/common/HalFilterRequest.java new file mode 100644 index 000000000..6a00380a4 --- /dev/null +++ b/src/main/java/com/vonage/client/common/HalFilterRequest.java @@ -0,0 +1,226 @@ +/* + * 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.common; + +import com.vonage.client.QueryParamsRequest; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Provides basic filtering parameters for HAL resources. + * + * @since 8.4.0 + */ +public abstract class HalFilterRequest implements QueryParamsRequest { + protected final String cursor; + protected final Integer page, pageSize; + protected final SortOrder order; + protected final Instant startDate, endDate; + + protected HalFilterRequest(Builder builder) { + cursor = builder.cursor; + page = validatePage(builder.page); + pageSize = validatePageSize(builder.pageSize); + order = builder.order; + startDate = builder.startDate; + endDate = builder.endDate; + } + + protected Integer validatePage(Integer page) { + if (page != null && page < 1) { + throw new IllegalArgumentException("Page must be positive."); + } + return page; + } + + protected Integer validatePageSize(Integer pageSize) { + if (pageSize != null && (pageSize < 1 || pageSize > 1000)) { + throw new IllegalArgumentException("Page size must be between 1 and 1000."); + } + return pageSize; + } + + protected HalFilterRequest(Integer page, Integer pageSize, SortOrder order) { + this.page = page; + this.pageSize = pageSize; + this.order = order; + cursor = null; + startDate = null; + endDate = null; + } + + @Override + public Map makeParams() { + Map params = new LinkedHashMap<>(); + if (cursor != null) { + params.put("cursor", cursor); + } + if (page != null) { + params.put("page", page.toString()); + } + if (pageSize != null) { + params.put("page_size", pageSize.toString()); + } + if (order != null) { + params.put("order", order.toString()); + } + return params; + } + + /** + * Page number to navigate to in the response. + * + * @return The page as an integer, or {@code null} if not specified. + */ + protected Integer getPage() { + return page; + } + + /** + * Number of results per page. + * + * @return The page size as an integer, or {@code null} if not specified. + */ + protected Integer getPageSize() { + return pageSize; + } + + /** + * Order to sort the results by. + * + * @return The result sort order as an enum, or {@code null} if not specified. + */ + protected SortOrder getOrder() { + return order; + } + + /** + * Filter records that occurred after this point in time. + * + * @return The start timestamp for results, or {@code null} if unspecified. + */ + protected Instant getStartDate() { + return startDate; + } + + /** + * Filter records that occurred before this point in time. + * + * @return The end timestamp for results, or {@code null} if unspecified. + */ + protected Instant getEndDate() { + return endDate; + } + + /** + * The cursor to start returning results from. This can be obtained from + * the URL in the relevant section from {@link HalPageResponse#getLinks()}. + * + * @return The page navigation cursor as a string, or {@code null} if unspecified. + */ + protected String getCursor() { + return cursor; + } + + @SuppressWarnings("unchecked") + protected abstract static class Builder> { + protected String cursor; + protected Integer page, pageSize; + protected SortOrder order; + protected Instant startDate, endDate; + + /** + * The cursor to start returning results from. This can be obtained from the URL in the + * relevant section from {@link HalPageResponse#getLinks()}. + * + * @param cursor The page navigation cursor as a string. + * + * @return This builder. + */ + protected B cursor(String cursor) { + this.cursor = cursor; + return (B) this; + } + + /** + * Page to navigate to in the response. + * + * @param page The page as an int. + * + * @return This builder. + */ + protected B page(int page) { + this.page = page; + return (B) this; + } + + /** + * Number of results per page. + * + * @param pageSize he page size as an int. + * + * @return This builder. + */ + protected B pageSize(int pageSize) { + this.pageSize = pageSize; + return (B) this; + } + + /** + * Order to sort the results by. + * + * @param order The results sort order as an enum. + * + * @return This builder. + */ + protected B order(SortOrder order) { + this.order = order; + return (B) this; + } + + /** + * Filter records that occurred after this point in time. + * + * @param startDate The start timestamp for results. + * + * @return This builder. + */ + protected B startDate(Instant startDate) { + this.startDate = startDate; + return (B) this; + } + + /** + * Filter records that occurred before this point in time. + * + * @param endDate The end timestamp for results. + * + * @return This builder. + */ + protected B endDate(Instant endDate) { + this.endDate = endDate; + return (B) this; + } + + /** + * Builds the filter request. + * + * @return A new FilterRequest with this builder's properties. + */ + public abstract F build(); + } +} diff --git a/src/main/java/com/vonage/client/common/HalLinks.java b/src/main/java/com/vonage/client/common/HalLinks.java index 2163aab91..eb49e111f 100644 --- a/src/main/java/com/vonage/client/common/HalLinks.java +++ b/src/main/java/com/vonage/client/common/HalLinks.java @@ -15,7 +15,9 @@ */ package com.vonage.client.common; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.JsonableBaseObject; import java.net.URI; @@ -23,6 +25,7 @@ /** * Represents the {@code _links} section of a HAL response. */ +@JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class HalLinks extends JsonableBaseObject { @JsonProperty("first") UrlContainer first; @@ -39,6 +42,7 @@ protected HalLinks() { * * @return URL of the first page, or {@code null} if absent. */ + @JsonIgnore public URI getFirstUrl() { return first != null ? first.getHref() : null; } @@ -48,6 +52,7 @@ public URI getFirstUrl() { * * @return URL of the current page, or {@code null} if absent. */ + @JsonIgnore public URI getSelfUrl() { return self != null ? self.getHref() : null; } @@ -57,6 +62,7 @@ public URI getSelfUrl() { * * @return URL of the previous page, or {@code null} if absent. */ + @JsonIgnore public URI getPrevUrl() { return prev != null ? prev.getHref() : null; } @@ -66,6 +72,7 @@ public URI getPrevUrl() { * * @return URL of the next page, or {@code null} if absent. */ + @JsonIgnore public URI getNextUrl() { return next != null ? next.getHref() : null; } @@ -75,6 +82,7 @@ public URI getNextUrl() { * * @return URL of the last page, or {@code null} if absent. */ + @JsonIgnore public URI getLastUrl() { return last != null ? last.getHref() : null; } diff --git a/src/main/java/com/vonage/client/common/HalPageResponse.java b/src/main/java/com/vonage/client/common/HalPageResponse.java index dbabf3552..3d8b6d782 100644 --- a/src/main/java/com/vonage/client/common/HalPageResponse.java +++ b/src/main/java/com/vonage/client/common/HalPageResponse.java @@ -16,6 +16,7 @@ package com.vonage.client.common; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.JsonableBaseObject; @@ -23,6 +24,7 @@ * Abstract base class for responses that conform to the * HAL specification. */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public abstract class HalPageResponse extends JsonableBaseObject { protected Integer page, pageSize, totalItems, totalPages; diff --git a/src/main/java/com/vonage/client/common/MessageType.java b/src/main/java/com/vonage/client/common/MessageType.java new file mode 100644 index 000000000..210a3b7b7 --- /dev/null +++ b/src/main/java/com/vonage/client/common/MessageType.java @@ -0,0 +1,41 @@ +/* + * 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.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents a message media type. + * + * @since 8.4.0 + */ +public enum MessageType { + TEXT, IMAGE, AUDIO, VIDEO, FILE, VCARD, TEMPLATE, CUSTOM, + LOCATION, STICKER, UNSUPPORTED, REPLY, ORDER, RANDOM; + + @JsonCreator + public static MessageType fromString(String value) { + if (value == null) return null; + return MessageType.valueOf(value.toUpperCase()); + } + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/vonage/client/common/SortOrder.java b/src/main/java/com/vonage/client/common/SortOrder.java new file mode 100644 index 000000000..cd5cfc50c --- /dev/null +++ b/src/main/java/com/vonage/client/common/SortOrder.java @@ -0,0 +1,52 @@ +/* + * 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.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the sort order for resources returned. + * + * @since 8.4.0 + */ +public enum SortOrder { + /** + * Ascending (asc) + */ + ASCENDING, + + /** + * Descending (desc) + */ + DESCENDING; + + @JsonCreator + public static SortOrder fromString(String value) { + if (value == null || value.isEmpty()) return null; + switch (value.toLowerCase()) { + case "asc": case "ascending": return ASCENDING; + case "desc": case "descending": return DESCENDING; + default: throw new IllegalArgumentException("Unknown SortOrder: "+value); + } + } + + @JsonValue + @Override + public String toString() { + return this == SortOrder.ASCENDING ? "asc" : "desc"; + } +} diff --git a/src/main/java/com/vonage/client/common/UrlContainer.java b/src/main/java/com/vonage/client/common/UrlContainer.java index d2cdfd2e5..633c0fd0e 100644 --- a/src/main/java/com/vonage/client/common/UrlContainer.java +++ b/src/main/java/com/vonage/client/common/UrlContainer.java @@ -16,14 +16,19 @@ package com.vonage.client.common; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.JsonableBaseObject; import java.net.URI; /** * Represents a link under the {@code _links} section of a HAL response. + * + * @deprecated This class will be made package-private in the next major release. */ +@JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class UrlContainer extends JsonableBaseObject { protected URI href; diff --git a/src/main/java/com/vonage/client/common/Webhook.java b/src/main/java/com/vonage/client/common/Webhook.java index f6dd25087..dbc02291e 100644 --- a/src/main/java/com/vonage/client/common/Webhook.java +++ b/src/main/java/com/vonage/client/common/Webhook.java @@ -24,7 +24,10 @@ /** * Represents the "webhooks" field used in Application capabilities. + * + * @deprecated Will be moved to the {@code com.vonage.client.application} package. */ +@Deprecated @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Webhook extends JsonableBaseObject { diff --git a/src/main/java/com/vonage/client/conversations/AbstractConversationsFilterRequest.java b/src/main/java/com/vonage/client/conversations/AbstractConversationsFilterRequest.java new file mode 100644 index 000000000..b884c7edf --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AbstractConversationsFilterRequest.java @@ -0,0 +1,94 @@ +/* + * 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.conversations; + +import com.vonage.client.common.HalFilterRequest; +import com.vonage.client.common.SortOrder; +import java.time.Instant; +import java.util.Map; + +abstract class AbstractConversationsFilterRequest extends HalFilterRequest { + String conversationId; + + protected AbstractConversationsFilterRequest(Builder< + ? extends AbstractConversationsFilterRequest, + ? extends Builder> builder) { + super(builder); + } + + @Override + protected Integer validatePageSize(Integer pageSize) { + if (pageSize != null && (pageSize < 1 || pageSize > 100)) { + throw new IllegalArgumentException("Page size must be between 1 and 100."); + } + return pageSize; + } + + protected String formatTimestamp(Instant time) { + return time.toString() + .replace('T', ' ') + .replace("Z", ""); + } + + @Override + public String getCursor() { + return super.getCursor(); + } + + @Override + public Map makeParams() { + Map params = super.makeParams(); + if (startDate != null) { + params.put("date_start", formatTimestamp(startDate)); + } + if (endDate != null) { + params.put("date_end", formatTimestamp(endDate)); + } + return params; + } + + @Override + public Integer getPageSize() { + return super.getPageSize(); + } + + @Override + public SortOrder getOrder() { + return super.getOrder(); + } + + protected abstract static class Builder< + F extends AbstractConversationsFilterRequest, B extends Builder> + extends HalFilterRequest.Builder { + + Builder() {} + + @Override + public B pageSize(int pageSize) { + return super.pageSize(pageSize); + } + + @Override + public B order(SortOrder order) { + return super.order(order); + } + + @Override + public B cursor(String cursor) { + return super.cursor(cursor); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AbstractListUserRequest.java b/src/main/java/com/vonage/client/conversations/AbstractListUserRequest.java new file mode 100644 index 000000000..89f6d95c0 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AbstractListUserRequest.java @@ -0,0 +1,24 @@ +/* + * 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.conversations; + +abstract class AbstractListUserRequest extends AbstractConversationsFilterRequest { + protected String userId; + + AbstractListUserRequest(Builder builder) { + super(builder); + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioEarmuffOffEvent.java b/src/main/java/com/vonage/client/conversations/AudioEarmuffOffEvent.java new file mode 100644 index 000000000..219144b25 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioEarmuffOffEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_EARMUFF_OFF} event. + */ +public final class AudioEarmuffOffEvent extends AudioRtcEvent { + + AudioEarmuffOffEvent() {} + + private AudioEarmuffOffEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioRtcEvent.Builder { + Builder() { + super(EventType.AUDIO_EARMUFF_OFF); + } + + @Override + public AudioEarmuffOffEvent build() { + return new AudioEarmuffOffEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioEarmuffOnEvent.java b/src/main/java/com/vonage/client/conversations/AudioEarmuffOnEvent.java new file mode 100644 index 000000000..f74903e4b --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioEarmuffOnEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_EARMUFF_ON} event. + */ +public final class AudioEarmuffOnEvent extends AudioRtcEvent { + + AudioEarmuffOnEvent() {} + + private AudioEarmuffOnEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioRtcEvent.Builder { + Builder() { + super(EventType.AUDIO_EARMUFF_ON); + } + + @Override + public AudioEarmuffOnEvent build() { + return new AudioEarmuffOnEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioMuteOffEvent.java b/src/main/java/com/vonage/client/conversations/AudioMuteOffEvent.java new file mode 100644 index 000000000..6e0be784c --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioMuteOffEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_MUTE_OFF} event. + */ +public final class AudioMuteOffEvent extends AudioRtcEvent { + + AudioMuteOffEvent() {} + + private AudioMuteOffEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioRtcEvent.Builder { + Builder() { + super(EventType.AUDIO_MUTE_OFF); + } + + @Override + public AudioMuteOffEvent build() { + return new AudioMuteOffEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioMuteOnEvent.java b/src/main/java/com/vonage/client/conversations/AudioMuteOnEvent.java new file mode 100644 index 000000000..74a6ca337 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioMuteOnEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_MUTE_ON} event. + */ +public final class AudioMuteOnEvent extends AudioRtcEvent { + + AudioMuteOnEvent() {} + + private AudioMuteOnEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioRtcEvent.Builder { + Builder() { + super(EventType.AUDIO_MUTE_ON); + } + + @Override + public AudioMuteOnEvent build() { + return new AudioMuteOnEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioOutEvent.java b/src/main/java/com/vonage/client/conversations/AudioOutEvent.java new file mode 100644 index 000000000..1aa0ffaed --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioOutEvent.java @@ -0,0 +1,133 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; + +/** + * Groups common audio controls together. + */ +abstract class AudioOutEvent extends EventWithBody { + + AudioOutEvent() {} + + AudioOutEvent(Builder, ?> builder) { + super(builder); + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Body extends JsonableBaseObject { + @JsonProperty("queue") Boolean queue; + @JsonProperty("level") Double level; + @JsonProperty("loop") Integer loop; + + Body() {} + + Body(Builder builder) { + queue = builder.queue; + if ((level = builder.level) != null && (level > 1.0 || level < -1.0)) { + throw new IllegalArgumentException("Level must be between -1 and 1."); + } + if ((loop = builder.loop) != null && loop < 0) { + throw new IllegalArgumentException("Loop cannot be negative."); + } + } + } + + /** + * Whether to queue the audio. + * + * @return {@code true} if queuing is enabled, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getQueue() { + return body != null ? body.queue : null; + } + + /** + * Audio volume level, with -1 being quietest, +1 being loudest and 0 the default. + * + * @return The volume as a Double, or {@code null} if unspecified. + */ + @JsonIgnore + public Double getLevel() { + return body != null ? body.level : null; + } + + /** + * Number of times to repeat the audio. + * + * @return The loop count as an Integer, or {@code null} if unspecified. + */ + @JsonIgnore + public Integer getLoop() { + return body != null ? body.loop : null; + } + + @SuppressWarnings("unchecked") + static abstract class Builder, B extends Builder> + extends EventWithBody.Builder { + + Builder(EventType type) { + super(type); + } + + Boolean queue; + Double level; + Integer loop; + + /** + * Whether to queue the audio. + * + * @param queue {@code true} to enable queuing, {@code false} otherwise. + * + * @return This builder. + */ + public B queue(boolean queue) { + this.queue = queue; + return (B) this; + } + + /** + * Audio volume level, with -1 being quietest, +1 being loudest and 0 the default. + * + * @param level The volume as a double. + * + * @return This builder. + */ + public B level(double level) { + this.level = level; + return (B) this; + } + + /** + * Number of times to repeat the audio. Default is 1. + * + * @param loop The loop count as an int. + * + * @return This builder. + */ + public B loop(int loop) { + this.loop = loop; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioPlayDoneEvent.java b/src/main/java/com/vonage/client/conversations/AudioPlayDoneEvent.java new file mode 100644 index 000000000..f76e34a13 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioPlayDoneEvent.java @@ -0,0 +1,25 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_PLAY_DONE} event. + */ +public final class AudioPlayDoneEvent extends AudioPlayStatusEvent { + + AudioPlayDoneEvent() {} + +} diff --git a/src/main/java/com/vonage/client/conversations/AudioPlayEvent.java b/src/main/java/com/vonage/client/conversations/AudioPlayEvent.java new file mode 100644 index 000000000..6c15ffa32 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioPlayEvent.java @@ -0,0 +1,98 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.net.URI; +import java.util.UUID; + +/** + * Represents an {@link EventType#AUDIO_PLAY} event. + */ +public final class AudioPlayEvent extends AudioOutEvent { + + AudioPlayEvent() {} + + private AudioPlayEvent(Builder builder) { + super(builder); + body = new AudioPlayEventBody(builder); + } + + /** + * Unique audio play identifier. + * + * @return The play ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getPlayId() { + return body != null ? body.playId : null; + } + + /** + * Source URL of the audio to play. + * + * @return The stream URL, or {@code null} if unspecified. + */ + @JsonIgnore + public URI getStreamUrl() { + return body != null && body.streamUrl != null && body.streamUrl.length > 0 ? body.streamUrl[0] : null; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioOutEvent.Builder { + URI[] streamUrl; + + Builder() { + super(EventType.AUDIO_PLAY); + } + + /** + * Source URL of the audio to play. + * + * @param streamUrl The stream URL as a string. + * + * @return This builder. + */ + public Builder streamUrl(String streamUrl) { + return streamUrl(URI.create(streamUrl)); + } + + /** + * Source URL of the audio to play. + * + * @param streamUrl The stream URL. + * + * @return This builder. + */ + public Builder streamUrl(URI streamUrl) { + this.streamUrl = new URI[]{streamUrl}; + return this; + } + + @Override + public AudioPlayEvent build() { + return new AudioPlayEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioPlayEventBody.java b/src/main/java/com/vonage/client/conversations/AudioPlayEventBody.java new file mode 100644 index 000000000..7657ac97e --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioPlayEventBody.java @@ -0,0 +1,42 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.net.URI; +import java.util.Objects; +import java.util.UUID; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +final class AudioPlayEventBody extends AudioOutEvent.Body { + @JsonProperty("play_id") UUID playId; + @JsonProperty("stream_url") URI[] streamUrl; + + AudioPlayEventBody() { + } + + AudioPlayEventBody(UUID playId) { + this.playId = playId; + } + + AudioPlayEventBody(AudioPlayEvent.Builder builder) { + super(builder); + streamUrl = Objects.requireNonNull(builder.streamUrl, "Stream URL is required."); + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioPlayStatusEvent.java b/src/main/java/com/vonage/client/conversations/AudioPlayStatusEvent.java new file mode 100644 index 000000000..a503f108a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioPlayStatusEvent.java @@ -0,0 +1,75 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.UUID; + +abstract class AudioPlayStatusEvent extends EventWithBody { + + AudioPlayStatusEvent() {} + + AudioPlayStatusEvent(Builder builder) { + super(builder); + body = new AudioPlayEventBody(builder.playId); + } + + /** + * Unique audio play identifier. + * + * @return The play ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getPlayId() { + return body != null ? body.playId : null; + } + + + @SuppressWarnings("unchecked") + static abstract class Builder> + extends EventWithBody.Builder { + + UUID playId; + + Builder(EventType type) { + super(type); + } + + /** + * Unique audio play identifier. + * + * @param playId Unique audio play identifier as a string. + * + * @return This builder. + */ + public B playId(String playId) { + return playId(UUID.fromString(playId)); + } + + /** + * Unique audio play identifier. + * + * @param playId Unique audio play identifier. + * + * @return This builder. + */ + public B playId(UUID playId) { + this.playId = playId; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioPlayStopEvent.java b/src/main/java/com/vonage/client/conversations/AudioPlayStopEvent.java new file mode 100644 index 000000000..52542c47d --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioPlayStopEvent.java @@ -0,0 +1,49 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_PLAY_STOP} event. + */ +public final class AudioPlayStopEvent extends AudioPlayStatusEvent { + + AudioPlayStopEvent() {} + + private AudioPlayStopEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioPlayStatusEvent.Builder { + + Builder() { + super(EventType.AUDIO_PLAY_STOP); + } + + @Override + public AudioPlayStopEvent build() { + return new AudioPlayStopEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioRecordEvent.java b/src/main/java/com/vonage/client/conversations/AudioRecordEvent.java new file mode 100644 index 000000000..acbce7fdb --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioRecordEvent.java @@ -0,0 +1,303 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.vonage.client.voice.TextToSpeechLanguage; + +/** + * Represents an {@link EventType#AUDIO_RECORD} event. + */ +public final class AudioRecordEvent extends EventWithBody { + + AudioRecordEvent() {} + + private AudioRecordEvent(Builder builder) { + super(builder); + body = new AudioRecordEventBody(builder); + } + + /** + * Text-to-speech transcription language. + * + * @return The transcription language as an enum, or {@code null} if unspecified. + */ + @JsonIgnore + public TextToSpeechLanguage getLanguage() { + return body.transcription != null ? body.transcription.language : null; + } + + /** + * Whether sentiment analysis is enabled. + * + * @return {@code true} if sentiment analysis is enabled, + * or {@code null} if transcription is not enabled. + */ + @JsonIgnore + public Boolean getSentimentAnalysis() { + return body.transcription != null ? body.transcription.sentimentAnalysis : null; + } + + /** + * File format for the recording. + * + * @return The recording format as a string, or {@code null} if unspecified. + */ + @JsonIgnore + public String getFormat() { + return body.format; + } + + /** + * The validity parameter. + * + * @return The validity as an integer, or {@code null} if unspecified. + */ + @JsonIgnore + public Integer getValidity() { + return body.validity; + } + + /** + * Number of channels for the recording. + * + * @return The number of channels as an Integer, or {@code null} if unspecified. + */ + @JsonIgnore + public Integer getChannels() { + return body.channels; + } + + /** + * Whether the audio recording is streamed. + * + * @return {@code true} if the recording is streamed, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getStreamed() { + return body.streamed; + } + + /** + * Whether the audio is split. + * + * @return {@code true} if the recording is split, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getSplit() { + return body.split; + } + + /** + * Whether the audio has multiple tracks. + * + * @return {@code true} if the recording is multi-track, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getMultitrack() { + return body.multitrack; + } + + /** + * Whether to detect speech in the recording. + * + * @return {@code true} if speech detection is enabled, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getDetectSpeech() { + return body.detectSpeech; + } + + /** + * Whether to enable beep start. + * + * @return {@code true} if beep start, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getBeepStart() { + return body.beepStart; + } + + /** + * Whether to enable beep stop. + * + * @return {@code true} if beep stop, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getBeepStop() { + return body.beepStop; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for setting the Audio Record event parameters. + */ + public static final class Builder extends EventWithBody.Builder { + String format; + Integer validity, channels; + Boolean streamed, split, multitrack, detectSpeech, beepStart, beepStop, sentimentAnalysis; + TextToSpeechLanguage language; + + Builder() { + super(EventType.AUDIO_RECORD); + } + + /** + * File format for the recording. + * + * @param format The format as a string. + * + * @return This builder. + */ + public Builder format(String format) { + this.format = format; + return this; + } + + /** + * Audio recording validity parameter. + * + * @param validity The validity as an int. + * + * @return This builder. + */ + public Builder validity(int validity) { + this.validity = validity; + return this; + } + + /** + * Number of channels for the recording. + * + * @param channels The number of channels as an int. + * + * @return This builder. + */ + public Builder channels(int channels) { + this.channels = channels; + return this; + } + + /** + * Whether the audio recording is streamed. + * + * @param streamed {@code true} if the recording should be streamed. + * + * @return This builder. + */ + public Builder streamed(boolean streamed) { + this.streamed = streamed; + return this; + } + + /** + * Whether the recording should be split. + * + * @param split {@code true} if the audio should be split. + * + * @return This builder. + */ + public Builder split(boolean split) { + this.split = split; + return this; + } + + /** + * Whether the audio should have multiple tracks. + * + * @param multitrack {@code true} if the recording should be multi-track. + * + * @return This builder. + */ + public Builder multitrack(boolean multitrack) { + this.multitrack = multitrack; + return this; + } + + /** + * Whether speech detection is enabled. + * + * @param detectSpeech {@code true} to enable speech detection. + * + * @return This builder. + */ + public Builder detectSpeech(boolean detectSpeech) { + this.detectSpeech = detectSpeech; + return this; + } + + /** + * Whether to set the {@code beep_start} flag. + * + * @param beepStart {@code true} to enable beep start. + * + * @return This builder. + */ + public Builder beepStart(boolean beepStart) { + this.beepStart = beepStart; + return this; + } + + /** + * Whether to set the {@code beep_stop} flag. + * + * @param beepStop {@code true} to enable beep stop. + * + * @return This builder. + */ + public Builder beepStop(boolean beepStop) { + this.beepStop = beepStop; + return this; + } + + /** + * Whether to enable sentiment analysis in the recording transcription. + * + * @param sentimentAnalysis {@code true} to enable transcription sentiment analysis. + * + * @return This builder. + */ + public Builder sentimentAnalysis(boolean sentimentAnalysis) { + this.sentimentAnalysis = sentimentAnalysis; + return this; + } + + /** + * Text-to-speech transcription language. Setting this will enable recording transcription. + * + * @param language The transcription language as an enum. + * + * @return This builder. + */ + public Builder language(TextToSpeechLanguage language) { + this.language = language; + return this; + } + + @Override + public AudioRecordEvent build() { + return new AudioRecordEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioRecordEventBody.java b/src/main/java/com/vonage/client/conversations/AudioRecordEventBody.java new file mode 100644 index 000000000..38e11892b --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioRecordEventBody.java @@ -0,0 +1,63 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.voice.TextToSpeechLanguage; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +class AudioRecordEventBody extends JsonableBaseObject { + @JsonProperty("transcription") Transcription transcription; + @JsonProperty("format") String format; + @JsonProperty("validity") Integer validity; + @JsonProperty("channels") Integer channels; + @JsonProperty("streamed") Boolean streamed; + @JsonProperty("split") Boolean split; + @JsonProperty("multitrack") Boolean multitrack; + @JsonProperty("detect_speech") Boolean detectSpeech; + @JsonProperty("beep_start") Boolean beepStart; + @JsonProperty("beep_stop") Boolean beepStop; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static class Transcription extends JsonableBaseObject { + @JsonProperty("language") TextToSpeechLanguage language; + @JsonProperty("sentiment_analysis") Boolean sentimentAnalysis; + } + + AudioRecordEventBody() {} + + AudioRecordEventBody(AudioRecordEvent.Builder builder) { + if (builder.language != null || builder.sentimentAnalysis != null) { + transcription = new Transcription(); + transcription.language = builder.language; + transcription.sentimentAnalysis = builder.sentimentAnalysis; + } + format = builder.format; + validity = builder.validity; + channels = builder.channels; + streamed = builder.streamed; + split = builder.split; + multitrack = builder.multitrack; + detectSpeech = builder.detectSpeech; + beepStart = builder.beepStart; + beepStop = builder.beepStop; + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioRecordStopEvent.java b/src/main/java/com/vonage/client/conversations/AudioRecordStopEvent.java new file mode 100644 index 000000000..752929a9d --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioRecordStopEvent.java @@ -0,0 +1,97 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.util.UUID; + +/** + * Represents an {@link EventType#AUDIO_RECORD_STOP} event. + */ +public final class AudioRecordStopEvent extends EventWithBody { + + private AudioRecordStopEvent() {} + + private AudioRecordStopEvent(Builder builder) { + super(builder); + (body = new Body()).recordId = builder.recordId; + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static class Body extends JsonableBaseObject { + @JsonProperty("record_id") private UUID recordId; + } + + /** + * Unique recording identifier. + * + * @return The recording ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getRecordId() { + return body != null ? body.recordId : null; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends EventWithBody.Builder { + private UUID recordId; + + Builder() { + super(EventType.AUDIO_RECORD_STOP); + } + + /** + * Unique recording identifier. + * + * @param recordId The recording ID as a string. + * + * @return This builder. + */ + public Builder recordId(String recordId) { + return recordId(UUID.fromString(recordId)); + } + + /** + * Unique recording identifier. + * + * @param recordId The recording ID. + * + * @return This builder. + */ + public Builder recordId(UUID recordId) { + this.recordId = recordId; + return this; + } + + @Override + public AudioRecordStopEvent build() { + return new AudioRecordStopEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioRtcEvent.java b/src/main/java/com/vonage/client/conversations/AudioRtcEvent.java new file mode 100644 index 000000000..56062107c --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioRtcEvent.java @@ -0,0 +1,87 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.util.UUID; + +abstract class AudioRtcEvent extends EventWithBody { + + AudioRtcEvent() {} + + AudioRtcEvent(Builder builder) { + super(builder); + (body = new Body()).rtcId = builder.rtcId; + } + + /** + * Main body object for Audio events with {@code rtc_id}. + */ + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static class Body extends JsonableBaseObject { + @JsonProperty("rtc_id") private UUID rtcId; + } + + /** + * Unique audio RTC identifier. + * + * @return The RTC ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getRtcId() { + return body != null ? body.rtcId : null; + } + + @SuppressWarnings("unchecked") + static abstract class Builder> + extends EventWithBody.Builder> { + + private UUID rtcId; + + Builder(EventType type) { + super(type); + } + + /** + * Unique audio RTC identifier. + * + * @param rtcId The RTC ID as a string. + * + * @return This builder. + */ + public B rtcId(String rtcId) { + return rtcId(UUID.fromString(rtcId)); + } + + /** + * Unique audio RTC identifier. + * + * @param rtcId The RTC ID. + * + * @return This builder. + */ + public B rtcId(UUID rtcId) { + this.rtcId = rtcId; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioSayDoneEvent.java b/src/main/java/com/vonage/client/conversations/AudioSayDoneEvent.java new file mode 100644 index 000000000..e114c78c8 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioSayDoneEvent.java @@ -0,0 +1,25 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_SAY_DONE} event. + */ +public final class AudioSayDoneEvent extends AudioSayStatusEvent { + + AudioSayDoneEvent() {} + +} diff --git a/src/main/java/com/vonage/client/conversations/AudioSayEvent.java b/src/main/java/com/vonage/client/conversations/AudioSayEvent.java new file mode 100644 index 000000000..19abae1ab --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioSayEvent.java @@ -0,0 +1,177 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.vonage.client.voice.TextToSpeechLanguage; +import java.util.UUID; + +/** + * Represents an {@link EventType#AUDIO_SAY} event. + */ +public final class AudioSayEvent extends AudioOutEvent { + + AudioSayEvent() {} + + private AudioSayEvent(Builder builder) { + super(builder); + body = new AudioSayEventBody(builder); + } + + /** + * Unique audio say identifier. + * + * @return The say ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getSayId() { + return body != null ? body.sayId : null; + } + + /** + * Text to be spoken. + * + * @return The speech text. + */ + @JsonIgnore + public String getText() { + return body.text; + } + + /** + * Text-to-speech voice style. See the + * + * Voice API documentation for valid options. + * + * @return The TTS style as an Integer, or {@code null} if unspecified. + */ + @JsonIgnore + public Integer getStyle() { + return body.style; + } + + /** + * Language for the spoken text. + * + * @return The TTS language as an enum, or {@code null} if unspecified. + */ + @JsonIgnore + public TextToSpeechLanguage getLanguage() { + return body.language; + } + + /** + * Whether to use the premium version of the text-to-speech voice. + * + * @return {@code true} to use Premium TTS, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getPremium() { + return body.premium; + } + + /** + * Whether to enable Synthesized Speech Markup Language (SSML). + * + * @return {@code true} to use SSML, or {@code null} if unspecified. + */ + @JsonIgnore + public Boolean getSsml() { + return body.ssml; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioOutEvent.Builder { + String text; + Integer style; + TextToSpeechLanguage language; + Boolean premium, ssml; + + Builder() { + super(EventType.AUDIO_SAY); + } + + /** + * (REQUIRED) Text to be spoken. + * + * @param text The speech text. + * @return This builder. + */ + public Builder text(String text) { + this.text = text; + return this; + } + + /** + * Text-to-speech voice style. See the + * + * Voice API documentation for valid options. + * + * @param style The TTS style as an int. + * @return This builder. + */ + public Builder style(int style) { + this.style = style; + return this; + } + + /** + * Language for the spoken text. + * + * @param language The TTS language as an enum. + * @return This builder. + */ + public Builder language(TextToSpeechLanguage language) { + this.language = language; + return this; + } + + /** + * Whether to use the premium version of the text-to-speech voice. + * + * @param premium {@code true} to use Premium TTS. + * @return This builder. + */ + public Builder premium(boolean premium) { + this.premium = premium; + return this; + } + + /** + * Whether to enable Synthesized Speech Markup Language (SSML). + * + * @param ssml {@code true} to use SSML. + * @return This builder. + */ + public Builder ssml(boolean ssml) { + this.ssml = ssml; + return this; + } + + @Override + public AudioSayEvent build() { + return new AudioSayEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioSayEventBody.java b/src/main/java/com/vonage/client/conversations/AudioSayEventBody.java new file mode 100644 index 000000000..c0168ddbd --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioSayEventBody.java @@ -0,0 +1,51 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.voice.TextToSpeechLanguage; +import java.util.UUID; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +class AudioSayEventBody extends AudioOutEvent.Body { + @JsonProperty("say_id") UUID sayId; + @JsonProperty("text") String text; + @JsonProperty("style") Integer style; + @JsonProperty("language") TextToSpeechLanguage language; + @JsonProperty("premium") Boolean premium; + @JsonProperty("ssml") Boolean ssml; + + AudioSayEventBody() { + } + + AudioSayEventBody(UUID sayId) { + this.sayId = sayId; + } + + AudioSayEventBody(AudioSayEvent.Builder builder) { + super(builder); + if ((text = builder.text) == null || text.trim().isEmpty()) { + throw new IllegalArgumentException("Speech text is required and cannot be empty."); + } + style = builder.style; + language = builder.language; + premium = builder.premium; + ssml = builder.ssml; + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioSayStatusEvent.java b/src/main/java/com/vonage/client/conversations/AudioSayStatusEvent.java new file mode 100644 index 000000000..5c5709ca6 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioSayStatusEvent.java @@ -0,0 +1,75 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.UUID; + +abstract class AudioSayStatusEvent extends EventWithBody { + + AudioSayStatusEvent() {} + + AudioSayStatusEvent(Builder builder) { + super(builder); + body = new AudioSayEventBody(builder.sayId); + } + + /** + * Unique audio say identifier. + * + * @return The say ID, or {@code null} if unknown. + */ + @JsonIgnore + public UUID getSayId() { + return body != null ? body.sayId : null; + } + + + @SuppressWarnings("unchecked") + static abstract class Builder> + extends EventWithBody.Builder { + + UUID sayId; + + Builder(EventType type) { + super(type); + } + + /** + * Unique audio say identifier. + * + * @param sayId The say ID as a string. + * + * @return This builder. + */ + public B sayId(String sayId) { + return sayId(UUID.fromString(sayId)); + } + + /** + * Unique audio say identifier. + * + * @param sayId The say ID + * + * @return This builder. + */ + public B sayId(UUID sayId) { + this.sayId = sayId; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/AudioSayStopEvent.java b/src/main/java/com/vonage/client/conversations/AudioSayStopEvent.java new file mode 100644 index 000000000..9ecd17f29 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/AudioSayStopEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an {@link EventType#AUDIO_SAY_STOP} event. + */ +public final class AudioSayStopEvent extends AudioSayStatusEvent { + + AudioSayStopEvent() {} + + private AudioSayStopEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AudioSayStatusEvent.Builder { + Builder() { + super(EventType.AUDIO_SAY_STOP); + } + + @Override + public AudioSayStopEvent build() { + return new AudioSayStopEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/BaseConversation.java b/src/main/java/com/vonage/client/conversations/BaseConversation.java new file mode 100644 index 000000000..4413ad0f5 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/BaseConversation.java @@ -0,0 +1,86 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.net.URI; + +/** + * Represents the main attributes of a conversation. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class BaseConversation extends JsonableBaseObject { + String id, name, displayName; + URI imageUrl; + ConversationTimestamp timestamp; + + protected BaseConversation() { + } + + /** + * Unique identifier for this conversation. + * + * @return The conversation ID as a string. + */ + @JsonProperty("id") + public String getId() { + return id; + } + + /** + * Internal conversation name. Must be unique. + * + * @return The conversation name, or {@code null} if unespecified. + */ + @JsonProperty("name") + public String getName() { + return name; + } + + /** + * The public facing name of the conversation. + * + * @return The display name, or {@code null} if unespecified. + */ + @JsonProperty("display_name") + public String getDisplayName() { + return displayName; + } + + /** + * An image URL that you associate with the conversation. + * + * @return The image URL, or {@code null} if unspecified. + */ + @JsonProperty("image_url") + public URI getImageUrl() { + return imageUrl; + } + + /** + * Timestamps for this conversation. + * + * @return The timestamps object, or {@code null} if unknown. + */ + @JsonProperty("timestamp") + public ConversationTimestamp getTimestamp() { + return timestamp; + } +} diff --git a/src/main/java/com/vonage/client/conversations/BaseMember.java b/src/main/java/com/vonage/client/conversations/BaseMember.java new file mode 100644 index 000000000..53cb0e51b --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/BaseMember.java @@ -0,0 +1,72 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.users.BaseUser; + +/** + * Represents the basic conversation member attributes, as returned from {@link ListMembersResponse#getMembers()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class BaseMember extends JsonableBaseObject { + @JsonProperty("id") String id; + @JsonProperty("state") MemberState state; + @JsonProperty("_embedded") Embedded _embedded; + @JsonProperty("user") BaseUser user; + + protected BaseMember() {} + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static final class Embedded extends JsonableBaseObject { + @JsonProperty("user") private BaseUser user; + } + + /** + * Unique member identifier. + * + * @return The member ID, or {@code null} if unknown. + */ + public String getId() { + return id; + } + + /** + * State that the member is in. + * + * @return The member state as an enum. + */ + public MemberState getState() { + return state; + } + + /** + * User associated with this member. + * Full details can be obtained via {@link com.vonage.client.users.UsersClient#getUserDetails(BaseUser)}. + * + * @return The basic user details, or {@code null} if unknown. + */ + @JsonIgnore + public BaseUser getUser() { + return _embedded != null ? _embedded.user : user; + } +} diff --git a/src/main/java/com/vonage/client/conversations/Callback.java b/src/main/java/com/vonage/client/conversations/Callback.java new file mode 100644 index 000000000..70e8b1d4b --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/Callback.java @@ -0,0 +1,211 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.HttpMethod; +import java.net.URI; +import java.util.UUID; + +/** + * Callback properties for a {@link Conversation}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class Callback extends JsonableBaseObject { + private URI url; + private String eventMask; + private Params params; + private HttpMethod method; + + Callback() {} + + Callback(Builder builder) { + url = builder.url; + if ((eventMask = builder.eventMask) != null && (eventMask.length() > 200 || eventMask.trim().isEmpty())) { + throw new IllegalArgumentException("Event mask must be between 1 and 200 characters"); + } + if ((method = builder.method) != null && !(method == HttpMethod.POST || method == HttpMethod.GET)) { + throw new IllegalArgumentException("Callback HTTP method must be either POST or GET, not "+method); + } + params = builder.params; + } + + /** + * Event URL for the callback. + * + * @return The callback URL, or {@code null} if unspecified. + */ + @JsonProperty("url") + public URI getUrl() { + return url; + } + + @JsonProperty("event_mask") + public String getEventMask() { + return eventMask; + } + + /** + * Additional parameters. + * + * @return The callback parameters, or {@code null} if unspecified. + */ + @JsonProperty("params") + public Params getParams() { + return params; + } + + /** + * Method to use for the callback, either {@linkplain HttpMethod#GET} or {@linkplain HttpMethod#POST}. + * + * @return The HTTP method as an enum, or {@code null} if unspecified. + */ + @JsonProperty("method") + public HttpMethod getMethod() { + return method; + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Params extends JsonableBaseObject { + private UUID applicationId; + private URI nccoUrl; + + protected Params() {} + + /** + * Vonage Application ID. + * + * @return The application ID, or {@code null} if unspecified. + */ + @JsonProperty("applicationId") + public UUID getApplicationId() { + return applicationId; + } + + /** + * Call Control Object URL to use for the callback. + * + * @return The NCCO URL, or {@code null} if unspecified. + */ + @JsonProperty("ncco_url") + public URI getNccoUrl() { + return nccoUrl; + } + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for creating Callback settings. All parameters are optional. + */ + public static final class Builder { + private URI url; + private String eventMask; + private HttpMethod method; + private Params params; + + private Builder() {} + + private Params initParams() { + if (params == null) { + params = new Params(); + } + return params; + } + + /** + * Event URL for the callback. + * + * @param url The callback URL as a string. + * + * @return This builder. + */ + public Builder url(String url) { + this.url = URI.create(url); + return this; + } + + /** + * Callback event mask. + * + * @param eventMask The event mask as a string. + * + * @return This builder. + */ + public Builder eventMask(String eventMask) { + this.eventMask = eventMask; + return this; + } + + /** + * HTTP method to use for the callback. + * Must be either {@linkplain HttpMethod#GET} or {@linkplain HttpMethod#POST}. + * + * @param method The HTTP method as an enum, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder method(HttpMethod method) { + this.method = method; + return this; + } + + /** + * Vonage Application ID. + * + * @param applicationId The application ID as a string. + * + * @return This builder. + */ + public Builder applicationId(String applicationId) { + initParams().applicationId = UUID.fromString(applicationId); + return this; + } + + /** + * Call Control Object URL to use for the callback. + * + * @param nccoUrl The NCCO URL as a string. + * + * @return This builder. + */ + public Builder nccoUrl(String nccoUrl) { + initParams().nccoUrl = URI.create(nccoUrl); + return this; + } + + /** + * Builds the {@linkplain Callback}. + * + * @return An instance of Callback, populated with all fields from this builder. + */ + public Callback build() { + return new Callback(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/Conversation.java b/src/main/java/com/vonage/client/conversations/Conversation.java new file mode 100644 index 000000000..8ebcb8e10 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/Conversation.java @@ -0,0 +1,233 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.users.channels.Channel; +import com.vonage.client.users.channels.Pstn; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +/** + * Represents a Conversation (request and response). + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Conversation extends BaseConversation { + private ConversationStatus state; + private Integer sequenceNumber; + private ConversationProperties properties; + private Collection numbers; + private Callback callback; + + protected Conversation() { + } + + Conversation(Builder builder) { + if ((name = builder.name) != null && (name.length() > 100 || name.trim().isEmpty())) { + throw new IllegalArgumentException("Name must be between 1 and 100 characters."); + } + if ((displayName = builder.displayName) != null && (displayName.length() > 50 || displayName.trim().isEmpty())) { + throw new IllegalArgumentException("Display name must be between 1 and 50 characters."); + } + imageUrl = builder.imageUrl; + properties = builder.properties; + callback = builder.callback; + if ((numbers = builder.numbers) != null) { + numbers.forEach(Channel::setTypeField); + } + } + + /** + * The state the conversation is in. + * + * @return The conversation state as an enum, or {@code null} if unknown. + */ + @JsonProperty("state") + public ConversationStatus getState() { + return state; + } + + /** + * The last Event ID in this conversation. This ID can be used to retrieve a specific event. + * + * @return The last event ID as an integer, or {@code null} if unknown. + */ + @JsonProperty("sequence_number") + public Integer getSequenceNumber() { + return sequenceNumber; + } + + /** + * Properties for this conversation. + * + * @return The conversation properties object, or {@code null} if not applicable. + */ + @JsonProperty("properties") + public ConversationProperties getProperties() { + return properties; + } + + /** + * Channels containing the contact numbers for this conversation. + * Currently, only {@link Pstn} (Phone) type is supported. + * + * @return The channels associated with this conversation, or {@code null} if unspecified. + */ + @JsonProperty("numbers") + public Collection getNumbers() { + return numbers; + } + + /** + * Specifies callback parameters for webhooks. + * + * @return The callback properties, or {@code null} if unspecified. + */ + @JsonProperty("callback") + public Callback getCallback() { + return callback; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for creating or updating a Conversation. All fields are optional. + */ + public static class Builder { + private String name, displayName; + private URI imageUrl; + private ConversationProperties properties; + private Collection numbers; + private Callback callback; + + Builder() {} + + /** + * Internal conversation name. Must be unique. + * + * @param name The conversation name. + * + * @return This builder. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * The public facing name of the conversation. + * + * @param displayName The display name. + * + * @return This builder. + */ + public Builder displayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * An image URL that you associate with the conversation. + * + * @param imageUrl The image URL as a string. + * + * @return This builder. + */ + public Builder imageUrl(String imageUrl) { + this.imageUrl = URI.create(imageUrl); + return this; + } + + /** + * Properties for this conversation. + * + * @param properties The conversation properties object. + * + * @return This builder. + */ + public Builder properties(ConversationProperties properties) { + this.properties = properties; + return this; + } + + /** + * Sets the PSTN numbers for this conversation. + * + * @param phoneNumber The telephone or mobile number(s) for this conversation in E.164 format. + * + * @return This builder. + */ + public Builder phone(String... phoneNumber) { + return numbers(Arrays.stream(phoneNumber).map(Pstn::new).toArray(Channel[]::new)); + } + + /** + * Channels containing the contact numbers for this conversation. + * + * @param numbers The channels associated with this conversation. + * + * @return This builder. + */ + Builder numbers(Channel... numbers) { + return numbers(Arrays.asList(numbers)); + } + + /** + * Channels containing the contact numbers for this conversation. + * + * @param numbers The channels associated with this conversation. + * + * @return This builder. + */ + Builder numbers(Collection numbers) { + this.numbers = new ArrayList<>(numbers); + return this; + } + + /** + * Specifies callback parameters for webhooks. + * + * @param callback The callback properties, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder callback(Callback callback) { + this.callback = callback; + return this; + } + + /** + * Builds the {@linkplain Conversation}. + * + * @return An instance of Conversation, populated with all fields from this builder. + */ + public Conversation build() { + return new Conversation(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationProperties.java b/src/main/java/com/vonage/client/conversations/ConversationProperties.java new file mode 100644 index 000000000..5ae26a02a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationProperties.java @@ -0,0 +1,161 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.util.Map; + +/** + * Additional properties for a {@linkplain Conversation}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ConversationProperties extends JsonableBaseObject { + private Integer ttl; + private String type, customSortKey; + private Map customData; + + ConversationProperties() {} + + ConversationProperties(Builder builder) { + ttl = builder.ttl; + if ((type = builder.type) != null && (type.length() > 200 || type.trim().isEmpty())) { + throw new IllegalArgumentException("Type must be between 1 and 200 characters."); + } + if ((customSortKey = builder.customSortKey) != null && (customSortKey.length() > 200 || customSortKey.trim().isEmpty())) { + throw new IllegalArgumentException("Custom sort key must be between 1 and 200 characters."); + } + customData = builder.customData; + } + + /** + * Number of seconds after which an empty conversation is deleted. + * + * @return The empty time-to-live in seconds, or {@code null} if unspecified. + */ + @JsonProperty("ttl") + public Integer getTtl() { + return ttl; + } + + /** + * Conversation type. + * + * @return The conversation type as a string, or {@code null} if unknown. + */ + @JsonProperty("type") + public String getType() { + return type; + } + + /** + * Custom sort key. + * + * @return The custom sort key as a string, or {@code null} if unspecified. + */ + @JsonProperty("custom_sort_key") + public String getCustomSortKey() { + return customSortKey; + } + + /** + * Custom key-value pairs to be included with conversation data. + * + * @return The custom properties as a Map, or {@code null} if unspecified. + */ + @JsonProperty("custom_data") + public Map getCustomData() { + return customData; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Integer ttl; + private String type, customSortKey; + private Map customData; + + Builder() {} + + /** + * Number of seconds after which an empty conversation is deleted. + * + * @param ttl The empty time-to-live in seconds, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder ttl(Integer ttl) { + this.ttl = ttl; + return this; + } + + /** + * Conversation type. + * + * @param type The conversation type as a string, or {@code null} if unknown. + * + * @return This builder. + */ + public Builder type(String type) { + this.type = type; + return this; + } + + /** + * Custom sort key. + * + * @param customSortKey The custom sort key as a string, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder customSortKey(String customSortKey) { + this.customSortKey = customSortKey; + return this; + } + + /** + * Custom key-value pairs to be included with conversation data. + * + * @param customData The custom properties as a Map, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder customData(Map customData) { + this.customData = customData; + return this; + } + + + /** + * Builds the {@linkplain ConversationProperties}. + * + * @return An instance of ConversationProperties, populated with all fields from this builder. + */ + public ConversationProperties build() { + return new ConversationProperties(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationResourceRequestWrapper.java b/src/main/java/com/vonage/client/conversations/ConversationResourceRequestWrapper.java new file mode 100644 index 000000000..0f6a56f6f --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationResourceRequestWrapper.java @@ -0,0 +1,25 @@ +/* + * 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.conversations; + +class ConversationResourceRequestWrapper { + final String conversationId, resourceId; + + ConversationResourceRequestWrapper(String conversationId, String resourceId) { + this.conversationId = conversationId; + this.resourceId = resourceId; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationStatus.java b/src/main/java/com/vonage/client/conversations/ConversationStatus.java new file mode 100644 index 000000000..6b7b59db1 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationStatus.java @@ -0,0 +1,44 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the status of a {@linkplain Conversation}. + */ +public enum ConversationStatus { + ACTIVE, + INACTIVE, + DELETED; + + @JsonCreator + public static ConversationStatus fromString(String name) { + try { + return valueOf(name.toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException ex) { + return null; + } + } + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationTimestamp.java b/src/main/java/com/vonage/client/conversations/ConversationTimestamp.java new file mode 100644 index 000000000..06610a891 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationTimestamp.java @@ -0,0 +1,63 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.time.Instant; + +/** + * Represents the timestamps in {@link BaseConversation#getTimestamp()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ConversationTimestamp extends JsonableBaseObject { + private Instant created, updated, destroyed; + + protected ConversationTimestamp() {} + + /** + * Time that the conversation was created. + * + * @return The conversation creation time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("created") + public Instant getCreated() { + return created; + } + + /** + * Time that the conversation was update. + * + * @return The conversation update time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("updated") + public Instant getUpdated() { + return updated; + } + + /** + * Time that the conversation was destroyed. + * + * @return The conversation deletion time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("destroyed") + public Instant getDestroyed() { + return destroyed; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationUpdatedEvent.java b/src/main/java/com/vonage/client/conversations/ConversationUpdatedEvent.java new file mode 100644 index 000000000..3082dac90 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationUpdatedEvent.java @@ -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.conversations; + +/** + * Represents a {@link EventType#CONVERSATION_UPDATED} event. + */ +public final class ConversationUpdatedEvent extends EventWithBody { + + ConversationUpdatedEvent() {} + + /** + * Basic details of the updated conversation. + * + * @return The main Conversation object properties. + */ + public BaseConversation getConversation() { + return body; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationsClient.java b/src/main/java/com/vonage/client/conversations/ConversationsClient.java new file mode 100644 index 000000000..f6fbc321a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationsClient.java @@ -0,0 +1,412 @@ +/* + * 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.conversations; + +import com.vonage.client.DynamicEndpoint; +import com.vonage.client.HttpWrapper; +import com.vonage.client.RestEndpoint; +import com.vonage.client.VonageClient; +import com.vonage.client.auth.JWTAuthMethod; +import com.vonage.client.common.HttpMethod; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; + +/** + * A client for communicating with the Vonage Conversations API. The standard way to obtain an instance + * of this class is to use {@link VonageClient#getConversationsClient()}. + */ +public class ConversationsClient { + final RestEndpoint listConversations; + final RestEndpoint createConversation; + final RestEndpoint getConversation; + final RestEndpoint updateConversation; + final RestEndpoint deleteConversation; + final RestEndpoint listUserConversations; + final RestEndpoint listMembers; + final RestEndpoint getMember; + final RestEndpoint createMember; + final RestEndpoint updateMember; + final RestEndpoint deleteEvent; + final RestEndpoint getEvent; + final RestEndpoint listEvents; + final RestEndpoint createEvent; + + /** + * Constructor. + * + * @param wrapper (REQUIRED) shared HTTP wrapper object used for making REST calls. + */ + @SuppressWarnings("unchecked") + public ConversationsClient(HttpWrapper wrapper) { + final String v1c = "/v1/conversations/", v1u = "/v1/users/", mems = "/members/", events = "/events/"; + + class Endpoint extends DynamicEndpoint { + Endpoint(Function pathGetter, HttpMethod method, R... type) { + super(DynamicEndpoint. builder(type) + .authMethod(JWTAuthMethod.class) + .responseExceptionType(ConversationsResponseException.class) + .requestMethod(method).wrapper(wrapper).pathGetter((de, req) -> { + String base = de.getHttpWrapper().getHttpConfig().getApiBaseUri(); + return base + pathGetter.apply(req); + }) + ); + } + } + + listConversations = new Endpoint<>(req -> v1c, HttpMethod.GET); + createConversation = new Endpoint<>(req -> v1c, HttpMethod.POST); + getConversation = new Endpoint<>(id -> v1c+id, HttpMethod.GET); + updateConversation = new Endpoint<>(req -> v1c+req.getId(), HttpMethod.PUT); + deleteConversation = new Endpoint<>(id -> v1c+id, HttpMethod.DELETE); + listUserConversations = new Endpoint<>(req -> v1u+req.userId+"/conversations", HttpMethod.GET); + listMembers = new Endpoint<>(req -> v1c+req.conversationId+mems, HttpMethod.GET); + getMember = new Endpoint<>(req -> v1c+req.conversationId+mems+req.resourceId, HttpMethod.GET); + createMember = new Endpoint<>(req -> v1c+req.getConversationId()+mems, HttpMethod.POST); + updateMember = new Endpoint<>(req -> v1c+req.conversationId+mems+req.resourceId, HttpMethod.PATCH); + deleteEvent = new Endpoint<>(req -> v1c+req.conversationId+events+req.resourceId, HttpMethod.DELETE); + getEvent = new Endpoint<>(req -> v1c+req.conversationId+events+req.resourceId, HttpMethod.GET); + listEvents = new Endpoint<>(req -> v1c+req.conversationId+events, HttpMethod.GET); + createEvent = new Endpoint<>(req -> v1c+req.conversationId+events, HttpMethod.POST); + } + + // VALIDATION + + private static String validateId(String prefix, String arg) { + final int prefixLength = prefix.length(), expectedLength = prefixLength + 36; + if (arg == null || arg.length() != expectedLength) { + throw new IllegalArgumentException( + "Invalid ID: '"+arg+"' is not "+expectedLength+" characters in length." + ); + } + if (!arg.startsWith(prefix)) { + String actualPrefix = arg.substring(0, prefixLength); + throw new IllegalArgumentException( + "Invalid ID: expected prefix '"+prefix+"' but got '"+actualPrefix+"'." + ); + } + return prefix + UUID.fromString(arg.substring(prefixLength)); + } + + private static String validateConversationId(String id) { + return validateId("CON-", id); + } + + static String validateMemberId(String id) { + return validateId("MEM-", id); + } + + private static String validateUserId(String id) { + return validateId("USR-", id); + } + + private static String validateEventId(int id) { + if (id < 0) { + throw new IllegalArgumentException("Event ID cannot be negative."); + } + return String.valueOf(id); + } + + private static T validateRequest(T request) { + return Objects.requireNonNull(request, "Request parameter is required."); + } + + private static > F defaultFilterParams(B builder) { + return builder.pageSize(100).build(); + } + + // ENDPOINTS + + /** + * Retrieve the first 100 Conversations in the application. Note that the returned conversations are + * incomplete, hence of type {@linkplain BaseConversation}. To get the full data, use the + * {@link #getConversation(String)} method, passing in the ID from {@linkplain BaseConversation#getId()}. + * + * @return A list of the first 100 conversations returned from the API, in default (ascending) order. + * + * @throws ConversationsResponseException If the API call fails due to a bad request (400). + * @see #listConversations(ListConversationsRequest) + */ + public List listConversations() { + return listConversations(defaultFilterParams(ListConversationsRequest.builder())).getConversations(); + } + + /** + * Retrieve conversations in the application which match the specified filter criteria. Note that the + * returned conversations in {@linkplain ListConversationsResponse#getConversations()} are incomplete, + * hence type of {@linkplain BaseConversation}. To get the full data, use {@link #getConversation(String)} + * method, passing in the ID from {@linkplain BaseConversation#getId()}. + * + * @param filter Filter options to narrow down the search results. + * + * @return The search results along with HAL metadata. + * + * @throws ConversationsResponseException If the API call fails due to a bad request (400). + */ + public ListConversationsResponse listConversations(ListConversationsRequest filter) { + return listConversations.execute(validateRequest(filter)); + } + + /** + * Creates a new Conversation within the application. + * + * @param request The Conversation parameters. Use {@code Conversation.builder().build()} for default settings. + * + * @return The created Conversation response with additional fields populated. + * + * @throws ConversationsResponseException If the Conversation name already exists (409), or any other API error. + */ + public Conversation createConversation(Conversation request) { + return createConversation.execute(validateRequest(request)); + } + + /** + * Retrieve a conversation by its ID. + * + * @param conversationId Unique identifier of the conversation to look up. + * + * @return Details of the conversation corresponding to the specified ID. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + */ + public Conversation getConversation(String conversationId) { + return getConversation.execute(validateConversationId(conversationId)); + } + + /** + * Update an existing conversation's settings / parameters. + * + * @param conversationId Unique conversation identifier. + * @param request Conversation object with the updated parameters. Any fields not set will be unchanged. + * + * @return The full updated conversation details. + * + * @throws ConversationsResponseException If the conversation was not found (404) + * or the parameters are invalid (400), e.g. the updated name already exists (409). + */ + public Conversation updateConversation(String conversationId, Conversation request) { + validateRequest(request).id = validateConversationId(conversationId); + return updateConversation.execute(request); + } + + /** + * Delete an existing conversation by ID. + * + * @param conversationId Unique conversation identifier. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + */ + public void deleteConversation(String conversationId) { + deleteConversation.execute(validateConversationId(conversationId)); + } + + /** + * List the first 100 conversations for a given user. + * + * @param userId Unique identifier for the user. + * + * @return The list of conversations the specified user is in, with default (ascending) order. + * + * @throws ConversationsResponseException If the user was not found (404), or any other API error. + * + * @see #listUserConversations(String, ListUserConversationsRequest) + * @see com.vonage.client.users + */ + public List listUserConversations(String userId) { + return listUserConversations(userId, + defaultFilterParams(ListUserConversationsRequest.builder()) + ).getConversations(); + } + + /** + * List the first 100 conversations for a given user. + * + * @param userId Unique identifier for the user. + * @param filter Filter options to narrow down the search results. + * + * @return The wrapped list of user conversations, along with HAL metadata. + * + * @throws ConversationsResponseException If the user was not found (404), + * the filter options were invalid (400) or any other API error. + * + * @see com.vonage.client.users + */ + public ListUserConversationsResponse listUserConversations(String userId, ListUserConversationsRequest filter) { + validateRequest(filter).userId = validateUserId(userId); + return listUserConversations.execute(filter); + } + + /** + * List the first 100 Members for a given Conversation. Note that the returned members are + * incomplete, hence of type {@linkplain BaseMember}. To get the full data, use the + * {@link #getMember(String, String)} method, passing in the ID from {@linkplain BaseMember#getId()}. + * + * @param conversationId Unique conversation identifier. + * + * @return The list of members in default (ascending) order. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + * + * @see #listMembers(String, ListMembersRequest) + */ + public List listMembers(String conversationId) { + return listMembers(conversationId, ListMembersRequest.builder().pageSize(100).build()).getMembers(); + } + + /** + * Retrieve Members associated with a particular Conversation which match the specified filter criteria. Note + * that the returned members are incomplete, hence of type {@linkplain BaseMember}. To get the full data, use + * the {@link #getMember(String, String)} method, passing in the ID from {@linkplain BaseMember#getId()}. + * + * @param conversationId Unique conversation identifier. + * @param filter Filter options to narrow down the search results. + * + * @return The wrapped list of Members, along with HAL metadata. + * + * @throws ConversationsResponseException If the conversation was not found (404), + * the filter options were invalid (400) or any other API error. + */ + public ListMembersResponse listMembers(String conversationId, ListMembersRequest filter) { + validateRequest(filter).conversationId = validateConversationId(conversationId); + return listMembers.execute(filter); + } + + /** + * Retrieve a conversation Member by its ID. + * + * @param conversationId Unique conversation identifier. + * @param memberId Unique identifier for the member. + * + * @return Details of the member corresponding to the specified ID. + * + * @throws ConversationsResponseException If the conversation or member was not found (404), or any other API error. + */ + public Member getMember(String conversationId, String memberId) { + return getMember.execute(new ConversationResourceRequestWrapper( + validateConversationId(conversationId), validateMemberId(memberId) + )); + } + + /** + * Creates a new Member for the specified conversation. + * + * @param conversationId Unique conversation identifier. + * @param request The Members parameters. Use {@link Member#builder()}, remember to set the mandatory parameters. + * + * @return The created Member response with additional fields populated. + * + * @throws ConversationsResponseException If the conversation was not found (404), + * the request parameters were invalid (400) or any other API error. + */ + public Member createMember(String conversationId, Member request) { + validateRequest(request).setConversationId(validateConversationId(conversationId)); + return createMember.execute(request); + } + + /** + * Update an existing member's state. + * + * @param request Details of the member to update. Use {@link UpdateMemberRequest#builder()}, + * remember to set the mandatory parameters, including the conversation and member IDs. + * + * @return The updated Member object response. + * + * @throws ConversationsResponseException If the conversation or member were not found (404), + * the request parameters were invalid (400) or any other API error. + */ + public Member updateMember(UpdateMemberRequest request) { + validateConversationId(validateRequest(request).conversationId); + validateMemberId(request.resourceId); + return updateMember.execute(request); + } + + /** + * List the first 100 events for a given Conversation. + * + * @param conversationId Unique conversation identifier. + * + * @return The list of events in default (ascending) order. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + */ + public List listEvents(String conversationId) { + return listEvents(conversationId, ListEventsRequest.builder().pageSize(100).build()).getEvents(); + } + + /** + * Retrieve Events associated with a particular Conversation which match the specified filter criteria. + * + * @param conversationId Unique conversation identifier. + * @param request Filter options to narrow down the search results. + * + * @return The wrapped list of Events, along with HAL metadata. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + */ + public ListEventsResponse listEvents(String conversationId, ListEventsRequest request) { + validateRequest(request).conversationId = validateConversationId(conversationId); + return listEvents.execute(request); + } + + /** + * Retrieve a conversation Event by its ID. + * + * @param conversationId Unique conversation identifier. + * @param eventId Sequence ID of the event to retrieve as an integer. + * + * @return Details of the event corresponding to the specified ID. + * + * @throws ConversationsResponseException If the conversation or event was not found (404), or any other API error. + */ + public Event getEvent(String conversationId, int eventId) { + return getEvent.execute(new ConversationResourceRequestWrapper( + validateConversationId(conversationId), validateEventId(eventId) + )); + } + + /** + * Creates a new Event for the specified conversation. + * + * @param conversationId Unique conversation identifier. + * @param request Details of the event to create. + * + * @return The created Event response with additional fields populated. + * + * @throws ConversationsResponseException If the conversation was not found (404), or any other API error. + */ + @SuppressWarnings("unchecked") + public E createEvent(String conversationId, E request) { + validateRequest(request).conversationId = validateConversationId(conversationId); + return (E) createEvent.execute(request); + } + + /** + * Deletes an event. Only message and custom events can be deleted. + * + * @param conversationId Unique conversation identifier. + * @param eventId Sequence ID of the event to retrieve as an integer. + * + * @throws ConversationsResponseException If the conversation or event was not found (404), + * the event could not be deleted, or any other API error. + */ + public void deleteEvent(String conversationId, int eventId) { + deleteEvent.execute(new ConversationResourceRequestWrapper( + validateConversationId(conversationId), validateEventId(eventId) + )); + } +} diff --git a/src/main/java/com/vonage/client/conversations/ConversationsResponseException.java b/src/main/java/com/vonage/client/conversations/ConversationsResponseException.java new file mode 100644 index 000000000..a60db854c --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ConversationsResponseException.java @@ -0,0 +1,45 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.vonage.client.Jsonable; +import com.vonage.client.VonageApiResponseException; + +/** + * Response returned when an error is encountered (i.e. the API returns a non-2xx status code). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ConversationsResponseException extends VonageApiResponseException { + + void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + /** + * Creates an instance of this class from a JSON payload. + * + * @param json The JSON string to parse. + * @return An instance of this class with all known fields populated from the JSON payload, if present. + */ + @JsonCreator + public static ConversationsResponseException fromJson(String json) { + return Jsonable.fromJson(json); + } +} diff --git a/src/main/java/com/vonage/client/conversations/CustomEvent.java b/src/main/java/com/vonage/client/conversations/CustomEvent.java new file mode 100644 index 000000000..9468dbfd4 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/CustomEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents a Custom event. + */ +public final class CustomEvent extends GenericEvent { + + private CustomEvent() {} + + private CustomEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends GenericEvent.Builder { + Builder() { + super(EventType.CUSTOM); + } + + @Override + public CustomEvent build() { + return new CustomEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/EphemeralEvent.java b/src/main/java/com/vonage/client/conversations/EphemeralEvent.java new file mode 100644 index 000000000..756c03bff --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/EphemeralEvent.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents an Ephemeral event. + */ +public final class EphemeralEvent extends GenericEvent { + + private EphemeralEvent() {} + + private EphemeralEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends GenericEvent.Builder { + Builder() { + super(EventType.EPHEMERAL); + } + + @Override + public EphemeralEvent build() { + return new EphemeralEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/Event.java b/src/main/java/com/vonage/client/conversations/Event.java new file mode 100644 index 000000000..f3ca0d3cd --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/Event.java @@ -0,0 +1,177 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.*; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.users.User; +import java.time.Instant; +import java.util.Objects; + +/** + * Events are actions that occur within a conversation. + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = GenericEvent.class +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = EventDeleteEvent.class, name = "event:delete"), + @JsonSubTypes.Type(value = ConversationUpdatedEvent.class, name = "conversation:updated"), + @JsonSubTypes.Type(value = MessageSeenEvent.class, name = "message:seen"), + @JsonSubTypes.Type(value = MessageSubmittedEvent.class, name = "message:submitted"), + @JsonSubTypes.Type(value = MessageDeliveredEvent.class, name = "message:delivered"), + @JsonSubTypes.Type(value = MessageUndeliverableEvent.class, name = "message:undeliverable"), + @JsonSubTypes.Type(value = MessageRejectedEvent.class, name = "message:rejected"), + @JsonSubTypes.Type(value = AudioPlayEvent.class, name = "audio:play"), + @JsonSubTypes.Type(value = AudioPlayStopEvent.class, name = "audio:play:stop"), + @JsonSubTypes.Type(value = AudioPlayDoneEvent.class, name = "audio:play:done"), + @JsonSubTypes.Type(value = AudioSayEvent.class, name = "audio:say"), + @JsonSubTypes.Type(value = AudioSayStopEvent.class, name = "audio:say:stop"), + @JsonSubTypes.Type(value = AudioSayDoneEvent.class, name = "audio:say:done"), + @JsonSubTypes.Type(value = AudioRecordEvent.class, name = "audio:record"), + @JsonSubTypes.Type(value = AudioRecordStopEvent.class, name = "audio:record:stop"), + @JsonSubTypes.Type(value = AudioMuteOnEvent.class, name = "audio:mute:on"), + @JsonSubTypes.Type(value = AudioMuteOffEvent.class, name = "audio:mute:off"), + @JsonSubTypes.Type(value = AudioEarmuffOnEvent.class, name = "audio:earmuff:on"), + @JsonSubTypes.Type(value = AudioEarmuffOffEvent.class, name = "audio:earmuff:off"), + @JsonSubTypes.Type(value = EphemeralEvent.class, name = "ephemeral"), + @JsonSubTypes.Type(value = CustomEvent.class, name = "custom") +}) +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Event extends JsonableBaseObject { + @JsonIgnore String conversationId; + @JsonProperty("type") EventType type; + @JsonProperty("id") Integer id; + @JsonProperty("from") String from; + @JsonProperty("timestamp") Instant timestamp; + @JsonProperty("_embedded") Embedded _embedded; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + private static class Embedded extends JsonableBaseObject { + @JsonProperty("from_user") User fromUser; + @JsonProperty("from_member") BaseMember fromMember; + } + + protected Event() { + } + + Event(Builder builder) { + type = Objects.requireNonNull(builder.type, "Event type is required."); + from = ConversationsClient.validateMemberId(builder.from); + } + + /** + * Event id. This is a progressive integer. + * + * @return The event ID as an integer, or {@code null} if unknown. + */ + public Integer getId() { + return id; + } + + /** + * Type of event. + * + * @return The event type as an enum. + */ + public EventType getType() { + return type; + } + + /** + * Member ID this event was sent from. + * + * @return The member ID, or {@code null} if unspecified. + */ + public String getFrom() { + return from; + } + + /** + * Time of creation. + * + * @return The event timestamp, or {@code null} if unknown. + */ + public Instant getTimestamp() { + return timestamp; + } + + /** + * Details about the user that initiated the event. + * + * @return The embedded {@code from_user} object, or {@code null} if absent. + */ + @JsonIgnore + public User getFromUser() { + return _embedded != null ? _embedded.fromUser : null; + } + + /** + * Member that initiated the event. Only the {@code id} field will be present. + * + * @return The embedded {@code from_member} object, or {@code null} if absent. + */ + @JsonIgnore + public BaseMember getFromMember() { + return _embedded != null ? _embedded.fromMember : null; + } + + /** + * Builder for constructing an event request's parameters. + * + * @param The event type. + * @param The builder type. + */ + @SuppressWarnings("unchecked") + public abstract static class Builder> { + private final EventType type; + private String from; + + /** + * Construct a new builder for a given event type. + * + * @param type The event type as an enum. + */ + protected Builder(EventType type) { + this.type = type; + } + + /** + * Member ID this event was sent from. + * + * @param from The member ID, or {@code null} if unspecified. + * + * @return This builder. + */ + public B from(String from) { + this.from = from; + return (B) this; + } + + /** + * Builds the {@linkplain EventWithBody}. + * + * @return An instance of Event, populated with all fields from this builder. + */ + public abstract E build(); + } +} diff --git a/src/main/java/com/vonage/client/conversations/EventDeleteEvent.java b/src/main/java/com/vonage/client/conversations/EventDeleteEvent.java new file mode 100644 index 000000000..a500d7e38 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/EventDeleteEvent.java @@ -0,0 +1,24 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#EVENT_DELETE} event. + */ +public final class EventDeleteEvent extends MessageStatusEvent { + + EventDeleteEvent() {} +} diff --git a/src/main/java/com/vonage/client/conversations/EventType.java b/src/main/java/com/vonage/client/conversations/EventType.java new file mode 100644 index 000000000..269bc7e8a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/EventType.java @@ -0,0 +1,286 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the possible types of events in {@link Event#getType()}. + */ +public enum EventType { + /** + * Event type is not defined in this enum. + */ + UNKNOWN, + + /** + * Ephemeral (temporary) event. + */ + EPHEMERAL, + + /** + * Custom event. + */ + CUSTOM, + + /** + * Message event. See {@link com.vonage.client.common.MessageType} for the specifics. Applicable events are: + * Text, Image, Audio, Video, File, Template, Custom, VCard, Location, Random. + */ + MESSAGE, + + /** + * Message submitted. + */ + MESSAGE_SUBMITTED, + + /** + * Message rejected. + */ + MESSAGE_REJECTED, + + /** + * Message could not be delivered. + */ + MESSAGE_UNDELIVERABLE, + + /** + * Message has been seen. + */ + MESSAGE_SEEN, + + /** + * Message has been delivered. + */ + MESSAGE_DELIVERED, + + /** + * Play audio. + */ + AUDIO_PLAY, + + /** + * Stop the audio currently playing. + */ + AUDIO_PLAY_STOP, + + /** + * Use text-to-speech to say the given text. + */ + AUDIO_SAY, + + /** + * Stop the currently playing text-to-speech. + */ + AUDIO_SAY_STOP, + + /** + * Play DTMF audio. + */ + AUDIO_DTMF, + + /** + * Record the audio. + */ + AUDIO_RECORD, + + /** + * Stop current recording of audio. + */ + AUDIO_RECORD_STOP, + + /** + * Mute audio, i.e. the producer won't be able to be heard by others. + */ + AUDIO_MUTE_ON, + + /** + * Unmute audio, i.e. the producer will be able to be heard by others. + */ + AUDIO_MUTE_OFF, + + /** + * Earmuff audio, i.e. the receiver won't be able to hear it. + */ + AUDIO_EARMUFF_ON, + + /** + * Unearmuff audio, i.e. the receiver will be able to hear it. + */ + AUDIO_EARMUFF_OFF, + + /** + * Event has been deleted. + */ + EVENT_DELETE, + + /** + * Update on the status of a conversation leg. + */ + LEG_STATUS_UPDATE, + + /** + * Conversation details have been updated. + */ + CONVERSATION_UPDATED, + + /** + * Playing text-to-speech has finished. + */ + AUDIO_SAY_DONE, + + /** + * Playing audio has finished. + */ + AUDIO_PLAY_DONE, + + /** + * Audio recording has finished. + */ + AUDIO_RECORD_DONE, + + /** + * Enable speaking. + */ + AUDIO_SPEAKING_ON, + + /** + * Disable speaking. + */ + AUDIO_SPEAKING_OFF, + + /** + * Automatic speech recognition has finished. + */ + AUDIO_ASR_DONE, + + /** + * Automatic speech recognition of recording has finished. + */ + AUDIO_ASR_RECORD_DONE, + + /** + * Status update on a member message. + */ + MEMBER_MESSAGE_STATUS, + + /** + * Member has been invited to the conversation. + */ + MEMBER_INVITED, + + /** + * Member has joined the conversation. + */ + MEMBER_JOINED, + + /** + * Member has left the conversation. + */ + MEMBER_LEFT, + + /** + * Member has media. + */ + MEMBER_MEDIA, + + /** + * Status of SIP. + */ + SIP_STATUS, + + /** + * SIP call has been hung up. + */ + SIP_HANGUP, + + /** + * SIP call has been answered. + */ + SIP_ANSWERED, + + /** + * SIP call encountered machine. + */ + SIP_MACHINE, + + /** + * SIP call encountered machine using Advanced Machine Detection. + */ + SIP_AMD_MACHINE, + + /** + * SIP call is ringing. + */ + SIP_RINGING, + + /** + * RTC status. + */ + RTC_STATUS, + + /** + * RTC call transfer. + */ + RTC_TRANSFER, + + /** + * RTC call has hung up. + */ + RTC_HANGUP, + + /** + * RTC call has been answered. + */ + RTC_ANSWERED, + + /** + * RTC call is ringing. + */ + RTC_RINGING, + + /** + * RTC call answer. + */ + RTC_ANSWER; + + @JsonCreator + public static EventType fromString(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + String upper = name.toUpperCase(); + if (upper.startsWith("CUSTOM:")) { + return CUSTOM; + } + try { + return valueOf(upper.replace(':', '_')); + } + catch (IllegalArgumentException ex) { + return EventType.UNKNOWN; + } + } + + @JsonValue + @Override + public String toString() { + if (this == SIP_AMD_MACHINE) { + return "sip:amd_machine"; + } + return name().toLowerCase().replace('_', ':'); + } +} diff --git a/src/main/java/com/vonage/client/conversations/EventWithBody.java b/src/main/java/com/vonage/client/conversations/EventWithBody.java new file mode 100644 index 000000000..2026f303b --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/EventWithBody.java @@ -0,0 +1,32 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class EventWithBody extends Event { + @JsonProperty("body") T body; + + EventWithBody() {} + + EventWithBody(Event.Builder, ?> builder) { + super(builder); + } +} diff --git a/src/main/java/com/vonage/client/conversations/GenericEvent.java b/src/main/java/com/vonage/client/conversations/GenericEvent.java new file mode 100644 index 000000000..8a62c3a21 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/GenericEvent.java @@ -0,0 +1,61 @@ +/* + * 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.conversations; + +import java.util.Map; + +class GenericEvent extends EventWithBody> { + + GenericEvent() {} + + GenericEvent(Builder builder) { + super(builder); + body = builder.body; + } + + /** + * Custom event data as key-value pairs. + * + * @return The event's main body as a Map. + */ + public Map getBody() { + return body; + } + + @SuppressWarnings("unchecked") + static abstract class Builder> + extends EventWithBody.Builder> { + + Map body; + + Builder(EventType type) { + super(type); + } + + /** + * Custom data send in the body. + * + * @param body The custom data as key-value pairs. + * + * @return This builder. + */ + public B body(Map body) { + this.body = body; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListConversationsRequest.java b/src/main/java/com/vonage/client/conversations/ListConversationsRequest.java new file mode 100644 index 000000000..5db51534a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListConversationsRequest.java @@ -0,0 +1,69 @@ +/* + * 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.conversations; + +import java.time.Instant; + +/** + * Filters results for {@link ConversationsClient#listConversations(ListConversationsRequest)}. + */ +public final class ListConversationsRequest extends AbstractConversationsFilterRequest { + ListConversationsRequest(Builder builder) { + super(builder); + } + + @Override + public Instant getStartDate() { + return startDate; + } + + @Override + public Instant getEndDate() { + return endDate; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AbstractConversationsFilterRequest.Builder { + Builder() {} + + @Override + public Builder startDate(Instant startDate) { + return super.startDate(startDate); + } + + @Override + public Builder endDate(Instant endDate) { + return super.endDate(endDate); + } + + /** + * Builds the {@linkplain ListConversationsRequest}. + * + * @return An instance of ListConversationsRequest, populated with all fields from this builder. + */ + public ListConversationsRequest build() { + return new ListConversationsRequest(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListConversationsResponse.java b/src/main/java/com/vonage/client/conversations/ListConversationsResponse.java new file mode 100644 index 000000000..480f68baf --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListConversationsResponse.java @@ -0,0 +1,56 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.HalPageResponse; +import java.util.List; + +/** + * HAL response for {@link ConversationsClient#listConversations(ListConversationsRequest)}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListConversationsResponse extends HalPageResponse { + @JsonProperty("_embedded") private Embedded _embedded; + + ListConversationsResponse() { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class Embedded extends JsonableBaseObject { + private List conversations; + + @JsonProperty("conversations") + public List getConversations() { + return conversations; + } + } + + /** + * Gets the conversations contained in the {@code _embedded} response. + * + * @return The conversations for this page. + */ + @JsonIgnore + public List getConversations() { + return _embedded != null ? _embedded.getConversations() : null; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListEventsRequest.java b/src/main/java/com/vonage/client/conversations/ListEventsRequest.java new file mode 100644 index 000000000..7f017c4c2 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListEventsRequest.java @@ -0,0 +1,164 @@ +/* + * 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.conversations; + +import java.util.Map; + +/** + * Filters results for {@link ConversationsClient#listEvents(String, ListEventsRequest)}. + */ +public class ListEventsRequest extends AbstractConversationsFilterRequest { + private final Boolean excludeDeletedEvents; + private final Integer startId, endId; + private final EventType eventType; + + ListEventsRequest(Builder builder) { + super(builder); + excludeDeletedEvents = builder.excludeDeletedEvents; + startId = builder.startId; + endId = builder.endId; + eventType = builder.eventType; + } + + @Override + public Map makeParams() { + Map params = super.makeParams(); + if (excludeDeletedEvents != null) { + params.put("exclude_deleted_events", excludeDeletedEvents.toString()); + } + if (startId != null) { + params.put("start_id", String.valueOf(startId)); + } + if (endId != null) { + params.put("end_id", String.valueOf(endId)); + } + if (eventType != null) { + params.put("event_type", eventType.toString()); + } + return params; + } + + /** + * Whether to exclude deleted events from the results. + * + * @return {@code true} to exclude deleted events, or {@code null} if unspecified. + */ + public Boolean getExcludeDeletedEvents() { + return excludeDeletedEvents; + } + + /** + * The ID to start returning events at. + * + * @return The start ID as an Integer, or {@code null} if unspecified. + */ + public Integer getStartId() { + return startId; + } + + /** + * The ID to stop returning events at. + * + * @return The end ID as an Integer, or {@code null} if unspecified. + */ + public Integer getEndId() { + return endId; + } + + /** + * The type of event to search for. Does not currently support custom events. + * + * @return The event type to search for, or {@code null} if unspecified. + */ + public EventType getEventType() { + return eventType; + } + + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AbstractConversationsFilterRequest.Builder { + private Boolean excludeDeletedEvents; + private Integer startId, endId; + private EventType eventType; + + Builder() {} + + /** + * Whether to exclude deleted events from the results. + * + * @param excludeDeletedEvents {@code true} to exclude deleted events, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder excludeDeletedEvents(boolean excludeDeletedEvents) { + this.excludeDeletedEvents = excludeDeletedEvents; + return this; + } + + /** + * The ID to start returning events at. + * + * @param startId The start ID as an int. + * + * @return This builder. + */ + public Builder startId(int startId) { + this.startId = startId; + return this; + } + + /** + * The ID to stop returning events at. + * + * @param endId The end ID as an int. + * + * @return This builder. + */ + public Builder endId(int endId) { + this.endId = endId; + return this; + } + + /** + * The type of event to search for. Does not currently support custom events. + * + * @param eventType The event type to search for as an enum. + * + * @return This builder. + */ + public Builder eventType(EventType eventType) { + this.eventType = eventType; + return this; + } + + /** + * Builds the {@linkplain ListEventsRequest}. + * + * @return An instance of ListEventsRequest, populated with all fields from this builder. + */ + public ListEventsRequest build() { + return new ListEventsRequest(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListEventsResponse.java b/src/main/java/com/vonage/client/conversations/ListEventsResponse.java new file mode 100644 index 000000000..4cfe551c8 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListEventsResponse.java @@ -0,0 +1,44 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.common.HalPageResponse; +import java.util.List; + +/** + * HAL response for {@link ConversationsClient#listEvents(String, ListEventsRequest)}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListEventsResponse extends HalPageResponse { + @JsonProperty("_embedded") private List events; + + ListEventsResponse() { + } + + /** + * Gets the events contained in the {@code _embedded} response. + * + * @return The events for this page. + */ + @JsonProperty("_embedded") + public List getEvents() { + return events; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListMembersRequest.java b/src/main/java/com/vonage/client/conversations/ListMembersRequest.java new file mode 100644 index 000000000..8a543f6b0 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListMembersRequest.java @@ -0,0 +1,48 @@ +/* + * 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.conversations; + +/** + * Represents filter options for {@link ConversationsClient#listMembers(String, ListMembersRequest)}. + */ +public final class ListMembersRequest extends AbstractConversationsFilterRequest { + + ListMembersRequest(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AbstractConversationsFilterRequest.Builder { + Builder() {} + + /** + * Builds the {@linkplain ListMembersRequest}. + * + * @return An instance of ListMembersRequest, populated with all fields from this builder. + */ + public ListMembersRequest build() { + return new ListMembersRequest(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListMembersResponse.java b/src/main/java/com/vonage/client/conversations/ListMembersResponse.java new file mode 100644 index 000000000..2dcc5f00a --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListMembersResponse.java @@ -0,0 +1,56 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.HalPageResponse; +import java.util.List; + +/** + * HAL response for {@link ConversationsClient#listMembers(String, ListMembersRequest)}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListMembersResponse extends HalPageResponse { + @JsonProperty("_embedded") private Embedded _embedded; + + ListMembersResponse() { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class Embedded extends JsonableBaseObject { + private List members; + + @JsonProperty("members") + public List getMembers() { + return members; + } + } + + /** + * Gets the members contained in the {@code _embedded} response. + * + * @return The members for this page. + */ + @JsonIgnore + public List getMembers() { + return _embedded != null ? _embedded.getMembers() : null; + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListUserConversationsRequest.java b/src/main/java/com/vonage/client/conversations/ListUserConversationsRequest.java new file mode 100644 index 000000000..0d6be01c0 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListUserConversationsRequest.java @@ -0,0 +1,150 @@ +/* + * 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.conversations; + +import java.time.Instant; +import java.util.Map; + +/** + * Represents filter options for {@link ConversationsClient#listUserConversations(String, ListUserConversationsRequest)}. + */ +public final class ListUserConversationsRequest extends AbstractListUserRequest { + private final MemberState state; + private final OrderBy orderBy; + private final Boolean includeCustomData; + + ListUserConversationsRequest(Builder builder) { + super(builder); + state = builder.state; + orderBy = builder.orderBy; + includeCustomData = builder.includeCustomData; + } + + @Override + public Map makeParams() { + Map params = super.makeParams(); + if (state != null) { + params.put("state", state.toString()); + } + if (orderBy != null) { + params.put("order_by", orderBy.toString()); + } + if (includeCustomData != null) { + params.put("include_custom_data", includeCustomData.toString()); + } + return params; + } + + + @Override + public Instant getStartDate() { + return startDate; + } + + /** + * Only include conversations with this member state. + * + * @return The state to filter by, or {@code null} if not specified. + */ + public MemberState getState() { + return state; + } + + /** + * Determines how the results should be compared and ordered. + * + * @return The result ordering strategy, or {@code null} if not specified. + */ + public OrderBy getOrderBy() { + return orderBy; + } + + /** + * Whether to include custom data in the responses. + * + * @return {@code true} if custom data should be included, or {@code null} if not specified. + */ + public Boolean getIncludeCustomData() { + return includeCustomData; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AbstractConversationsFilterRequest.Builder { + private MemberState state; + private OrderBy orderBy; + private Boolean includeCustomData; + + Builder() {} + + @Override + public Builder startDate(Instant startDate) { + return super.startDate(startDate); + } + + /** + * Only include conversations with this member state. + * + * @param state The state to filter by.. + * + * @return This builder. + */ + public Builder state(MemberState state) { + this.state = state; + return this; + } + + /** + * Determines how the results should be compared and ordered. + * + * @param orderBy The result ordering strategy. + * + * @return This builder. + */ + public Builder orderBy(OrderBy orderBy) { + this.orderBy = orderBy; + return this; + } + + /** + * Whether to include custom data in the responses. + * + * @param includeCustomData {@code true} if custom data should be included. + * + * @return This builder. + */ + public Builder includeCustomData(Boolean includeCustomData) { + this.includeCustomData = includeCustomData; + return this; + } + + /** + * Builds the {@linkplain ListUserConversationsRequest}. + * + * @return An instance of ListUserConversationsRequest, populated with all fields from this builder. + */ + public ListUserConversationsRequest build() { + return new ListUserConversationsRequest(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/ListUserConversationsResponse.java b/src/main/java/com/vonage/client/conversations/ListUserConversationsResponse.java new file mode 100644 index 000000000..08b0ea542 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/ListUserConversationsResponse.java @@ -0,0 +1,57 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.HalPageResponse; +import java.util.List; + +/** + * HAL response for {@link ConversationsClient#listUserConversations(String)}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListUserConversationsResponse extends HalPageResponse { + @JsonProperty("_embedded") private Embedded _embedded; + + ListUserConversationsResponse() { + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static final class Embedded extends JsonableBaseObject { + private List conversations; + + @JsonProperty("conversations") + public List getConversations() { + return conversations; + } + } + + /** + * Gets the conversations contained in the {@code _embedded} response. + * + * @return The conversations for this page. + */ + @JsonIgnore + public List getConversations() { + return _embedded != null ? _embedded.getConversations() : null; + } +} diff --git a/src/main/java/com/vonage/client/conversations/Location.java b/src/main/java/com/vonage/client/conversations/Location.java new file mode 100644 index 000000000..241001274 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/Location.java @@ -0,0 +1,156 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; + +/** + * Describes parameters for a Location message in {@link MessageEvent#getLocation()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class Location extends JsonableBaseObject { + private Double longitude, latitude; + private String name, address; + + Location() {} + + Location(Builder builder) { + longitude = builder.longitude; + latitude = builder.latitude; + name = builder.name; + address = builder.address; + } + + /** + * Longitude of the location. + * + * @return The longitude as a Double, or {@code null} if unspecified. + */ + @JsonProperty("longitude") + public Double getLongitude() { + return longitude; + } + + /** + * Latitude of the location. + * + * @return The latitude as a Double, or {@code null} if unspecified. + */ + @JsonProperty("latitude") + public Double getLatitude() { + return latitude; + } + + /** + * Name of the location. + * + * @return The name, or {@code null} if unspecified. + */ + @JsonProperty("name") + public String getName() { + return name; + } + + /** + * Full location address. + * + * @return The address as a string, or {@code null} if unspecified. + */ + @JsonProperty("address") + public String getAddress() { + return address; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for setting Location parameters. + */ + public static final class Builder { + private Double longitude, latitude; + private String name, address; + + private Builder() {} + + /** + * Longitude of the location. + * + * @param longitude The longitude as a double. + * + * @return This builder. + */ + public Builder longitude(double longitude) { + this.longitude = longitude; + return this; + } + + /** + * Latitude of the location. + * + * @param latitude The latitude as a double. + * + * @return This builder. + */ + public Builder latitude(double latitude) { + this.latitude = latitude; + return this; + } + + /** + * Name of the location. + * + * @param name The name. + * + * @return This builder. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Full address. + * + * @param address The address as a string. + * + * @return This builder. + */ + public Builder address(String address) { + this.address = address; + return this; + } + + /** + * Builds the {@linkplain Location}. + * + * @return A new Location instance, populated with all fields from this builder. + */ + public Location build() { + return new Location(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MediaAudioSettings.java b/src/main/java/com/vonage/client/conversations/MediaAudioSettings.java new file mode 100644 index 000000000..d3f52185e --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MediaAudioSettings.java @@ -0,0 +1,62 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; + +/** + * Represents the audio options in {@link MemberMedia#getAudioSettings}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MediaAudioSettings extends JsonableBaseObject { + Boolean enabled, earmuffed, muted; + + protected MediaAudioSettings() {} + + /** + * Whether audio is enabled. + * + * @return {@code true} if enabled, or {@code null} if unspecified. + */ + @JsonProperty("enabled") + public Boolean getEnabled() { + return enabled; + } + + /** + * Whether hearing audio is disabled. + * + * @return {@code true} if earmuffed, or {@code null} if unspecified. + */ + @JsonProperty("earmuffed") + public Boolean getEarmuffed() { + return earmuffed; + } + + /** + * Whether producing audio is disabled. + * + * @return {@code true} if muted, or {@code null} if unspecified. + */ + @JsonProperty("muted") + public Boolean getMuted() { + return muted; + } +} diff --git a/src/main/java/com/vonage/client/conversations/Member.java b/src/main/java/com/vonage/client/conversations/Member.java new file mode 100644 index 000000000..a012ed454 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/Member.java @@ -0,0 +1,307 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.*; +import com.vonage.client.common.ChannelType; +import com.vonage.client.users.BaseUser; +import com.vonage.client.users.channels.Channel; +import java.util.Objects; +import java.util.UUID; + +/** + * Represents a conversation membership. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Member extends BaseMember { + private String conversationId, memberIdInviting, from, invitedBy; + private UUID knockingId; + private MemberChannel channel; + private MemberMedia media; + private MemberInitiator initiator; + private MemberTimestamp timestamp; + + protected Member() { + } + + Member(Builder builder) { + user = Objects.requireNonNull(builder.user, "User is required."); + state = Objects.requireNonNull(builder.state, "State is required."); + channel = Objects.requireNonNull(builder.channel, "Channel is required."); + media = builder.media; + knockingId = builder.knockingId; + memberIdInviting = builder.memberIdInviting; + from = builder.from; + } + + @JsonSetter("conversation_id") + void setConversationId(String conversationId) { + this.conversationId = conversationId; + } + + /** + * Unique identifier for this member's conversation. + * + * @return The conversation ID, or {@code null} if this is a request. + */ + @JsonIgnore + public String getConversationId() { + return conversationId; + } + + /** + * Channel details for this membership. + * + * @return The channel. + */ + @JsonProperty("channel") + public MemberChannel getChannel() { + return channel; + } + + /** + * Media settings for this member. + * + * @return The media settings object, or {@code null} if unspecified. + */ + @JsonProperty("media") + public MemberMedia getMedia() { + return media; + } + + /** + * Unique knocking identifier. + * + * @return The knocking ID, or {@code null} if unspecified. + */ + @JsonProperty("knocking_id") + public UUID getKnockingId() { + return knockingId; + } + + /** + * Unique member ID this member was invited by. + * This will be set when the invite has been created but not accepted. + * + * @return The inviting member ID, or {@code null} if unspecified. + * @see #getInvitedBy() + */ + @JsonProperty("member_id_inviting") + public String getMemberIdInviting() { + return memberIdInviting; + } + + /** + * TODO document this + * + * @return The from field, or {@code null} if unspecified. + */ + @JsonProperty("from") + public String getFrom() { + return from; + } + + /** + * Unique member ID this member was invited by. + * This will be set when the invite has been accepted. + * + * @return The inviting member ID, or {@code null} if unknown / not applicable. + * @see #getMemberIdInviting() + */ + @JsonProperty("invited_by") + public String getInvitedBy() { + return invitedBy; + } + + /** + * Describes how this member was initiated. + * + * @return The initiator details, or {@code null} if unspecified. + */ + @JsonProperty("initiator") + public MemberInitiator getInitiator() { + return initiator; + } + + /** + * Timestamps for this member. + * + * @return The timestamps object, or {@code null} if this is a request. + */ + @JsonProperty("timestamp") + public MemberTimestamp getTimestamp() { + return timestamp; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private BaseUser user; + private MemberState state; + private MemberChannel channel; + private MemberMedia media; + private UUID knockingId; + private String memberIdInviting, from; + + Builder() {} + + private MemberChannel initChannel() { + if (channel == null) channel = new MemberChannel(); + return channel; + } + + /** + * (REQUIRED) Either the user ID or unique name is required. This method will automatically + * determine which is provided. User IDs start with {@code USR-} followed by a UUID. + * + * @param userNameOrId Either the unique user ID, or the name. + * + * @return This builder. + */ + public Builder user(String userNameOrId) { + if (userNameOrId == null || userNameOrId.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid user name or ID."); + } + if (userNameOrId.startsWith("USR-") && userNameOrId.length() == 40) { + user = new BaseUser(userNameOrId, null); + } + else { + user = new BaseUser(null, userNameOrId); + } + return this; + } + + /** + * (REQUIRED) Invite or join a member to a conversation. + * + * @param state The state as an enum. + * + * @return This builder. + */ + public Builder state(MemberState state) { + this.state = state; + return this; + } + + /** + * (REQUIRED) Top-level channel type. You should also provide {@linkplain #fromChannel(Channel)} and + * {@linkplain #toChannel(Channel)}. If this is set to anything other than {@linkplain ChannelType#APP}, + * then both {@linkplain #fromChannel(Channel)} and {@linkplain #toChannel(Channel)} + * must be of the same type as each other. + * + * @param channelType The channel type as an enum. + * + * @return This builder. + */ + public Builder channelType(ChannelType channelType) { + initChannel().type = channelType; + return this; + } + + /** + * (OPTIONAL) Concrete channel to use when sending messages. + * See {@linkplain com.vonage.client.users.channels} for options. + * + * @param from The sender channel. + * + * @return This builder. + * @see #channelType(ChannelType) + */ + public Builder fromChannel(Channel from) { + (initChannel().from = from).setTypeField(); + return this; + } + + /** + * (OPTIONAL) Concrete channel to use when receiving messages. + * See {@linkplain com.vonage.client.users.channels} for options. + * + * @param to The receiver channel. + * + * @return This builder. + * @see #channelType(ChannelType) + */ + public Builder toChannel(Channel to) { + (initChannel().to = to).setTypeField(); + return this; + } + + /** + * (OPTIONAL) Media settings for this member. + * + * @param media The media settings object. + * + * @return This builder. + */ + public Builder media(MemberMedia media) { + this.media = media; + return this; + } + + /** + * (OPTIONAL) Unique knocking identifier. + * + * @param knockingId The knocking ID as a string. + * + * @return This builder. + */ + public Builder knockingId(String knockingId) { + this.knockingId = UUID.fromString(knockingId); + return this; + } + + /** + * (OPTIONAL) Unique member ID to invite. + * + * @param memberIdInviting The inviting member ID, or {@code null} if unspecified. + * + * @return This builder. + */ + public Builder memberIdInviting(String memberIdInviting) { + this.memberIdInviting = memberIdInviting; + return this; + } + + /** + * (OPTIONAL) TODO document this + * + * @param from The from field. + * + * @return This builder. + */ + public Builder from(String from) { + this.from = from; + return this; + } + + /** + * Builds the {@linkplain Member}. + * + * @return An instance of Member, populated with all fields from this builder. + */ + public Member build() { + return new Member(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MemberChannel.java b/src/main/java/com/vonage/client/conversations/MemberChannel.java new file mode 100644 index 000000000..89547a69c --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MemberChannel.java @@ -0,0 +1,111 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.vonage.client.Jsonable; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.ChannelType; +import com.vonage.client.users.channels.Channel; +import java.io.IOException; +import java.util.Objects; + +/** + * Contains the channel properties for {@link Member#getChannel()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = MemberChannel.Deserializer.class) +public class MemberChannel extends JsonableBaseObject { + ChannelType type; + Channel from, to; + + protected MemberChannel() { + } + + /** + * Main channel type. + * + * @return The channel type as an enum, or {@code null} if unspecified. + */ + @JsonProperty("type") + public ChannelType getType() { + return type; + } + + /** + * Sender channel. + * + * @return The from channel, or {@code null} if unspecified. + */ + @JsonProperty("from") + public Channel getFrom() { + return from; + } + + /** + * Receiver channel. + * + * @return The to channel, or {@code null} if unspecified. + */ + @JsonProperty("to") + public Channel getTo() { + return to; + } + + static class Deserializer extends StdDeserializer { + private MemberChannel mc; + + protected Deserializer() { + super(MemberChannel.class); + } + + @Override + public MemberChannel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserialize(p, ctxt, new MemberChannel()); + } + + @Override + public MemberChannel deserialize(JsonParser p, DeserializationContext ctxt, MemberChannel intoValue) throws IOException { + mc = Objects.requireNonNull(intoValue); + JsonNode rootNode = p.readValueAsTree(), typeNode = rootNode.get("type"); + if (typeNode != null) { + mc.type = ChannelType.fromString(typeNode.asText()); + } + mc.from = inferConcreteChannel(rootNode.get("from")); + mc.to = inferConcreteChannel(rootNode.get("to")); + return mc; + } + + private Channel inferConcreteChannel(JsonNode node) { + if (node == null || !node.isObject()) return null; + JsonNode typeNode = node.get("type"); + ChannelType fromType = typeNode != null ? ChannelType.fromString(typeNode.asText()) : mc.type; + Class concreteClass = Channel.getConcreteClass(fromType); + if (concreteClass == null) { + throw new IllegalStateException("Unmapped class for type "+fromType); + } + return Jsonable.fromJson(node.toString(), concreteClass); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MemberInitiator.java b/src/main/java/com/vonage/client/conversations/MemberInitiator.java new file mode 100644 index 000000000..e24e67f4e --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MemberInitiator.java @@ -0,0 +1,73 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.*; +import com.vonage.client.JsonableBaseObject; + +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MemberInitiator extends JsonableBaseObject { + @JsonProperty("invited") private Invited invited; + @JsonProperty("joined") private Joined joined; + + protected MemberInitiator() { + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + private static class Invited extends JsonableBaseObject { + @JsonProperty("is_system") Boolean isSystem; + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + private static class Joined extends Invited { + @JsonProperty("user_id") String userId; + @JsonProperty("member_id") String memberId; + } + + /** + * Whether the member was invited by an admin JWT or a user. + * + * @return {@code true} if invited by admin JWT, + * {@code false} if invited by user (joined), {@code null} if unknown. + */ + @JsonIgnore + public Boolean invitedByAdmin() { + return invited != null ? invited.isSystem : joined != null ? joined.isSystem : null; + } + + /** + * If {@linkplain #invitedByAdmin()} is {@code false}, returns the ID of the inviting user. + * + * @return The user ID that invited this member, or {@code null} if not applicable. + */ + @JsonIgnore + public String getUserId() { + return joined != null ? joined.userId : null; + } + + /** + * If {@linkplain #invitedByAdmin()} is {@code false}, returns the ID of the inviting member. + * + * @return The member ID that sent the invite, or {@code null} if not applicable. + */ + @JsonIgnore + public String getMemberId() { + return joined != null ? joined.memberId : null; + } +} diff --git a/src/main/java/com/vonage/client/conversations/MemberMedia.java b/src/main/java/com/vonage/client/conversations/MemberMedia.java new file mode 100644 index 000000000..0edf9a957 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MemberMedia.java @@ -0,0 +1,139 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; + +/** + * Contains the media properties for {@link Member#getMedia()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class MemberMedia extends JsonableBaseObject { + private Boolean audio; + private MediaAudioSettings audioSettings; + + MemberMedia(Builder builder) { + audio = builder.audio; + audioSettings = builder.audioSettings; + } + + MemberMedia() { + } + + /** + * Whether the media has audio. + * + * @return {@code true} if audio is present, or {@code null} if unspecified. + */ + @JsonProperty("audio") + public Boolean getAudio() { + return audio; + } + + /** + * Audio-related properties. + * + * @return The audio settings object, or {@code null} if not applicable. + */ + @JsonProperty("audio_settings") + public MediaAudioSettings getAudioSettings() { + return audioSettings; + } + + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing MemberMedia. All fields are optional. + */ + public static final class Builder { + private Boolean audio; + private MediaAudioSettings audioSettings; + + private Builder() {} + + private MediaAudioSettings initAudioSettings() { + if (audioSettings == null) { + audioSettings = new MediaAudioSettings(); + } + return audioSettings; + } + + /** + * + * @param audio + * + * @return This builder. + */ + public Builder audio(boolean audio) { + this.audio = audio; + return this; + } + + /** + * + * @param enabled + * + * @return This builder. + */ + public Builder audioEnabled(boolean enabled) { + initAudioSettings().enabled = enabled; + return this; + } + + /** + * + * @param earmuffed + * + * @return This builder. + */ + public Builder earmuffed(boolean earmuffed) { + initAudioSettings().earmuffed = earmuffed; + return this; + } + + /** + * + * @param muted + * + * @return This builder. + */ + public Builder muted(boolean muted) { + initAudioSettings().muted = muted; + return this; + } + + /** + * Builds the {@linkplain MemberMedia}. + * + * @return An instance of MemberMedia, populated with all fields from this builder. + */ + public MemberMedia build() { + return new MemberMedia(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MemberState.java b/src/main/java/com/vonage/client/conversations/MemberState.java new file mode 100644 index 000000000..68f3476c5 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MemberState.java @@ -0,0 +1,38 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * Represents the state of a conversation member. + */ +public enum MemberState { + INVITED, + JOINED, + LEFT, + UNKNOWN; + + @JsonCreator + public static MemberState fromString(String name) { + try { + return valueOf(name.toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException ex) { + return null; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MemberTimestamp.java b/src/main/java/com/vonage/client/conversations/MemberTimestamp.java new file mode 100644 index 000000000..3a2427a8f --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MemberTimestamp.java @@ -0,0 +1,63 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.time.Instant; + +/** + * Represents the timestamps in {@link Member#getTimestamp()}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MemberTimestamp extends JsonableBaseObject { + private Instant invited, joined, left; + + protected MemberTimestamp() {} + + /** + * Time that the member was invited. + * + * @return The member invitation time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("invited") + public Instant getInvited() { + return invited; + } + + /** + * Time that the member joined. + * + * @return The member join time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("joined") + public Instant getJoined() { + return joined; + } + + /** + * Time that the member left. + * + * @return The member leave time as an Instant, or {@code null} if unknown. + */ + @JsonProperty("left") + public Instant getLeft() { + return left; + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageDeliveredEvent.java b/src/main/java/com/vonage/client/conversations/MessageDeliveredEvent.java new file mode 100644 index 000000000..5a69eed08 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageDeliveredEvent.java @@ -0,0 +1,52 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#MESSAGE_DELIVERED} event. + */ +public final class MessageDeliveredEvent extends MessageStatusEvent { + + MessageDeliveredEvent() {} + + MessageDeliveredEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static class Builder extends MessageStatusEvent.Builder { + + Builder() { + super(EventType.MESSAGE_DELIVERED); + } + + @Override + public MessageDeliveredEvent build() { + return new MessageDeliveredEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageEvent.java b/src/main/java/com/vonage/client/conversations/MessageEvent.java new file mode 100644 index 000000000..989217d82 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageEvent.java @@ -0,0 +1,149 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.vonage.client.common.MessageType; +import java.net.URI; + +/** + * Represents a {@link EventType#MESSAGE} event. All possible fields are presented and accessible, + * but only those applicable to the message type will be populated. Use {@linkplain #getMessageType()} + * to determine the type of message, and query the other fields accordingly. + */ +public final class MessageEvent extends EventWithBody { + + MessageEvent() {} + + MessageEvent(Builder builder) { + super(builder); + body = new MessageEventBody(builder); + } + + /** + * Describes the media type for this event. + * + * @return The message type as an enum. + */ + @JsonIgnore + public MessageType getMessageType() { + return body.messageType; + } + + /** + * If {@linkplain #getMessageType()} is {@linkplain MessageType#TEXT}, returns the text. + * + * @return The message text, or {@code null} if not applicable. + */ + @JsonIgnore + public String getText() { + return body.text; + } + + /** + * If {@linkplain #getMessageType()} is multimedia, returns the URL of the media. + * + * @return The absolute media URL, or {@code null} if not applicable. + */ + @JsonIgnore + public URI getUrl() { + switch (getMessageType()) { + default: return null; + case FILE: return body.file.url; + case IMAGE: return body.image.url; + case AUDIO: return body.audio.url; + case VIDEO: return body.video.url; + case VCARD: return body.vcard.url; + } + } + + /** + * If {@linkplain #getMessageType()} is {@linkplain MessageType#LOCATION}, returns the location. + * + * @return The location details, or {@code null} if not applicable. + */ + @JsonIgnore + public Location getLocation() { + return body.location; + } + + /** + * Entry point for constructing an instance of this class. + * + * @param messageType The type of message for this event. + * + * @return A new Builder. + */ + public static Builder builder(MessageType messageType) { + return new Builder(messageType); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static final class Builder extends EventWithBody.Builder { + final MessageType messageType; + String text; + URI url; + Location location; + + Builder(MessageType messageType) { + super(EventType.MESSAGE); + this.messageType = messageType; + } + + /** + * Sets the message text, if the type is {@linkplain MessageType#TEXT}. + * + * @param text The message text. + * + * @return This builder. + */ + public Builder text(String text) { + this.text = text; + return this; + } + + /** + * Sets the URL, if appropriate for the type. + * + * @param url The absolute media URL as a string. + * + * @return This builder. + */ + public Builder url(String url) { + this.url = URI.create(url); + return this; + } + + /** + * Sets the message location, if the type is {@linkplain MessageType#LOCATION}. + * + * @param location The location details. + * + * @return This builder. + */ + public Builder location(Location location) { + this.location = location; + return this; + } + + @Override + public MessageEvent build() { + return new MessageEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageEventBody.java b/src/main/java/com/vonage/client/conversations/MessageEventBody.java new file mode 100644 index 000000000..ada2e8fa8 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageEventBody.java @@ -0,0 +1,70 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.*; +import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.MessageType; +import java.net.URI; +import java.util.Objects; + +/** + * Main body container for {@link MessageEvent}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +class MessageEventBody extends JsonableBaseObject { + @JsonProperty("message_type") MessageType messageType; + @JsonProperty("text") String text; + @JsonProperty("image") UrlContainer image; + @JsonProperty("audio") UrlContainer audio; + @JsonProperty("video") UrlContainer video; + @JsonProperty("file") UrlContainer file; + @JsonProperty("vcard") UrlContainer vcard; + @JsonProperty("location") Location location; + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static class UrlContainer extends JsonableBaseObject { + @JsonProperty("url") URI url; + } + + MessageEventBody() {} + + MessageEventBody(MessageEvent.Builder builder) { + messageType = Objects.requireNonNull(builder.messageType, "Message type is required."); + if ((text = builder.text) != null && messageType != MessageType.TEXT) { + throw new IllegalStateException("Text is not applicable to '"+messageType+"'."); + } + if ((location = builder.location) != null && messageType != MessageType.LOCATION) { + throw new IllegalStateException("Location is not applicable to '"+messageType+"'."); + } + if (builder.url != null) { + UrlContainer urlRef = new UrlContainer(); + urlRef.url = builder.url; + switch (messageType) { + default: throw new IllegalStateException( + "URL is not applicable for '"+messageType+"'." + ); + case IMAGE: image = urlRef; break; + case AUDIO: audio = urlRef; break; + case VIDEO: video = urlRef; break; + case FILE: file = urlRef; break; + case VCARD: vcard = urlRef; break; + } + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageRejectedEvent.java b/src/main/java/com/vonage/client/conversations/MessageRejectedEvent.java new file mode 100644 index 000000000..5e3aef596 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageRejectedEvent.java @@ -0,0 +1,52 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#MESSAGE_REJECTED} event. + */ +public final class MessageRejectedEvent extends MessageStatusEvent { + + MessageRejectedEvent() {} + + MessageRejectedEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static class Builder extends MessageStatusEvent.Builder { + + Builder() { + super(EventType.MESSAGE_REJECTED); + } + + @Override + public MessageRejectedEvent build() { + return new MessageRejectedEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageSeenEvent.java b/src/main/java/com/vonage/client/conversations/MessageSeenEvent.java new file mode 100644 index 000000000..afa382883 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageSeenEvent.java @@ -0,0 +1,52 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#MESSAGE_SEEN} event. + */ +public final class MessageSeenEvent extends MessageStatusEvent { + + MessageSeenEvent() {} + + MessageSeenEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static class Builder extends MessageStatusEvent.Builder { + + Builder() { + super(EventType.MESSAGE_SEEN); + } + + @Override + public MessageSeenEvent build() { + return new MessageSeenEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageStatusEvent.java b/src/main/java/com/vonage/client/conversations/MessageStatusEvent.java new file mode 100644 index 000000000..d6f4fc862 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageStatusEvent.java @@ -0,0 +1,72 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; + +abstract class MessageStatusEvent extends EventWithBody { + + MessageStatusEvent() {} + + MessageStatusEvent(Builder builder) { + super(builder); + (body = new Body()).eventId = builder.eventId; + } + + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + static class Body extends JsonableBaseObject { + @JsonProperty("event_id") private Integer eventId; + } + + /** + * ID of the event. Note that this is the body for the event status update, + * so may be different from {@link #getId()}. + * + * @return The event ID pointer as an Integer. + */ + @JsonIgnore + public Integer getEventId() { + return body != null ? body.eventId : null; + } + + @SuppressWarnings("unchecked") + static abstract class Builder> + extends EventWithBody.Builder> { + + private Integer eventId; + + Builder(EventType type) { + super(type); + } + + /** + * ID of the event. + * + * @param eventId The event ID as an Integer. + * @return This builder. + */ + public B eventId(int eventId) { + this.eventId = eventId; + return (B) this; + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageSubmittedEvent.java b/src/main/java/com/vonage/client/conversations/MessageSubmittedEvent.java new file mode 100644 index 000000000..db4c71b68 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageSubmittedEvent.java @@ -0,0 +1,52 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#MESSAGE_SUBMITTED} event. + */ +public final class MessageSubmittedEvent extends MessageStatusEvent { + + MessageSubmittedEvent() {} + + MessageSubmittedEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static class Builder extends MessageStatusEvent.Builder { + + Builder() { + super(EventType.MESSAGE_SUBMITTED); + } + + @Override + public MessageSubmittedEvent build() { + return new MessageSubmittedEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/MessageUndeliverableEvent.java b/src/main/java/com/vonage/client/conversations/MessageUndeliverableEvent.java new file mode 100644 index 000000000..02ee97f08 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/MessageUndeliverableEvent.java @@ -0,0 +1,52 @@ +/* + * 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.conversations; + +/** + * Represents a {@link EventType#MESSAGE_UNDELIVERABLE} event. + */ +public final class MessageUndeliverableEvent extends MessageStatusEvent { + + MessageUndeliverableEvent() {} + + MessageUndeliverableEvent(Builder builder) { + super(builder); + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for configuring parameters of the event request. + */ + public static class Builder extends MessageStatusEvent.Builder { + + Builder() { + super(EventType.MESSAGE_UNDELIVERABLE); + } + + @Override + public MessageUndeliverableEvent build() { + return new MessageUndeliverableEvent(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/OrderBy.java b/src/main/java/com/vonage/client/conversations/OrderBy.java new file mode 100644 index 000000000..265107026 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/OrderBy.java @@ -0,0 +1,32 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Result ordering options for {@link ListUserConversationsRequest}. + */ +public enum OrderBy { + CREATED, + CUSTOM_SORT_KEY; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/vonage/client/conversations/UpdateMemberRequest.java b/src/main/java/com/vonage/client/conversations/UpdateMemberRequest.java new file mode 100644 index 000000000..06e3f10c2 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/UpdateMemberRequest.java @@ -0,0 +1,217 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.vonage.client.Jsonable; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.JsonableBaseObject; +import java.util.Objects; + +/** + * Options for updating a membership using {@link ConversationsClient#updateMember(UpdateMemberRequest)}. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class UpdateMemberRequest extends ConversationResourceRequestWrapper implements Jsonable { + private final MemberState state; + private final String from; + @JsonProperty("reason") private final Reason reason; + + UpdateMemberRequest(Builder builder) { + super(builder.conversationId, builder.memberId); + switch (state = Objects.requireNonNull(builder.state, "State is required.")) { + case JOINED: case LEFT: break; + default: throw new IllegalArgumentException("Invalid state: "+state); + } + if ((reason = builder.reason) != null && state != MemberState.LEFT) { + throw new IllegalStateException("Reason is only applicable when leaving."); + } + from = builder.from; + } + + static final class Reason extends JsonableBaseObject { + @JsonProperty("code") String code; + @JsonProperty("text") String text; + } + + /** + * State to transition the member into. + * + * @return The updated state as an enum. + */ + @JsonProperty("state") + public MemberState getState() { + return state; + } + + /** + * TODO document this. + * + * @return The from, or {@code null} if unspecified. + */ + @JsonProperty("from") + public String getFrom() { + return from; + } + + /** + * Reason code for leaving. Only applicable when {@linkplain #getState()} is {@linkplain MemberState#LEFT}. + * + * @return The reason code, or {@code null} if unspecified / not applicable. + */ + @JsonIgnore + public String getCode() { + return reason != null ? reason.code : null; + } + + /** + * Reason text for leaving. Only applicable when {@linkplain #getState()} is {@linkplain MemberState#LEFT}. + * + * @return The reason text, or {@code null} if unspecified / not applicable. + */ + @JsonIgnore + public String getText() { + return reason != null ? reason.text : null; + } + + /** + * Unique Conversation identifier. + * + * @return The conversation ID. + */ + @JsonIgnore + public String getConversationId() { + return conversationId; + } + + /** + * Unique Member identifier. + * + * @return The member ID. + */ + @JsonIgnore + public String getMemberId() { + return resourceId; + } + + /** + * Entry point for constructing an instance of this class. + * + * @return A new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for constructing an UpdateMemberRequest. Note the mandatory parameters. + */ + public static final class Builder { + private String conversationId, memberId, from; + private MemberState state; + private Reason reason; + + Builder() {} + + /** + * (REQUIRED) Unique conversation identifier. + * + * @param conversationId The conversation ID. + * + * @return This builder. + */ + public Builder conversationId(String conversationId) { + this.conversationId = conversationId; + return this; + } + + /** + * (REQUIRED) Unique member identifier. + * + * @param memberId The member ID. + * + * @return This builder. + */ + public Builder memberId(String memberId) { + this.memberId = memberId; + return this; + } + + /** + * (REQUIRED) State to transition the member into. + * + * @param state The updated state as an enum. + * + * @return This builder. + */ + public Builder state(MemberState state) { + this.state = state; + return this; + } + + /** + * TODO document this + * + * @param from The from (??) + * + * @return This builder. + */ + public Builder from(String from) { + this.from = from; + return this; + } + + /** + * Reason code for leaving. + * Only applicable when {@linkplain #state(MemberState)} is {@linkplain MemberState#LEFT}. + * + * @param code The reason code as a string. + * + * @return This builder. + */ + public Builder code(String code) { + if (reason == null) reason = new Reason(); + reason.code = code; + return this; + } + + /** + * Reason text for leaving. + * Only applicable when {@linkplain #state(MemberState)} is {@linkplain MemberState#LEFT}. + * + * @param text The reason text. + * + * @return This builder. + */ + public Builder text(String text) { + if (reason == null) reason = new Reason(); + reason.text = text; + return this; + } + + /** + * Builds the {@linkplain UpdateMemberRequest}. + * + * @return An instance of UpdateMemberRequest, populated with all fields from this builder. + */ + public UpdateMemberRequest build() { + return new UpdateMemberRequest(this); + } + } +} diff --git a/src/main/java/com/vonage/client/conversations/UserConversation.java b/src/main/java/com/vonage/client/conversations/UserConversation.java new file mode 100644 index 000000000..0b45f0611 --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/UserConversation.java @@ -0,0 +1,41 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response object when querying a user's conversations. + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class UserConversation extends Conversation { + private BaseMember member; + + protected UserConversation() {} + + /** + * Basic metadata about the conversation membership. + * + * @return The embedded member response, or {@code null} if absent. + */ + @JsonProperty("_embedded") + public BaseMember getMember() { + return member; + } +} diff --git a/src/main/java/com/vonage/client/conversations/package-info.java b/src/main/java/com/vonage/client/conversations/package-info.java new file mode 100644 index 000000000..31914837f --- /dev/null +++ b/src/main/java/com/vonage/client/conversations/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +/** + * Implementation of the Conversation API. + * + * @since 8.4.0 + */ +package com.vonage.client.conversations; \ No newline at end of file diff --git a/src/main/java/com/vonage/client/meetings/ListRecordingsResponse.java b/src/main/java/com/vonage/client/meetings/ListRecordingsResponse.java index 0e40b73b7..36ce84e85 100644 --- a/src/main/java/com/vonage/client/meetings/ListRecordingsResponse.java +++ b/src/main/java/com/vonage/client/meetings/ListRecordingsResponse.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.vonage.client.Jsonable; import com.vonage.client.JsonableBaseObject; import java.util.List; @@ -34,8 +33,4 @@ static class Embedded extends JsonableBaseObject { public List getRecordings() { return embedded != null ? embedded.recordings : null; } - - public static ListRecordingsResponse fromJson(String json) { - return Jsonable.fromJson(json); - } } diff --git a/src/main/java/com/vonage/client/meetings/ListRoomsRequest.java b/src/main/java/com/vonage/client/meetings/ListRoomsRequest.java index 01a1c400f..82464f069 100644 --- a/src/main/java/com/vonage/client/meetings/ListRoomsRequest.java +++ b/src/main/java/com/vonage/client/meetings/ListRoomsRequest.java @@ -15,16 +15,16 @@ */ package com.vonage.client.meetings; -import com.vonage.client.QueryParamsRequest; -import java.util.LinkedHashMap; +import com.vonage.client.common.HalFilterRequest; import java.util.Map; import java.util.UUID; -class ListRoomsRequest implements QueryParamsRequest { +class ListRoomsRequest extends HalFilterRequest { final UUID themeId; final Integer pageSize, startId, endId; ListRoomsRequest(Integer startId, Integer endId, Integer pageSize, UUID themeId) { + super(null, pageSize, null); this.themeId = themeId; this.startId = startId; this.endId = endId; @@ -33,16 +33,13 @@ class ListRoomsRequest implements QueryParamsRequest { @Override public Map makeParams() { - Map params = new LinkedHashMap<>(4); + Map params = super.makeParams(); if (startId != null) { params.put("start_id", String.valueOf(startId)); } if (endId != null) { params.put("end_id", String.valueOf(endId)); } - if (pageSize != null) { - params.put("page_size", String.valueOf(pageSize)); - } return params; } } diff --git a/src/main/java/com/vonage/client/meetings/ListRoomsResponse.java b/src/main/java/com/vonage/client/meetings/ListRoomsResponse.java index 2f577f14e..0aa97b8c8 100644 --- a/src/main/java/com/vonage/client/meetings/ListRoomsResponse.java +++ b/src/main/java/com/vonage/client/meetings/ListRoomsResponse.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.vonage.client.Jsonable; import com.vonage.client.common.HalPageResponse; import java.util.List; @@ -36,14 +35,4 @@ protected ListRoomsResponse() { public List getMeetingRooms() { return rooms; } - - /** - * Creates an instance of this class from a JSON payload. - * - * @param json The JSON string to parse. - * @return An instance of this class with the fields populated, if present. - */ - public static ListRoomsResponse fromJson(String json) { - return Jsonable.fromJson(json); - } } diff --git a/src/main/java/com/vonage/client/messages/MessageType.java b/src/main/java/com/vonage/client/messages/MessageType.java index 9eb2686a0..2aa0fd571 100644 --- a/src/main/java/com/vonage/client/messages/MessageType.java +++ b/src/main/java/com/vonage/client/messages/MessageType.java @@ -20,7 +20,10 @@ /** * Represents the media type of the message. + * + * @deprecated Will be replaced by {@link com.vonage.client.common.MessageType} in the next major release. */ +@Deprecated public enum MessageType { TEXT, IMAGE, AUDIO, VIDEO, FILE, VCARD, TEMPLATE, CUSTOM, LOCATION, STICKER, UNSUPPORTED, REPLY, ORDER; diff --git a/src/main/java/com/vonage/client/messages/whatsapp/Location.java b/src/main/java/com/vonage/client/messages/whatsapp/Location.java index 279fc6031..dce1824dd 100644 --- a/src/main/java/com/vonage/client/messages/whatsapp/Location.java +++ b/src/main/java/com/vonage/client/messages/whatsapp/Location.java @@ -21,6 +21,8 @@ import com.vonage.client.JsonableBaseObject; /** + * Whatsapp Location message parameters. + * * @since 7.2.0 */ @JsonInclude(value = JsonInclude.Include.NON_NULL) diff --git a/src/main/java/com/vonage/client/messages/whatsapp/Template.java b/src/main/java/com/vonage/client/messages/whatsapp/Template.java index 0aee7040e..23b86d2fe 100644 --- a/src/main/java/com/vonage/client/messages/whatsapp/Template.java +++ b/src/main/java/com/vonage/client/messages/whatsapp/Template.java @@ -15,6 +15,7 @@ */ package com.vonage.client.messages.whatsapp; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.JsonableBaseObject; @@ -22,6 +23,7 @@ import java.util.Objects; @JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) public final class Template extends JsonableBaseObject { private final String name; private final List parameters; diff --git a/src/main/java/com/vonage/client/messages/whatsapp/Whatsapp.java b/src/main/java/com/vonage/client/messages/whatsapp/Whatsapp.java index fc0c364c1..d4ac9f9f0 100644 --- a/src/main/java/com/vonage/client/messages/whatsapp/Whatsapp.java +++ b/src/main/java/com/vonage/client/messages/whatsapp/Whatsapp.java @@ -15,6 +15,7 @@ */ package com.vonage.client.messages.whatsapp; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.JsonableBaseObject; @@ -22,6 +23,7 @@ import java.util.Objects; @JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) public final class Whatsapp extends JsonableBaseObject { private final Policy policy; private final Locale locale; diff --git a/src/main/java/com/vonage/client/proactiveconnect/HalRequestWrapper.java b/src/main/java/com/vonage/client/proactiveconnect/HalRequestWrapper.java index 69336e805..4e4cd8e7e 100644 --- a/src/main/java/com/vonage/client/proactiveconnect/HalRequestWrapper.java +++ b/src/main/java/com/vonage/client/proactiveconnect/HalRequestWrapper.java @@ -15,34 +15,13 @@ */ package com.vonage.client.proactiveconnect; -import com.vonage.client.QueryParamsRequest; -import java.util.LinkedHashMap; -import java.util.Map; +import com.vonage.client.common.HalFilterRequest; -class HalRequestWrapper implements QueryParamsRequest { - final Integer page, pageSize; - final SortOrder order; +class HalRequestWrapper extends HalFilterRequest { final String id; - HalRequestWrapper(Integer page, Integer pageSize, SortOrder order, String id) { - this.page = page; - this.pageSize = pageSize; - this.order = order; + HalRequestWrapper(Integer page, Integer pageSize, com.vonage.client.common.SortOrder order, String id) { + super(page, pageSize, order); this.id = id; } - - @Override - public Map makeParams() { - Map params = new LinkedHashMap<>(4); - if (page != null) { - params.put("page", page.toString()); - } - if (pageSize != null) { - params.put("page_size", pageSize.toString()); - } - if (order != null) { - params.put("order", order.toString()); - } - return params; - } } diff --git a/src/main/java/com/vonage/client/proactiveconnect/ListEventsResponse.java b/src/main/java/com/vonage/client/proactiveconnect/ListEventsResponse.java index aa537cf30..860c84a83 100644 --- a/src/main/java/com/vonage/client/proactiveconnect/ListEventsResponse.java +++ b/src/main/java/com/vonage/client/proactiveconnect/ListEventsResponse.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.vonage.client.Jsonable; import com.vonage.client.JsonableBaseObject; import com.vonage.client.common.HalPageResponse; import java.util.List; @@ -50,14 +49,4 @@ static final class Embedded extends JsonableBaseObject { public List getEvents() { return _embedded != null ? _embedded.events : null; } - - /** - * Creates an instance of this class from a JSON payload. - * - * @param json The JSON string to parse. - * @return An instance of this class with the fields populated, if present. - */ - public static ListEventsResponse fromJson(String json) { - return Jsonable.fromJson(json); - } } diff --git a/src/main/java/com/vonage/client/proactiveconnect/ProactiveConnectClient.java b/src/main/java/com/vonage/client/proactiveconnect/ProactiveConnectClient.java index 682baaa34..a7b5afa30 100644 --- a/src/main/java/com/vonage/client/proactiveconnect/ProactiveConnectClient.java +++ b/src/main/java/com/vonage/client/proactiveconnect/ProactiveConnectClient.java @@ -93,7 +93,7 @@ private R halRequest(RestEndpoint customData; - User() { + protected User() { } - User(Builder builder) { + protected User(Builder builder) { name = builder.name; displayName = builder.displayName; imageUrl = builder.imageUrl; @@ -58,7 +59,6 @@ public class User extends BaseUser { * * @return The user's friendly name. */ - @JsonProperty("display_name") public String getDisplayName() { return displayName; } @@ -68,21 +68,10 @@ public String getDisplayName() { * * @return The image URL, or {@code null} if not specified. */ - @JsonProperty("image_url") public URI getImageUrl() { return imageUrl; } - /** - * Additional properties for the user. - * - * @return The properties object, or {@code null} if not applicable. - */ - @JsonProperty("properties") - private Properties getProperties() { - return properties; - } - /** * Custom key/value pairs to associate with the user. * @@ -90,7 +79,7 @@ private Properties getProperties() { */ @JsonIgnore public Map getCustomData() { - return getProperties() != null ? properties.getCustomData() : null; + return properties != null ? properties.customData : customData; } /** @@ -98,7 +87,6 @@ private Properties getProperties() { * * @return The channels object, or {@code null} if unknown. */ - @JsonProperty("channels") public Channels getChannels() { return channels; } @@ -217,17 +205,8 @@ public User build() { * Represents the "properties" field of a User object. */ @JsonInclude(value = JsonInclude.Include.NON_NULL) - public static class Properties extends JsonableBaseObject { - private Map customData; - - /** - * Custom key/value pairs to associate with the user. - * - * @return The custom data as a Map, or {@code null} if not specified. - */ - @JsonProperty("custom_data") - public Map getCustomData() { - return customData; - } + @JsonIgnoreProperties(ignoreUnknown = true) + static class Properties extends JsonableBaseObject { + @JsonProperty("custom_data") Map customData; } } diff --git a/src/main/java/com/vonage/client/users/UsersClient.java b/src/main/java/com/vonage/client/users/UsersClient.java index b326dd890..2980df1ca 100644 --- a/src/main/java/com/vonage/client/users/UsersClient.java +++ b/src/main/java/com/vonage/client/users/UsersClient.java @@ -101,7 +101,7 @@ public User createUser(User user) throws UsersResponseException { * @throws UsersResponseException If there was an error processing the request. */ public User updateUser(String userId, User user) throws UsersResponseException { - validateUser(user).setId(validateUserId(userId)); + validateUser(user).id = validateUserId(userId); return updateUser.execute(user); } diff --git a/src/main/java/com/vonage/client/users/channels/Channel.java b/src/main/java/com/vonage/client/users/channels/Channel.java index bd22d9ab1..b4ed8063d 100644 --- a/src/main/java/com/vonage/client/users/channels/Channel.java +++ b/src/main/java/com/vonage/client/users/channels/Channel.java @@ -15,10 +15,102 @@ */ package com.vonage.client.users.channels; +import com.fasterxml.jackson.annotation.*; import com.vonage.client.JsonableBaseObject; +import com.vonage.client.common.ChannelType; /** * Base class for channels. */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Messenger.class, name = "messenger"), + @JsonSubTypes.Type(value = Mms.class, name = "mms"), + @JsonSubTypes.Type(value = Pstn.class, names = {"phone", "pstn"}), + @JsonSubTypes.Type(value = Sip.class, name = "sip"), + @JsonSubTypes.Type(value = Sms.class, name = "sms"), + @JsonSubTypes.Type(value = Vbc.class, name = "vbc"), + @JsonSubTypes.Type(value = Viber.class, name = "viber"), + @JsonSubTypes.Type(value = Websocket.class, name = "websocket"), + @JsonSubTypes.Type(value = Whatsapp.class, name = "whatsapp"), + @JsonSubTypes.Type(value = WhatsappVoice.class, name = "whatsapp-voice") +}) +@JsonInclude(value = JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) public abstract class Channel extends JsonableBaseObject { + protected ChannelType type; + + /** + * If the {@code type} field is present in JSON payload, returns the value as an enum. + * Usually however, the type can be inferred from the class name. + * This method is provided for completeness in cases where the field may be present. + * + * @return The channel type as an enum, or {@code null} if absent or not applicable in this context. + * + * @since 8.4.0 + */ + @JsonProperty("type") + public ChannelType getType() { + return type; + } + + /** + * Sets the {@link #getType()} based on this class's type. This is useful for some API + * calls where the type information is expected to be present in the JSON. + * + * @since 8.4.0 + * @see #removeTypeField() + */ + @JsonIgnore + public void setTypeField() { + String name = getClass().getSimpleName(); + if (name.equals("Pstn")) { + name = "Phone"; + } + if (name.equals("WhatsappVoice")) { + name = "whatsapp-voice"; + } + type = ChannelType.fromString(name); + } + + /** + * This method makes {@link #getType()} return {@code null}; effectively + * the opposite of {@linkplain #setTypeField()}. This is useful for some API calls + * where the type information should be omitted from the generated JSON. + * + * @since 8.4.0 + * @see #setTypeField() + */ + public void removeTypeField() { + type = null; + } + + /** + * Finds the corresponding subclass of Channel based on the enum's value. + * + * @param type The channel type as an enum. + * + * @return The appropriate concrete Channel class, or {@code null} if there isn't one. + * @since 8.4.0 + */ + public static Class getConcreteClass(ChannelType type) { + if (type == null) return null; + switch (type) { + default: return null; + case MESSENGER: return Messenger.class; + case MMS: return Mms.class; + case PHONE: return Pstn.class; + case SIP: return Sip.class; + case SMS: return Sms.class; + case VBC: return Vbc.class; + case VIBER: return Viber.class; + case WEBSOCKET: return Websocket.class; + case WHATSAPP: return Whatsapp.class; + case WHATSAPP_VOICE: return WhatsappVoice.class; + } + } } diff --git a/src/main/java/com/vonage/client/users/channels/Messenger.java b/src/main/java/com/vonage/client/users/channels/Messenger.java index c676426ea..1a0b88746 100644 --- a/src/main/java/com/vonage/client/users/channels/Messenger.java +++ b/src/main/java/com/vonage/client/users/channels/Messenger.java @@ -18,10 +18,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Facebook Messenger channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Messenger extends Channel { diff --git a/src/main/java/com/vonage/client/users/channels/Mms.java b/src/main/java/com/vonage/client/users/channels/Mms.java index 4e7df0159..ae8657219 100644 --- a/src/main/java/com/vonage/client/users/channels/Mms.java +++ b/src/main/java/com/vonage/client/users/channels/Mms.java @@ -17,10 +17,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Multimedia Messaging Service (MMS) channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Mms extends NumberChannel { diff --git a/src/main/java/com/vonage/client/users/channels/Pstn.java b/src/main/java/com/vonage/client/users/channels/Pstn.java index 186981774..4544cecd3 100644 --- a/src/main/java/com/vonage/client/users/channels/Pstn.java +++ b/src/main/java/com/vonage/client/users/channels/Pstn.java @@ -17,10 +17,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Public Switched Telephone Network (PSTN) channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Pstn extends NumberChannel { @@ -28,7 +30,7 @@ public class Pstn extends NumberChannel { protected Pstn() {} /** - * Creates a new PSTN channel. + * Creates a new PSTN (phone) channel. * * @param number The telephone number in E.164 format. */ diff --git a/src/main/java/com/vonage/client/users/channels/Sip.java b/src/main/java/com/vonage/client/users/channels/Sip.java index 3a02986f5..27c888762 100644 --- a/src/main/java/com/vonage/client/users/channels/Sip.java +++ b/src/main/java/com/vonage/client/users/channels/Sip.java @@ -18,12 +18,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import java.net.URI; import java.util.Objects; /** * Represents a Session Initiation Protocol (SIP) channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Sip extends Channel { diff --git a/src/main/java/com/vonage/client/users/channels/Sms.java b/src/main/java/com/vonage/client/users/channels/Sms.java index 884cdf674..05eaea738 100644 --- a/src/main/java/com/vonage/client/users/channels/Sms.java +++ b/src/main/java/com/vonage/client/users/channels/Sms.java @@ -17,10 +17,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Short Messaging Service (SMS) channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Sms extends NumberChannel { diff --git a/src/main/java/com/vonage/client/users/channels/Vbc.java b/src/main/java/com/vonage/client/users/channels/Vbc.java index 5217dce7a..d20fe3bc8 100644 --- a/src/main/java/com/vonage/client/users/channels/Vbc.java +++ b/src/main/java/com/vonage/client/users/channels/Vbc.java @@ -18,10 +18,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Vonage Business Cloud (VBC) channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Vbc extends Channel { diff --git a/src/main/java/com/vonage/client/users/channels/Viber.java b/src/main/java/com/vonage/client/users/channels/Viber.java index 1a3f1cad2..ece192350 100644 --- a/src/main/java/com/vonage/client/users/channels/Viber.java +++ b/src/main/java/com/vonage/client/users/channels/Viber.java @@ -17,10 +17,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a Viber channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Viber extends NumberChannel { diff --git a/src/main/java/com/vonage/client/users/channels/Websocket.java b/src/main/java/com/vonage/client/users/channels/Websocket.java index 90ab927b1..326ac6d11 100644 --- a/src/main/java/com/vonage/client/users/channels/Websocket.java +++ b/src/main/java/com/vonage/client/users/channels/Websocket.java @@ -23,6 +23,7 @@ /** * Represents a Websocket channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Websocket extends Channel { diff --git a/src/main/java/com/vonage/client/users/channels/Whatsapp.java b/src/main/java/com/vonage/client/users/channels/Whatsapp.java index 9e996e73b..f76f46a95 100644 --- a/src/main/java/com/vonage/client/users/channels/Whatsapp.java +++ b/src/main/java/com/vonage/client/users/channels/Whatsapp.java @@ -17,10 +17,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a WhatsApp channel. */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Whatsapp extends NumberChannel { @@ -28,7 +30,7 @@ public class Whatsapp extends NumberChannel { protected Whatsapp() {} /** - * Creates a new Whatsapp channel. + * Creates a new WhatsApp channel. * * @param number The phone number in E.164 format. */ diff --git a/src/main/java/com/vonage/client/users/channels/WhatsappVoice.java b/src/main/java/com/vonage/client/users/channels/WhatsappVoice.java new file mode 100644 index 000000000..b28bba88a --- /dev/null +++ b/src/main/java/com/vonage/client/users/channels/WhatsappVoice.java @@ -0,0 +1,42 @@ +/* + * 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.users.channels; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Represents a WhatsApp Voice channel. + * + * @since 8.4.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, visible = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class WhatsappVoice extends NumberChannel { + + protected WhatsappVoice() {} + + /** + * Creates a new WhatsApp Voice channel. + * + * @param number The phone number in E.164 format. + */ + public WhatsappVoice(String number) { + super(number); + } +} diff --git a/src/main/java/com/vonage/client/verify/CheckResponse.java b/src/main/java/com/vonage/client/verify/CheckResponse.java index f639213a1..7db85931b 100644 --- a/src/main/java/com/vonage/client/verify/CheckResponse.java +++ b/src/main/java/com/vonage/client/verify/CheckResponse.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.Jsonable; import com.vonage.client.JsonableBaseObject; import com.vonage.client.VonageResponseParseException; import java.math.BigDecimal; @@ -100,6 +101,14 @@ public BigDecimal getEstimatedPriceMessagesSent() { return estimatedPriceMessagesSent; } + @Override + public void updateFromJson(String json) { + super.updateFromJson(json); + if (status == null) { + throw new VonageResponseParseException("Response status is missing."); + } + } + /** * Constructs a CheckResponse with the fields populated from the JSON payload. * @@ -108,11 +117,6 @@ public BigDecimal getEstimatedPriceMessagesSent() { * @return A new instance of this class. */ public static CheckResponse fromJson(String json) { - CheckResponse response = new CheckResponse(); - response.updateFromJson(json); - if (response.status == null) { - throw new VonageResponseParseException("Response status is missing."); - } - return response; + return Jsonable.fromJson(json); } } diff --git a/src/main/java/com/vonage/client/video/CreateSessionResponse.java b/src/main/java/com/vonage/client/video/CreateSessionResponse.java index 4a7aa7b5f..d6b7d333d 100644 --- a/src/main/java/com/vonage/client/video/CreateSessionResponse.java +++ b/src/main/java/com/vonage/client/video/CreateSessionResponse.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.vonage.client.Jsonable; import com.vonage.client.JsonableBaseObject; import com.vonage.client.VonageResponseParseException; import java.io.IOException; @@ -75,8 +75,8 @@ public URI getMediaServerUrl() { */ public static CreateSessionResponse fromJson(String json) { try { - ObjectMapper mapper = new ObjectMapper(); - CreateSessionResponse[] array = mapper.readValue(json, CreateSessionResponse[].class); + CreateSessionResponse[] array = Jsonable.createDefaultObjectMapper() + .readValue(json, CreateSessionResponse[].class); if (array == null || array.length == 0) { return new CreateSessionResponse(); } diff --git a/src/main/java/com/vonage/client/video/VideoClient.java b/src/main/java/com/vonage/client/video/VideoClient.java index 7aa77d7b5..970529420 100644 --- a/src/main/java/com/vonage/client/video/VideoClient.java +++ b/src/main/java/com/vonage/client/video/VideoClient.java @@ -37,7 +37,7 @@ public class VideoClient { final Supplier newJwtSupplier; - final RestEndpoint createSession; + final RestEndpoint createSession; final RestEndpoint listStreams; final RestEndpoint getStream; final RestEndpoint setStreamLayout; @@ -201,7 +201,8 @@ public CreateSessionResponse createSession() { * @return Details of the created session. */ public CreateSessionResponse createSession(CreateSessionRequest request) { - return createSession.execute(request); + CreateSessionResponse[] response = createSession.execute(request); + return response == null || response.length == 0 ? new CreateSessionResponse() : response[0]; } /** diff --git a/src/main/java/com/vonage/client/voice/CallOrder.java b/src/main/java/com/vonage/client/voice/CallOrder.java index 57cb4d2d4..fa355b1d9 100644 --- a/src/main/java/com/vonage/client/voice/CallOrder.java +++ b/src/main/java/com/vonage/client/voice/CallOrder.java @@ -15,6 +15,10 @@ */ package com.vonage.client.voice; +/** + * @deprecated Replaced by {@link com.vonage.client.common.SortOrder}. + */ +@Deprecated public enum CallOrder { ASCENDING("asc"), DESCENDING("desc"); diff --git a/src/main/java/com/vonage/client/voice/Endpoint.java b/src/main/java/com/vonage/client/voice/Endpoint.java index 6c595b828..185b378f0 100644 --- a/src/main/java/com/vonage/client/voice/Endpoint.java +++ b/src/main/java/com/vonage/client/voice/Endpoint.java @@ -43,6 +43,8 @@ public interface Endpoint { * Description of the endpoint. * * @return String representation of the object. + * @deprecated This method will be removed in the next major release. */ + @Deprecated String toLog(); } diff --git a/src/test/java/com/vonage/client/ClientTest.java b/src/test/java/com/vonage/client/ClientTest.java index 9e2edf4fb..d06b830ec 100644 --- a/src/test/java/com/vonage/client/ClientTest.java +++ b/src/test/java/com/vonage/client/ClientTest.java @@ -34,17 +34,18 @@ import java.util.function.Supplier; public abstract class ClientTest { - protected final String applicationId = UUID.randomUUID().toString(); - protected final String apiKey = "a1b2c3d4"; - protected final String apiSecret = "1234567890abcdef"; - protected final String testReason = "Test reason"; + protected static final String + APPLICATION_ID = UUID.randomUUID().toString(), + API_KEY = "a1b2c3d4", API_SECRET = "1234567890abcdef", + TEST_REASON = "Test reason"; + protected HttpWrapper wrapper; protected T client; protected ClientTest() { wrapper = new HttpWrapper( - new TokenAuthMethod(apiKey, apiSecret), - new JWTAuthMethod(applicationId, new byte[0]) + new TokenAuthMethod(API_KEY, API_SECRET), + new JWTAuthMethod(APPLICATION_ID, new byte[0]) ); } @@ -64,7 +65,7 @@ protected HttpClient stubHttpClient(int statusCode, String content, String... ad InputStream[] contentsEncoded = Arrays.stream(additionalReturns).map(transformation).toArray(InputStream[]::new); when(entity.getContent()).thenReturn(transformation.apply(content), contentsEncoded); when(sl.getStatusCode()).thenReturn(statusCode); - when(sl.getReasonPhrase()).thenReturn(testReason); + when(sl.getReasonPhrase()).thenReturn(TEST_REASON); when(response.getStatusLine()).thenReturn(sl); when(response.getEntity()).thenReturn(entity); @@ -107,7 +108,11 @@ protected void stubResponseAndAssertThrows(int statusCode, String response, Exec } protected void stubResponseAndRun(String responseJson, Runnable invocation) throws Exception { - stubResponse(200, responseJson); + stubResponseAndRun(200, responseJson, invocation); + } + + protected void stubResponseAndRun(int statusCode, String responseJson, Runnable invocation) throws Exception { + stubResponse(statusCode, responseJson); invocation.run(); } @@ -154,10 +159,10 @@ protected E assertApiResponseException( catch (Throwable ex) { assertEquals(exClass, ex.getClass(), failPrefix + ex.getClass()); if (expectedResponse.getTitle() == null) { - expectedResponse.title = testReason; + expectedResponse.title = TEST_REASON; } assertEquals(expectedResponse, ex); - String actualJson = ((E) ex).toJson().replace("\"title\":\""+testReason+"\",", ""); + String actualJson = ((E) ex).toJson().replace("\"title\":\""+ TEST_REASON +"\",", ""); assertEquals(expectedJson, actualJson); } return expectedResponse; diff --git a/src/test/java/com/vonage/client/OrderedJsonMap.java b/src/test/java/com/vonage/client/OrderedJsonMap.java new file mode 100644 index 000000000..2236af179 --- /dev/null +++ b/src/test/java/com/vonage/client/OrderedJsonMap.java @@ -0,0 +1,50 @@ +/* + * 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; + +import java.util.LinkedHashMap; + +import java.util.Map; +import java.util.Map.Entry; + +public class OrderedJsonMap extends LinkedHashMap { + + public OrderedJsonMap() { + super(); + } + + @SafeVarargs + public OrderedJsonMap(Entry... entries) { + super(entries.length); + for (var entry : entries) { + put(entry.getKey(), entry.getValue()); + } + } + + public OrderedJsonMap add(String key, Object value) { + put(key, value); + return this; + } + + public OrderedJsonMap addAll(Map other) { + putAll(other); + return this; + } + + public static Entry entry(String key, Object value) { + return new SimpleEntry<>(key, value); + } +} diff --git a/src/test/java/com/vonage/client/TestUtils.java b/src/test/java/com/vonage/client/TestUtils.java index 88ed1a92e..c3cbbe5c3 100644 --- a/src/test/java/com/vonage/client/TestUtils.java +++ b/src/test/java/com/vonage/client/TestUtils.java @@ -120,4 +120,14 @@ public static void testJsonableBaseObject(T parse assertEquals(toString, clazz.getSimpleName()+' '+toJson); } } + + public static String mapToJson(Map expected) { + try { + return Jsonable.createDefaultObjectMapper().writeValueAsString(expected); + } + catch (JsonProcessingException ex) { + fail(ex); + return null; + } + } } diff --git a/src/test/java/com/vonage/client/VonageClientTest.java b/src/test/java/com/vonage/client/VonageClientTest.java index e90ba5c45..8f5d46821 100644 --- a/src/test/java/com/vonage/client/VonageClientTest.java +++ b/src/test/java/com/vonage/client/VonageClientTest.java @@ -337,6 +337,7 @@ public void testSubClientGetters() { VonageClient client = VonageClient.builder().build(); assertNotNull(client.getAccountClient()); assertNotNull(client.getApplicationClient()); + assertNotNull(client.getConversationsClient()); assertNotNull(client.getConversionClient()); assertNotNull(client.getVoiceClient()); assertNotNull(client.getInsightClient()); diff --git a/src/test/java/com/vonage/client/account/AccountClientTest.java b/src/test/java/com/vonage/client/account/AccountClientTest.java index d78190811..9109680a9 100644 --- a/src/test/java/com/vonage/client/account/AccountClientTest.java +++ b/src/test/java/com/vonage/client/account/AccountClientTest.java @@ -377,7 +377,7 @@ public void testListSecretSuccessful() throws Exception { @Test public void testParseListSecretsMalformed() throws Exception { stubResponse(200, "{malformed]"); - assertThrows(VonageResponseParseException.class, () -> client.listSecrets(apiKey)); + assertThrows(VonageResponseParseException.class, () -> client.listSecrets(API_KEY)); } @Test @@ -437,7 +437,7 @@ public void testCreateSecretFailed() throws Exception { + " \"reason\": \"Does not meet complexity requirements\"\n" + " }\n" + " ],\n" + " \"instance\": \"797a8f199c45014ab7b08bfe9cc1c12c\"\n" + "}"; stubResponse(400, json); - assertThrows(AccountResponseException.class, () -> client.createSecret(apiKey, "secret")); + assertThrows(AccountResponseException.class, () -> client.createSecret(API_KEY, "secret")); } @Test @@ -459,13 +459,13 @@ public void testGetSecretSuccessful() throws Exception { @Test public void testGetSecretMalformed() throws Exception { stubResponse(200, "{malformed]"); - assertThrows(VonageResponseParseException.class, () -> client.getSecret(apiKey, SECRET_ID)); + assertThrows(VonageResponseParseException.class, () -> client.getSecret(API_KEY, SECRET_ID)); } @Test public void testGetSecretNoSecretId() throws Exception { stubResponse(200, "{}"); - assertThrows(IllegalArgumentException.class, () -> client.getSecret(apiKey, " ")); + assertThrows(IllegalArgumentException.class, () -> client.getSecret(API_KEY, " ")); } @Test @@ -499,7 +499,7 @@ public void testRevokeSecretFailed() throws Exception { + " \"detail\": \"Can not delete the last secret. The account must always have at least 1 secret active at any time\",\n" + " \"instance\": \"797a8f199c45014ab7b08bfe9cc1c12c\"\n" + "}"; stubResponse(403, json); - assertThrows(AccountResponseException.class, () -> client.revokeSecret(apiKey, SECRET_ID)); + assertThrows(AccountResponseException.class, () -> client.revokeSecret(API_KEY, SECRET_ID)); } @Test @@ -785,7 +785,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected CreateSecretRequest sampleRequest() { - return new CreateSecretRequest(apiKey, SECRET_ID); + return new CreateSecretRequest(API_KEY, SECRET_ID); } } .runTests(); @@ -802,7 +802,7 @@ protected RestEndpoint endpoint() { @Override protected SecretRequest sampleRequest() { - return new SecretRequest(apiKey, SECRET_ID); + return new SecretRequest(API_KEY, SECRET_ID); } } .runTests(); @@ -819,7 +819,7 @@ protected RestEndpoint endpoint() { @Override protected String sampleRequest() { - return apiKey; + return API_KEY; } } .runTests(); @@ -841,7 +841,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected SecretRequest sampleRequest() { - return new SecretRequest(apiKey, SECRET_ID); + return new SecretRequest(API_KEY, SECRET_ID); } } .runTests(); diff --git a/src/test/java/com/vonage/client/conversations/AbstractEventTest.java b/src/test/java/com/vonage/client/conversations/AbstractEventTest.java new file mode 100644 index 000000000..047b43acd --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AbstractEventTest.java @@ -0,0 +1,59 @@ +/* + * 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.conversations; + +import com.vonage.client.Jsonable; +import com.vonage.client.TestUtils; +import static com.vonage.client.TestUtils.testJsonableBaseObject; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Map; +import java.util.UUID; + +abstract class AbstractEventTest { + final int randomEventId = ConversationsClientTest.EVENT_ID; + final UUID randomId = UUID.randomUUID(); + final String randomIdStr = randomId.toString(), from = STR."MEM-\{UUID.randomUUID()}"; + + , B extends EventWithBody.Builder> B applyBaseFields(B builder) { + return builder.from(from); + } + + , B extends EventWithBody.Builder> E testBaseEvent( + EventType eventType, B builder, Map bodyFields) { + + var event = applyBaseFields(builder).build(); + testJsonableBaseObject(event); + assertEquals(eventType, event.getType()); + assertEquals(from, event.getFrom()); + var json = event.toJson(); + assertTrue(json.contains("\"from\":\""+from+"\"")); + if (bodyFields != null && !bodyFields.isEmpty()) { + var bodyPartialJson = "\"body\":" + TestUtils.mapToJson(bodyFields); + assertTrue(json.contains(bodyPartialJson)); + } + return event; + } + + @SuppressWarnings("unchecked") + > E parseEvent(EventType eventType, Class expectedClass, String json) { + Event event = Jsonable.fromJson(json); + testJsonableBaseObject(event); + assertEquals(eventType, event.getType()); + assertEquals(expectedClass, event.getClass()); + return (E) event; + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioOutEventTest.java b/src/test/java/com/vonage/client/conversations/AudioOutEventTest.java new file mode 100644 index 000000000..09686ee78 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioOutEventTest.java @@ -0,0 +1,144 @@ +/* + * 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.conversations; + +import com.vonage.client.OrderedJsonMap; +import static com.vonage.client.OrderedJsonMap.entry; +import com.vonage.client.voice.TextToSpeechLanguage; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; +import java.net.URI; +import java.util.List; +import java.util.function.Function; + +public class AudioOutEventTest extends AbstractEventTest { + private final String text = "Hello, hi testing", streamUrl = "ftp://example.com/path/to/audio.mp3"; + + , B extends AudioOutEvent.Builder> E testAudioOutEventAllFields( + EventType type, B builder, OrderedJsonMap bodyFields) { + + boolean queue = true; + double level = 0.35; + int loop = 6; + + var event = testBaseEvent(type, + builder.loop(loop).level(level).queue(queue), + new OrderedJsonMap( + entry("queue", queue), + entry("level", level), + entry("loop", loop) + ).addAll(bodyFields) + ); + assertEquals(loop, event.getLoop()); + assertEquals(level, event.getLevel()); + assertEquals(queue, event.getQueue()); + return event; + } + + , B extends AudioOutEvent.Builder> E testAudioOutEventRequiredFields( + EventType type, B builder, OrderedJsonMap bodyFields) { + + var event = testBaseEvent(type, builder, bodyFields); + assertNull(event.getQueue()); + assertNull(event.getLevel()); + assertNull(event.getLoop()); + return event; + } + + @Test + public void testAudioPlayEventAllFields() { + var event = testAudioOutEventAllFields(EventType.AUDIO_PLAY, + AudioPlayEvent.builder().streamUrl(streamUrl), + new OrderedJsonMap(entry("stream_url", List.of(streamUrl))) + ); + assertNull(event.getPlayId()); + assertEquals(URI.create(streamUrl), event.getStreamUrl()); + } + + @Test + public void testAudioPlayEventRequiredFields() { + var event = testAudioOutEventRequiredFields(EventType.AUDIO_PLAY, + AudioPlayEvent.builder().streamUrl(streamUrl), new OrderedJsonMap() + ); + assertEquals(URI.create(streamUrl), event.getStreamUrl()); + assertNull(event.getPlayId()); + } + + @Test + public void testAudioSayEventAllFields() { + boolean ssml = false, premium = true; + int style = 1; + TextToSpeechLanguage language = TextToSpeechLanguage.DANISH; + + var event = testAudioOutEventAllFields(EventType.AUDIO_SAY, + AudioSayEvent.builder().text(text).language(language) + .style(style).premium(premium).ssml(ssml), + new OrderedJsonMap( + entry("text", text), + entry("style", style), + entry("language", language), + entry("premium", premium), + entry("ssml", ssml) + ) + ); + assertNull(event.getSayId()); + assertEquals(text, event.getText()); + assertEquals(style, event.getStyle()); + assertEquals(language, event.getLanguage()); + assertEquals(premium, event.getPremium()); + assertEquals(ssml, event.getSsml()); + } + + @Test + public void testAudioSayEventRequiredFields() { + var event = testAudioOutEventRequiredFields(EventType.AUDIO_SAY, + AudioSayEvent.builder().text(text), + new OrderedJsonMap(entry("text", text)) + ); + assertEquals(text, event.getText()); + assertNull(event.getSayId()); + assertNull(event.getSsml()); + assertNull(event.getPremium()); + assertNull(event.getStyle()); + assertNull(event.getLanguage()); + } + + @Test + public void testAudioSayEventNoText() { + assertThrows(IllegalArgumentException.class, () -> applyBaseFields(AudioSayEvent.builder()).build()); + } + + @Test + public void testAudioOutLevelBounds() { + Function buildFunction = + d -> applyBaseFields(AudioPlayEvent.builder().streamUrl(streamUrl).level(d)).build(); + + assertThrows(IllegalArgumentException.class, () -> buildFunction.apply(-1.01)); + assertThrows(IllegalArgumentException.class, () -> buildFunction.apply(1.01)); + } + + @Test + public void testAudioOutLoopBounds() { + assertThrows(IllegalArgumentException.class, () -> + applyBaseFields(AudioPlayEvent.builder().streamUrl(streamUrl).loop(-1)).build() + ); + } + + @Test + public void testAudioPlayStreamRequired() { + assertThrows(NullPointerException.class, () -> applyBaseFields(AudioPlayEvent.builder()).build()); + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioPlayStatusEventTest.java b/src/test/java/com/vonage/client/conversations/AudioPlayStatusEventTest.java new file mode 100644 index 000000000..332e3656a --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioPlayStatusEventTest.java @@ -0,0 +1,62 @@ +/* + * 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.conversations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import java.util.Map; + +public class AudioPlayStatusEventTest extends AbstractEventTest { + + > void testStatusEvent( + B builder, EventType type) { + + var event = testBaseEvent(type, + builder.playId(randomIdStr), + Map.of("play_id", randomId) + ); + assertEquals(randomId, event.getPlayId()); + } + + void testStatusEvent( + EventType eventType, String eventTypeStr, Class eventClass) { + + E event = parseEvent(eventType, eventClass, STR.""" + { + "id": \{randomEventId}, + "type": "audio:play:\{eventTypeStr}", + "body": { + "play_id": "\{randomIdStr}" + }, + "_links": {} + } + """ + ); + assertEquals(randomId, event.getPlayId()); + assertEquals(randomEventId, event.getId()); + } + + @Test + public void testAudioPlayStopEvent() { + testStatusEvent(AudioPlayStopEvent.builder(), EventType.AUDIO_PLAY_STOP); + testStatusEvent(EventType.AUDIO_PLAY_STOP, "stop", AudioPlayStopEvent.class); + } + + @Test + public void testAudioPlayDoneEvent() { + testStatusEvent(EventType.AUDIO_PLAY_DONE, "done", AudioPlayDoneEvent.class); + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioRecordEventTest.java b/src/test/java/com/vonage/client/conversations/AudioRecordEventTest.java new file mode 100644 index 000000000..cac3556a7 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioRecordEventTest.java @@ -0,0 +1,89 @@ +/* + * 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.conversations; + +import com.vonage.client.OrderedJsonMap; +import static com.vonage.client.OrderedJsonMap.entry; +import com.vonage.client.voice.TextToSpeechLanguage; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.Map; + +public class AudioRecordEventTest extends AbstractEventTest { + + private final String format = "ogg"; + private final boolean + beepStart = true, beepStop = true, + detectSpeech = false, multitrack = false, split = true, + sentimentAnalysis = false, streamed = true; + private final int channels = 2, validity = 79; + private final TextToSpeechLanguage language = TextToSpeechLanguage.GREEK; + + @Test + public void testAllFields() { + var event = testBaseEvent(EventType.AUDIO_RECORD, + AudioRecordEvent.builder() + .beepStart(beepStart).beepStop(beepStop).split(split) + .sentimentAnalysis(sentimentAnalysis).streamed(streamed) + .detectSpeech(detectSpeech).multitrack(multitrack).format(format) + .channels(channels).validity(validity).language(language), + + new OrderedJsonMap( + entry("transcription", new OrderedJsonMap( + entry("language", language.getLanguage()), + entry("sentiment_analysis", sentimentAnalysis) + )), + entry("format", format), + entry("validity", validity), + entry("channels", channels), + entry("streamed", streamed), + entry("split", split), + entry("multitrack", multitrack), + entry("detect_speech", detectSpeech), + entry("beep_start", beepStart), + entry("beep_stop", beepStop) + ) + ); + assertEquals(language, event.getLanguage()); + assertEquals(sentimentAnalysis, event.getSentimentAnalysis()); + assertEquals(format, event.getFormat()); + assertEquals(validity, event.getValidity()); + assertEquals(channels, event.getChannels()); + assertEquals(streamed, event.getStreamed()); + assertEquals(split, event.getSplit()); + assertEquals(multitrack, event.getMultitrack()); + assertEquals(detectSpeech, event.getDetectSpeech()); + assertEquals(beepStart, event.getBeepStart()); + assertEquals(beepStop, event.getBeepStop()); + } + + @Test + public void testRequiredFields() { + var event = testBaseEvent(EventType.AUDIO_RECORD, AudioRecordEvent.builder(), Map.of()); + assertNull(event.getLanguage()); + assertNull(event.getSentimentAnalysis()); + assertNull(event.getFormat()); + assertNull(event.getValidity()); + assertNull(event.getChannels()); + assertNull(event.getStreamed()); + assertNull(event.getSplit()); + assertNull(event.getMultitrack()); + assertNull(event.getDetectSpeech()); + assertNull(event.getBeepStart()); + assertNull(event.getBeepStop()); + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioRecordStopEventTest.java b/src/test/java/com/vonage/client/conversations/AudioRecordStopEventTest.java new file mode 100644 index 000000000..a26a999f9 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioRecordStopEventTest.java @@ -0,0 +1,32 @@ +/* + * 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.conversations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import java.util.Map; + +public class AudioRecordStopEventTest extends AbstractEventTest { + + @Test + public void testAudioRecordStopEvent() { + var event = testBaseEvent(EventType.AUDIO_RECORD_STOP, + AudioRecordStopEvent.builder().recordId(randomIdStr), + Map.of("record_id", randomId) + ); + assertEquals(randomId, event.getRecordId()); + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioRtcEventTest.java b/src/test/java/com/vonage/client/conversations/AudioRtcEventTest.java new file mode 100644 index 000000000..9c3b0b7d7 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioRtcEventTest.java @@ -0,0 +1,41 @@ +/* + * 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.conversations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import java.util.Map; + +public class AudioRtcEventTest extends AbstractEventTest { + + > void testRtcEvent( + B builder, EventType type) { + + var event = testBaseEvent(type, + builder.rtcId(randomIdStr), + Map.of("rtc_id", randomId) + ); + assertEquals(randomId, event.getRtcId()); + } + + @Test + public void testRtcEvents() { + testRtcEvent(AudioMuteOffEvent.builder(), EventType.AUDIO_MUTE_OFF); + testRtcEvent(AudioMuteOnEvent.builder(), EventType.AUDIO_MUTE_ON); + testRtcEvent(AudioEarmuffOffEvent.builder(), EventType.AUDIO_EARMUFF_OFF); + testRtcEvent(AudioEarmuffOnEvent.builder(), EventType.AUDIO_EARMUFF_ON); + } +} diff --git a/src/test/java/com/vonage/client/conversations/AudioSayStatusEventTest.java b/src/test/java/com/vonage/client/conversations/AudioSayStatusEventTest.java new file mode 100644 index 000000000..846c37e14 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/AudioSayStatusEventTest.java @@ -0,0 +1,62 @@ +/* + * 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.conversations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import java.util.Map; + +public class AudioSayStatusEventTest extends AbstractEventTest { + + > void testStatusEvent( + B builder, EventType type) { + + var event = testBaseEvent(type, + builder.sayId(randomIdStr), + Map.of("say_id", randomId) + ); + assertEquals(randomId, event.getSayId()); + } + + void testStatusEvent( + EventType eventType, String eventTypeStr, Class eventClass) { + + E event = parseEvent(eventType, eventClass, STR.""" + { + "id": \{randomEventId}, + "type": "audio:say:\{eventTypeStr}", + "body": { + "say_id": "\{randomIdStr}" + }, + "_links": {} + } + """ + ); + assertEquals(randomId, event.getSayId()); + assertEquals(randomEventId, event.getId()); + } + + @Test + public void testAudioSayStopEvent() { + testStatusEvent(AudioSayStopEvent.builder(), EventType.AUDIO_SAY_STOP); + testStatusEvent(EventType.AUDIO_SAY_STOP, "stop", AudioSayStopEvent.class); + } + + @Test + public void testAudioSayDoneEvent() { + testStatusEvent(EventType.AUDIO_SAY_DONE, "done", AudioSayDoneEvent.class); + } +} diff --git a/src/test/java/com/vonage/client/conversations/ConversationUpdatedEventTest.java b/src/test/java/com/vonage/client/conversations/ConversationUpdatedEventTest.java new file mode 100644 index 000000000..c053da4eb --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/ConversationUpdatedEventTest.java @@ -0,0 +1,57 @@ +/* + * 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.conversations; + +import com.vonage.client.TestUtils; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.*; + +public class ConversationUpdatedEventTest extends AbstractEventTest { + + @Test + public void testParseConversationUpdatedEvent() { + var event = parseEvent(EventType.CONVERSATION_UPDATED, ConversationUpdatedEvent.class, STR.""" + { + "type": "conversation:updated", + "body": { + "id": "CON-\{randomId}", + "name": "Test_conv", + "timestamp": { + "created": "2020-01-01T14:00:00.02Z", + "updated": "2020-01-01T14:05:00.00Z", + "destroyed": "2020-01-01T14:20:00.36Z" + }, + "display_name": "Conversation DP", + "image_url": "https://example.org/pic.png", + "state": "ACTIVE" + } + } + """ + ); + BaseConversation conversation = event.getConversation(); + TestUtils.testJsonableBaseObject(conversation); + assertNotNull(conversation.getName()); + assertNotNull(conversation.getId()); + assertNotNull(conversation.getDisplayName()); + assertNotNull(conversation.getImageUrl()); + //assertNotNull(conversation.getState()); + var timestamp = conversation.getTimestamp(); + assertNotNull(timestamp); + assertNotNull(timestamp.getCreated()); + assertNotNull(timestamp.getUpdated()); + assertNotNull(timestamp.getDestroyed()); + } +} diff --git a/src/test/java/com/vonage/client/conversations/ConversationsClientTest.java b/src/test/java/com/vonage/client/conversations/ConversationsClientTest.java new file mode 100644 index 000000000..d36099797 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/ConversationsClientTest.java @@ -0,0 +1,1758 @@ +/* + * 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.conversations; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vonage.client.ClientTest; +import com.vonage.client.RestEndpoint; +import static com.vonage.client.TestUtils.testJsonableBaseObject; +import com.vonage.client.VonageResponseParseException; +import com.vonage.client.common.ChannelType; +import com.vonage.client.common.HttpMethod; +import com.vonage.client.common.SortOrder; +import com.vonage.client.users.BaseUser; +import com.vonage.client.users.channels.Channel; +import com.vonage.client.users.channels.Mms; +import com.vonage.client.users.channels.Sms; +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.time.Instant; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.function.Supplier; + +public class ConversationsClientTest extends ClientTest { + static final Random RANDOM = new Random(); + static final boolean IS_SYSTEM = false, EXCLUDE_DELETED_EVENTS = true, + AUDIO = true, AUDIO_EARMUFFED = false, AUDIO_MUTED = true, AUDIO_ENABLED = true; + static final int PAGE_SIZE = 30, + EVENT_ID = RANDOM.nextInt(100), + EVENT_START_ID = RANDOM.nextInt(EVENT_ID), + EVENT_END_ID = RANDOM.nextInt(EVENT_ID, EVENT_ID * 12), + CONVERSATION_SEQUENCE_NUMBER = 159, CONVERSATION_TTL = 60; + static final double USER_SESSION_TTL = 1.6; + static final SortOrder ORDER = SortOrder.DESCENDING; + static final ConversationStatus CONVERSATION_STATE = ConversationStatus.INACTIVE; + static final MemberState MEMBER_STATE = MemberState.JOINED; + static final Class KNOWN_EVENT_CLASS = AudioSayDoneEvent.class; + static final EventType KNOWN_EVENT_TYPE = EventType.AUDIO_SAY_DONE, CUSTOM_EVENT_TYPE = EventType.CUSTOM; + static final ChannelType CHANNEL_TYPE = ChannelType.PHONE, + CHANNEL_TYPE_TO = ChannelType.MMS, CHANNEL_TYPE_FROM = ChannelType.SMS; + static final Map CONVERSATION_CUSTOM_DATA = Map.of( + "property1", "value1", + "prop2", "Val 2" + ); + + static final String + KNOCKING_ID_STR = "ccc86f37-0a18-4f2e-9bee-da5dce04f601", + INVALID_UUID_STR = "12345678-9abc-defg-hijk-lmnopqrstuvw", + INVITED_BY = "MEM-7bda03b5-5d1b-4734-bf7b-bc83e37f2420", + MEMBER_ID_INVITING = "MEM-7b941a4a-122e-4d9a-868c-d641d185f98c", + MEMBER_ID = "MEM-df8e57d8-1c8e-4573-bf4d-29d5414dcb42", + CONVERSATION_ID = "CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a", + USER_ID = "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + INVITING_USER_ID = "USR-c051865e-ef59-4533-b58a-22cc6c4e962d", + SESSION_ID = "SES-63f61863-4a51-4f6b-86e1-46edebio0391", + START_DATE_STR = "2017-12-30 10:08:59", + END_DATE_STR = "2018-01-03 12:00:00", + TIMESTAMP_CREATED_STR = "2019-08-10T11:29:24.997Z", + TIMESTAMP_UPDATED_STR = "2019-09-03T18:40:24.324Z", + TIMESTAMP_DESTROYED_STR = "2022-02-03T04:58:59.601Z", + TIMESTAMP_INVITED_STR = "2019-07-03T18:52:24.301Z", + TIMESTAMP_JOINED_STR = "2019-09-03T17:02:01.342Z", + TIMESTAMP_LEFT_STR = "2020-10-30T04:59:57.106Z", + TO_NUMBER = "447900000001", + FROM_NUMBER = "491711234567", + MEMBER_FROM = "MEM-67bf6977-3eb8-40b8-b581-204fb4df33b1", + REASON_CODE = "test_code", + REASON_TEXT = "Because I said so", + PHONE_NUMBER = "447900000001", + USER_NAME = "my_user_name", + USER_DISPLAY_NAME = "My User Name", + USER_IMAGE_URL_STR = "https://example.com/profile.jpg", + CONVERSATION_NAME = "customer_chat", + CONVERSATION_DISPLAY_NAME = "Chat with Customer", + CONVERSATION_IMAGE_URL_STR = "https://example.com/image.png", + CONVERSATION_STATE_STR = "INACTIVE", + MEMBER_STATE_STR = "JOINED", + CHANNEL_TYPE_STR = "phone", + CHANNEL_TYPE_FROM_STR = "sms", + CHANNEL_TYPE_TO_STR = "mms", + ORDER_STR = "desc", + CUSTOM_EVENT_TYPE_STR = "custom", + KNOWN_EVENT_TYPE_STR = "audio:say:done", + CONVERSATION_TYPE = "quick_chat", + CONVERSATION_CUSTOM_SORT_KEY = "CSK_1", + CONVERSATION_CUSTOM_DATA_STR = "{\"property1\":\"value1\",\"prop2\":\"Val 2\"}", + REQUEST_CURSOR = "7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=", + SAMPLE_BASE_CONVERSATION_RESPONSE_PARTIAL = STR.""" + { + "id": "\{CONVERSATION_ID}", + "name": "\{CONVERSATION_NAME}", + "display_name": "\{CONVERSATION_DISPLAY_NAME}", + "image_url": "\{CONVERSATION_IMAGE_URL_STR}", + "timestamp": { + "created": "\{TIMESTAMP_CREATED_STR}", + "updated": "\{TIMESTAMP_UPDATED_STR}", + "destroyed": "\{TIMESTAMP_DESTROYED_STR}" + },""", + SAMPLE_BASE_CONVERSATION_RESPONSE = STR."\{SAMPLE_BASE_CONVERSATION_RESPONSE_PARTIAL + .substring(0, SAMPLE_BASE_CONVERSATION_RESPONSE_PARTIAL.length() - 1)}\n\t}", + SAMPLE_CONVERSATION_RESPONSE_PARTIAL = SAMPLE_BASE_CONVERSATION_RESPONSE_PARTIAL + STR.""" + "state": "\{CONVERSATION_STATE_STR}", + "sequence_number": \{CONVERSATION_SEQUENCE_NUMBER}, + "properties": { + "ttl": \{CONVERSATION_TTL}, + "type": "\{CONVERSATION_TYPE}", + "custom_sort_key": "\{CONVERSATION_CUSTOM_SORT_KEY}", + "custom_data": \{CONVERSATION_CUSTOM_DATA_STR} + }, + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/conversations/\{CONVERSATION_ID}" + } + }""", + SAMPLE_CONVERSATION_RESPONSE = SAMPLE_CONVERSATION_RESPONSE_PARTIAL + "\n}", + SAMPLE_LIST_CONVERSATIONS_RESPONSE = STR.""" + { + "page_size": \{PAGE_SIZE}, + "_embedded": { + "conversations": [ + {}, \{SAMPLE_BASE_CONVERSATION_RESPONSE}, {"id":null} + ] + }, + "_links": { + "first": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10" + }, + "self": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "next": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "prev": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + } + } + } + """, + SAMPLE_USER_CONVERSATION_RESPONSE = SAMPLE_CONVERSATION_RESPONSE_PARTIAL + STR.""" + , + "_embedded": { + "id": "\{MEMBER_ID}", + "state": "\{MEMBER_STATE_STR}" + } + } + """, + SAMPLE_LIST_USER_CONVERSATIONS_RESPONSE = STR.""" + { + "page_size": \{PAGE_SIZE}, + "_embedded": { + "conversations": [ + \{SAMPLE_USER_CONVERSATION_RESPONSE}, {"_embedded": {}} + ] + }, + "_links": { + "first": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10" + }, + "self": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "next": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "prev": { + "href": "https://api.nexmo.com/v1/conversations?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + } + } + } + """, + SAMPLE_USER_SESSION_RESPONSE = STR.""" + { + "id": "\{SESSION_ID}", + "_embedded": { + "user": { + "id": "\{USER_ID}", + "name": "\{USER_NAME}" + }, + "api_key": "\{API_KEY}" + }, + "properties": { + "ttl": \{USER_SESSION_TTL} + }, + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/users/\{USER_ID}/sessions" + } + } + } + """, + SAMPLE_LIST_USER_SESSIONS_RESPONSE = STR.""" + { + "page_size": \{PAGE_SIZE}, + "_embedded": { + "sessions": [ + {}, \{SAMPLE_USER_SESSION_RESPONSE}, {"_embedded":{"user":{}}} + ] + }, + "_links": { + "first": { + "href": "https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec/sessions?order=desc&page_size=10" + }, + "self": { + "href": "https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec/sessions?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "next": { + "href": "https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec/sessions?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + }, + "prev": { + "href": "https://api.nexmo.com/v1/users/USR-82e028d9-5201-4f1e-8188-604b2d3471ec/sessions?order=desc&page_size=10&cursor=7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" + } + } + } + """, + SAMPLE_BASE_MEMBER_RESPONSE_PARTIAL = STR.""" + { + "id": "\{MEMBER_ID}", + "conversation_id": "\{CONVERSATION_ID}", + "_embedded": { + "user": { + "id": "\{USER_ID}", + "name": "\{USER_NAME}", + "display_name": "\{USER_DISPLAY_NAME}", + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/users/\{USER_ID}" + } + } + } + }, + "state": "\{MEMBER_STATE_STR}", + "_links": { + "href": "https://api.nexmo.com/v1/conversations/\{CONVERSATION_ID}/members/\{MEMBER_ID}" + }""", + SAMPLE_BASE_MEMBER_RESPONSE = STR."\{SAMPLE_BASE_MEMBER_RESPONSE_PARTIAL}\n\t}", + SAMPLE_MEMBER_RESPONSE = SAMPLE_BASE_MEMBER_RESPONSE_PARTIAL + STR.""" + , + "timestamp": { + "invited": "\{TIMESTAMP_INVITED_STR}", + "joined": "\{TIMESTAMP_JOINED_STR}", + "left": "\{TIMESTAMP_LEFT_STR}" + }, + "initiator": { + "joined": { + "is_system": \{IS_SYSTEM}, + "user_id": "\{INVITING_USER_ID}", + "member_id": "\{MEMBER_ID_INVITING}" + } + }, + "channel": { + "type": "\{CHANNEL_TYPE_STR}", + "from": { + "type": "\{CHANNEL_TYPE_FROM_STR}", + "number": "\{FROM_NUMBER}" + }, + "to": { + "type": "\{CHANNEL_TYPE_TO_STR}", + "number": "\{TO_NUMBER}" + } + }, + "media": { + "audio_settings": { + "enabled": \{AUDIO_ENABLED}, + "earmuffed": \{AUDIO_EARMUFFED}, + "muted": \{AUDIO_MUTED} + }, + "audio": \{AUDIO} + }, + "knocking_id": "\{KNOCKING_ID_STR}", + "invited_by": "\{INVITED_BY}" + } + """, + SAMPLE_LIST_MEMBERS_RESPONSE = STR.""" + { + "page_size": \{PAGE_SIZE}, + "_embedded": { + "members": [ + {}, + \{SAMPLE_BASE_MEMBER_RESPONSE}, + {"state": "LEFT", "id": "\{MEMBER_ID_INVITING}"}, + {"_embedded": {"user": {}}}, + {"_embedded": {}, "_links": {}} + ] + }, + "_links": { + "first": { + "href": "https://api.nexmo.com/v1/conversations/CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a/members?order=desc&page_size=10" + }, + "self": { + "href": "https://api.nexmo.com/v1/conversations/CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a/members?order=desc&page_size=10&cursor=88b395c167da4d94e929705cbd63b82973771e7d390d274a58e301386d5762600a3ffd799bfb3fc5190c5a0d124cdd0fc72fe6e450506b18e4e2edf9fe84c7a0" + }, + "next": { + "href": "https://api.nexmo.com/v1/conversations/CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a/members?order=desc&page_size=10&cursor=88b395c167da4d94e929705cbd63b829a650e69a39197bfd4c949f4243f60dc4babb696afa404d2f44e7775e32b967f2a1a0bb8fb259c0999ba5a4e501eaab55" + }, + "prev": { + "href": "https://api.nexmo.com/v1/conversations/CON-d66d47de-5bcb-4300-94f0-0c9d4b948e9a/members?order=desc&page_size=10&cursor=069626a3de11d2ec900dff5042197bd75f1ce41dafc3f2b2481eb9151086e59aae9dba3e3a8858dc355232d499c310fbfbec43923ff657c0de8d49ffed9f7edb" + } + } + } + """, + SAMPLE_EVENT_RESPONSE = STR.""" + { + "id": \{EVENT_ID}, + "type": "\{CUSTOM_EVENT_TYPE_STR}", + "from": "\{MEMBER_ID}", + "body": \{CONVERSATION_CUSTOM_DATA_STR}, + "timestamp": "\{TIMESTAMP_CREATED_STR}", + "_embedded": { + "from_user": { + "id": "\{USER_ID}", + "name": "\{USER_NAME}", + "display_name": "\{USER_DISPLAY_NAME}", + "image_url": "\{USER_IMAGE_URL_STR}", + "custom_data": {} + }, + "from_member": { + "id": "\{MEMBER_ID_INVITING}" + } + }, + "_links": { + "self": { + "href": "https://api.nexmo.com/v1/conversations/\{CONVERSATION_ID}/events/\{EVENT_ID}" + } + } + } + """, + SAMPLE_LIST_EVENTS_RESPONSE = STR.""" + { + "page_size": \{PAGE_SIZE}, + "_links": { + "first": {},"self": {},"next": {},"prev": {} + }, + "_embedded": [ + \{SAMPLE_EVENT_RESPONSE}, + {}, {"type": "\{KNOWN_EVENT_TYPE_STR}"} + ] + } + """; + + static final Channel CHANNEL_FROM = new Sms(FROM_NUMBER), CHANNEL_TO = new Mms(TO_NUMBER); + static final UUID KNOCKING_ID = UUID.fromString(KNOCKING_ID_STR), RANDOM_UUID = UUID.randomUUID(); + static final URI + CONVERSATION_IMAGE_URL = URI.create(CONVERSATION_IMAGE_URL_STR), + USER_IMAGE_URL = URI.create(USER_IMAGE_URL_STR); + static final Instant + START_DATE = Instant.parse(START_DATE_STR.replace(' ','T')+'Z'), + END_DATE = Instant.parse(END_DATE_STR.replace(' ','T')+'Z'), + TIMESTAMP_CREATED = Instant.parse(TIMESTAMP_CREATED_STR), + TIMESTAMP_UPDATED = Instant.parse(TIMESTAMP_UPDATED_STR), + TIMESTAMP_DESTROYED = Instant.parse(TIMESTAMP_DESTROYED_STR), + TIMESTAMP_INVITED = Instant.parse(TIMESTAMP_INVITED_STR), + TIMESTAMP_JOINED = Instant.parse(TIMESTAMP_JOINED_STR), + TIMESTAMP_LEFT = Instant.parse(TIMESTAMP_LEFT_STR); + + static final Supplier MEMBER_REQUEST_BUILDER_FACTORY = () -> + Member.builder().state(MEMBER_STATE).user(USER_ID) + .channelType(CHANNEL_TYPE).fromChannel(CHANNEL_FROM).toChannel(CHANNEL_TO); + + public ConversationsClientTest() { + client = new ConversationsClient(wrapper); + } + + void assert401ResponseException(Executable invocation) throws Exception { + assert401ApiResponseException(ConversationsResponseException.class, invocation); + } + + void assert404ResponseException(Executable invocation) throws Exception { + stubResponseAndAssertThrows(404, invocation, ConversationsResponseException.class); + } + + void assertResponseExceptions(Executable invocation) throws Exception { + assert404ResponseException(invocation); + assert401ResponseException(invocation); + } + + static void assertEqualsSampleBaseConversation(BaseConversation parsed) { + testJsonableBaseObject(parsed); + assertEquals(CONVERSATION_ID, parsed.getId()); + assertEquals(CONVERSATION_NAME, parsed.getName()); + assertEquals(CONVERSATION_DISPLAY_NAME, parsed.getDisplayName()); + assertEquals(CONVERSATION_IMAGE_URL, parsed.getImageUrl()); + var timestamp = parsed.getTimestamp(); + assertNotNull(timestamp); + assertEquals(TIMESTAMP_CREATED, timestamp.getCreated()); + assertEquals(TIMESTAMP_UPDATED, timestamp.getUpdated()); + assertEquals(TIMESTAMP_DESTROYED, timestamp.getDestroyed()); + } + + static void assertEqualsSampleConversation(Conversation parsed) { + assertEqualsSampleBaseConversation(parsed); + assertEquals(CONVERSATION_STATE, parsed.getState()); + assertEquals(CONVERSATION_SEQUENCE_NUMBER, parsed.getSequenceNumber()); + var properties = parsed.getProperties(); + assertNotNull(properties); + assertEquals(CONVERSATION_TTL, properties.getTtl()); + assertEquals(CONVERSATION_TYPE, properties.getType()); + assertEquals(CONVERSATION_CUSTOM_SORT_KEY, properties.getCustomSortKey()); + assertEquals(CONVERSATION_CUSTOM_DATA, properties.getCustomData()); + } + + static void assertEqualsEmptyConversation(Conversation parsed) { + assertNotNull(parsed); + assertNull(parsed.getProperties()); + assertNull(parsed.getId()); + assertNull(parsed.getCallback()); + assertNull(parsed.getName()); + assertNull(parsed.getState()); + assertNull(parsed.getImageUrl()); + assertNull(parsed.getDisplayName()); + assertNull(parsed.getNumbers()); + assertNull(parsed.getSequenceNumber()); + assertNull(parsed.getTimestamp()); + } + + static void assertEqualsSampleUserConversation(UserConversation parsed) { + assertEqualsSampleConversation(parsed); + var member = parsed.getMember(); + testJsonableBaseObject(member); + assertEquals(MEMBER_ID, member.getId()); + assertEquals(MEMBER_STATE, member.getState()); + } + + static void assertEmptyBaseConversation(BaseConversation parsed) { + testJsonableBaseObject(parsed); + assertNull(parsed.getId()); + assertNull(parsed.getName()); + assertNull(parsed.getDisplayName()); + assertNull(parsed.getImageUrl()); + assertNull(parsed.getTimestamp()); + } + + static void assertEqualsSampleListConversations(ListConversationsResponse parsed) { + testJsonableBaseObject(parsed); + assertEquals(PAGE_SIZE, parsed.getPageSize()); + var conversations = parsed.getConversations(); + assertNotNull(conversations); + assertEquals(3, conversations.size()); + assertEmptyBaseConversation(conversations.getFirst()); + assertEqualsSampleBaseConversation(conversations.get(1)); + assertEmptyBaseConversation(conversations.getLast()); + } + + static void assertEqualsSampleListUserConversations(ListUserConversationsResponse parsed) { + testJsonableBaseObject(parsed); + assertEquals(PAGE_SIZE, parsed.getPageSize()); + var conversations = parsed.getConversations(); + assertNotNull(conversations); + assertEquals(2, conversations.size()); + assertEqualsSampleUserConversation(conversations.getFirst()); + var last = conversations.getLast(); + testJsonableBaseObject(last); + var member = last.getMember(); + assertNotNull(member); + assertNull(member.getId()); + assertNull(member.getState()); + } + + static void assertEqualsBaseUser(BaseUser parsed) { + testJsonableBaseObject(parsed); + assertEquals(USER_ID, parsed.getId()); + assertEquals(USER_NAME, parsed.getName()); + } + + static void assertEqualsSampleBaseMember(BaseMember parsed) { + testJsonableBaseObject(parsed); + assertEquals(MEMBER_STATE, parsed.getState()); + assertEquals(MEMBER_ID, parsed.getId()); + assertEqualsBaseUser(parsed.getUser()); + } + + static void assertEqualsSampleMember(Member parsed) { + assertNotNull(parsed); + assertEquals(CONVERSATION_ID, parsed.getConversationId()); + parsed.setConversationId(null); + assertEqualsSampleBaseMember(parsed); + + var timestamp = parsed.getTimestamp(); + testJsonableBaseObject(timestamp); + assertEquals(TIMESTAMP_INVITED, timestamp.getInvited()); + assertEquals(TIMESTAMP_JOINED, timestamp.getJoined()); + assertEquals(TIMESTAMP_LEFT, timestamp.getLeft()); + + var initiator = parsed.getInitiator(); + testJsonableBaseObject(initiator); + assertEquals(IS_SYSTEM, initiator.invitedByAdmin()); + assertEquals(MEMBER_ID_INVITING, initiator.getMemberId()); + assertEquals(INVITING_USER_ID, initiator.getUserId()); + + var channel = parsed.getChannel(); + testJsonableBaseObject(channel); + assertEquals(CHANNEL_TYPE, channel.getType()); + assertEquals(CHANNEL_FROM, channel.getFrom()); + assertEquals(CHANNEL_TYPE_FROM, CHANNEL_FROM.getType()); + assertEquals(CHANNEL_TO, channel.getTo()); + assertEquals(CHANNEL_TYPE_TO, CHANNEL_TO.getType()); + + var media = parsed.getMedia(); + testJsonableBaseObject(media); + assertEquals(AUDIO, media.getAudio()); + var audioSettings = media.getAudioSettings(); + testJsonableBaseObject(audioSettings); + assertEquals(AUDIO_ENABLED, audioSettings.getEnabled()); + assertEquals(AUDIO_EARMUFFED, audioSettings.getEarmuffed()); + assertEquals(AUDIO_MUTED, audioSettings.getMuted()); + + assertEquals(KNOCKING_ID, parsed.getKnockingId()); + assertEquals(INVITED_BY, parsed.getInvitedBy()); + var inviting = parsed.getMemberIdInviting(); + if (inviting != null) { + assertEquals(MEMBER_ID_INVITING, inviting); + } + } + + static void assertEqualsEmptyBaseMember(BaseMember parsed) { + testJsonableBaseObject(parsed); + assertNull(parsed.getUser()); + assertNull(parsed.getId()); + assertNull(parsed.getState()); + } + + static void assertEqualsEmptyMember(Member parsed) { + assertEqualsEmptyBaseMember(parsed); + assertNull(parsed.getConversationId()); + assertNull(parsed.getMedia()); + assertNull(parsed.getMemberIdInviting()); + assertNull(parsed.getKnockingId()); + assertNull(parsed.getInvitedBy()); + assertNull(parsed.getInitiator()); + assertNull(parsed.getChannel()); + assertNull(parsed.getFrom()); + assertNull(parsed.getTimestamp()); + } + + static void assertEqualsEmptyBaseUser(BaseUser parsed) { + testJsonableBaseObject(parsed); + assertNull(parsed.getName()); + assertNull(parsed.getId()); + } + + static void assertEqualsSampleListMembers(ListMembersResponse parsed) { + testJsonableBaseObject(parsed); + assertEquals(PAGE_SIZE, parsed.getPageSize()); + var members = parsed.getMembers(); + assertNotNull(members); + assertEquals(5, members.size()); + assertEqualsEmptyBaseMember(members.getFirst()); + assertEqualsSampleBaseMember(members.get(1)); + var third = members.get(2); + testJsonableBaseObject(third); + assertEquals(MemberState.LEFT, third.getState()); + assertEquals(MEMBER_ID_INVITING, third.getId()); + assertNull(third.getUser()); + var fourth = members.get(3); + testJsonableBaseObject(fourth); + assertNull(fourth.getId()); + assertNull(fourth.getState()); + assertEqualsEmptyBaseUser(fourth.getUser()); + assertEqualsEmptyBaseMember(members.get(4)); + } + + static void assertEqualsMinimalMember(Member request) { + assertNotNull(request); + assertEquals(MEMBER_STATE, request.getState()); + var user = request.getUser(); + assertNotNull(user); + assertEquals(USER_ID, user.getId()); + var channel = request.getChannel(); + assertNotNull(channel); + assertEquals(CHANNEL_TYPE, channel.getType()); + var fromChannel = channel.getFrom(); + assertNotNull(fromChannel); + assertEquals(CHANNEL_FROM, fromChannel); + assertEquals(CHANNEL_TYPE_FROM, fromChannel.getType()); + var toChannel = channel.getTo(); + assertNotNull(toChannel); + assertEquals(CHANNEL_TO, toChannel); + assertEquals(CHANNEL_TYPE_TO, toChannel.getType()); + assertNull(request.getFrom()); + assertNull(request.getConversationId()); + assertNull(request.getInvitedBy()); + assertNull(request.getMemberIdInviting()); + assertNull(request.getKnockingId()); + assertNull(request.getInitiator()); + assertNull(request.getMedia()); + assertNull(request.getTimestamp()); + } + + static void assertEqualsSampleEvent(Event parsed) { + assertNotNull(parsed); + String convId = parsed.conversationId; + parsed.conversationId = null; + testJsonableBaseObject(parsed); + parsed.conversationId = convId; + assertEquals(CUSTOM_EVENT_TYPE, parsed.getType()); + assertEquals(CustomEvent.class, parsed.getClass()); + assertEquals(CONVERSATION_CUSTOM_DATA, ((CustomEvent) parsed).getBody()); + assertEquals(EVENT_ID, parsed.getId()); + assertEquals(MEMBER_ID, parsed.getFrom()); + assertEquals(TIMESTAMP_CREATED, parsed.getTimestamp()); + var fromMember = parsed.getFromMember(); + testJsonableBaseObject(fromMember); + assertEquals(MEMBER_ID_INVITING, fromMember.getId()); + assertNull(fromMember.getUser()); + assertNull(fromMember.getState()); + var fromUser = parsed.getFromUser(); + testJsonableBaseObject(fromUser); + assertEquals(USER_ID, fromUser.getId()); + assertEquals(USER_NAME, fromUser.getName()); + assertEquals(USER_DISPLAY_NAME, fromUser.getDisplayName()); + assertEquals(USER_IMAGE_URL, fromUser.getImageUrl()); + assertEquals(Map.of(), fromUser.getCustomData()); + } + + static void assertEqualsEmptyEvent(Event parsed) { + testJsonableBaseObject(parsed); + assertEquals(GenericEvent.class, parsed.getClass()); + assertNull(parsed.getType()); + assertNull(parsed.getId()); + assertNull(parsed.getTimestamp()); + assertNull(parsed.getFrom()); + assertNull(parsed.getFromMember()); + assertNull(parsed.getFromUser()); + } + + static void assertEqualsSampleListEvents(ListEventsResponse parsed) { + testJsonableBaseObject(parsed); + assertEquals(PAGE_SIZE, parsed.getPageSize()); + assertNotNull(parsed.getLinks()); + var events = parsed.getEvents(); + assertNotNull(events); + assertEquals(3, events.size()); + assertEqualsSampleEvent(events.getFirst()); + assertEqualsEmptyEvent(events.get(1)); + var last = events.getLast(); + testJsonableBaseObject(last); + assertEquals(KNOWN_EVENT_TYPE, last.getType()); + assertEquals(KNOWN_EVENT_CLASS, last.getClass()); + } + + // CONVERSATIONS + + @Test + public void testListConversations() throws Exception { + ListConversationsRequest request = ListConversationsRequest.builder().build(); + stubResponse(200, SAMPLE_LIST_CONVERSATIONS_RESPONSE); + var response = client.listConversations(request); + assertEqualsSampleListConversations(response); + + var listOnly = stubResponseAndGet(SAMPLE_LIST_CONVERSATIONS_RESPONSE, client::listConversations); + assertEquals(response.getConversations(), listOnly); + + stubResponseAndAssertThrows(200, + () -> client.listConversations(null), NullPointerException.class + ); + stubResponseAndAssertThrows(409, + () -> client.listConversations(request), + ConversationsResponseException.class + ); + assert401ResponseException(() -> client.listConversations(request)); + } + + @Test + public void testListConversationsEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.listConversations; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ListConversationsRequest request) { + return "/v1/conversations/"; + } + + @Override + protected ListConversationsRequest sampleRequest() { + return ListConversationsRequest.builder() + .pageSize(PAGE_SIZE).order(ORDER).cursor(REQUEST_CURSOR) + .startDate(START_DATE).endDate(END_DATE).build(); + } + + @Override + protected Map sampleQueryParams() { + return Map.of( + "cursor", REQUEST_CURSOR, + "page_size", String.valueOf(PAGE_SIZE), + "order", String.valueOf(ORDER), + "date_start", START_DATE_STR, + "date_end", END_DATE_STR + ); + } + + @Override + public void runTests() throws Exception { + super.runTests(); + testSampleRequestGetters(); + testEmptyRequest(); + testPageSizeLimit(); + } + + void testSampleRequestGetters() { + var request = sampleRequest(); + assertEquals(REQUEST_CURSOR, request.getCursor()); + assertEquals(START_DATE, request.getStartDate()); + assertEquals(END_DATE, request.getEndDate()); + assertEquals(PAGE_SIZE, request.getPageSize()); + assertEquals(ORDER, request.getOrder()); + } + + void testEmptyRequest() { + var request = ListConversationsRequest.builder().build(); + assertNull(request.getCursor()); + assertNull(request.getPageSize()); + assertNull(request.getOrder()); + assertNull(request.getStartDate()); + assertNull(request.getEndDate()); + var toMap = request.makeParams(); + assertNotNull(toMap); + assertEquals(0, toMap.size()); + } + + void testPageSizeLimit() { + int limit = 100; + assertEquals(limit, ListConversationsRequest.builder().pageSize(limit).build().getPageSize()); + assertThrows(IllegalArgumentException.class, + () -> ListConversationsRequest.builder().pageSize(limit + 1).build() + ); + assertThrows(IllegalArgumentException.class, + () -> ListConversationsRequest.builder().pageSize(0).build() + ); + } + } + .runTests(); + } + + + @Test + public void testCreateConversation() throws Exception { + var request = Conversation.builder().build(); + stubResponse(201, SAMPLE_CONVERSATION_RESPONSE); + assertEqualsSampleConversation(client.createConversation(request)); + stubResponseAndAssertThrows(201, + () -> client.createConversation(null), + NullPointerException.class + ); + stubResponseAndAssertThrows(409, + () -> client.createConversation(request), + ConversationsResponseException.class + ); + assert401ResponseException(() -> client.createConversation(request)); + } + + @Test + public void testCreateConversationEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + final Callback callback = Callback.builder().url("http://example.com/callback") + .eventMask("Test value").applicationId(APPLICATION_ID) + .nccoUrl("http://example.com/ncco").method(HttpMethod.POST).build(); + + @Override + protected RestEndpoint endpoint() { + return client.createConversation; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.POST; + } + + @Override + protected String expectedEndpointUri(Conversation request) { + return "/v1/conversations/"; + } + + @Override + protected Conversation sampleRequest() { + return Conversation.builder() + .name(CONVERSATION_NAME) + .displayName(CONVERSATION_DISPLAY_NAME) + .imageUrl(CONVERSATION_IMAGE_URL_STR) + .properties(ConversationProperties.builder() + .ttl(CONVERSATION_TTL).type(CONVERSATION_TYPE) + .customSortKey(CONVERSATION_CUSTOM_SORT_KEY) + .customData(CONVERSATION_CUSTOM_DATA).build() + ) + .callback(callback).phone(PHONE_NUMBER).build(); + } + + @Override + protected String sampleRequestBodyString() { + var request = sampleRequest(); + var customData = request.getProperties().getCustomData(); + + try { + return STR.""" + {"name":"\{request.getName()}","display_name":"\{request.getDisplayName()}",\ + "image_url":"\{request.getImageUrl()}","properties":{\ + "ttl":\{request.getProperties().getTtl()},\ + "type":"\{request.getProperties().getType()}",\ + "custom_sort_key":"\{request.getProperties().getCustomSortKey()}",\ + "custom_data":\{new ObjectMapper().writeValueAsString(customData)}},\ + "numbers":[{"type":"phone","number":"\{PHONE_NUMBER}"}],"callback":{\ + "url":"\{request.getCallback().getUrl()}","event_mask":\ + "\{request.getCallback().getEventMask()}","params":{\ + "applicationId":"\{request.getCallback().getParams().getApplicationId()}",\ + "ncco_url":"\{request.getCallback().getParams().getNccoUrl()}"},\ + "method":"\{request.getCallback().getMethod()}"}}"""; + } + catch (JsonProcessingException impossible) { + throw new IllegalStateException(impossible); + } + } + + @Override + public void runTests() throws Exception { + super.runTests(); + testNameLength(); + testDisplayNameLength(); + testEventMaskLength(); + testCallbackMethod(); + testTypeLength(); + testCustomSortKeyLength(); + testJsonableBaseObject(callback); + } + + void testNameLength() { + String limit = "Conv".repeat(25); + assertEquals(100, Conversation.builder().name(limit).build().getName().length()); + assertThrows(IllegalArgumentException.class, + () -> Conversation.builder().name(limit+'1').build() + ); + } + + void testDisplayNameLength() { + String limit = "dN".repeat(25); + assertEquals(50, Conversation.builder().displayName(limit).build().getDisplayName().length()); + assertThrows(IllegalArgumentException.class, + () -> Conversation.builder().displayName(limit+'1').build() + ); + } + + void testEventMaskLength() { + String limit = "Event_Mask".repeat(20); + assertEquals(200, Conversation.builder() + .callback(Callback.builder().eventMask(limit).build()) + .build().getCallback().getEventMask().length() + ); + assertThrows(IllegalArgumentException.class, () -> Conversation.builder() + .callback(Callback.builder().eventMask(limit+'1').build()).build() + ); + } + + void testCallbackMethod() { + for (var method : HttpMethod.values()) { + switch (method) { + default -> assertThrows(IllegalArgumentException.class, () -> Conversation.builder() + .callback(Callback.builder().method(method).build()).build() + ); + case GET, POST -> assertEquals(method, Conversation.builder() + .callback(Callback.builder().method(method).build()) + .build().getCallback().getMethod() + ); + } + } + } + + void testTypeLength() { + String limit = CONVERSATION_TYPE.repeat(20); + assertEquals(200, Conversation.builder() + .properties(ConversationProperties.builder().type(limit).build()) + .build().getProperties().getType().length() + ); + assertThrows(IllegalArgumentException.class, () -> + ConversationProperties.builder().type(limit+'1').build() + ); + } + + void testCustomSortKeyLength() { + String limit = CONVERSATION_CUSTOM_SORT_KEY.repeat(40); + assertEquals(200, Conversation.builder() + .properties(ConversationProperties.builder().customSortKey(limit).build()) + .build().getProperties().getCustomSortKey().length() + ); + assertThrows(IllegalArgumentException.class, () -> + ConversationProperties.builder().customSortKey(limit+'0').build() + ); + } + } + .runTests(); + } + + + @Test + public void testGetConversation() throws Exception { + stubResponse(200, SAMPLE_CONVERSATION_RESPONSE); + assertEqualsSampleConversation(client.getConversation(CONVERSATION_ID)); + + stubResponseAndAssertThrows(200, + () -> client.getConversation(null), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.getConversation(MEMBER_ID), + IllegalArgumentException.class + ); + assertResponseExceptions(() -> client.getConversation(CONVERSATION_ID)); + + stubResponse(200, "{\"state\": \"limbo\"}"); + assertEqualsEmptyConversation(client.getConversation(CONVERSATION_ID)); + } + + @Test + public void testGetConversationEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.getConversation; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(String request) { + return "/v1/conversations/"+request; + } + + @Override + protected String sampleRequest() { + return CONVERSATION_ID; + } + } + .runTests(); + } + + + @Test + public void testUpdateConversation() throws Exception { + var request = Conversation.builder() + .imageUrl("ftp:///path/to/local/image.tiff") + .displayName("Support").build(); + stubResponse(200, SAMPLE_CONVERSATION_RESPONSE); + assertEqualsSampleConversation(client.updateConversation(CONVERSATION_ID, request)); + + stubResponseAndAssertThrows(200, + () -> client.updateConversation(null, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.updateConversation(CONVERSATION_ID, null), + NullPointerException.class + ); + stubResponseAndAssertThrows(200, + () -> client.updateConversation("CON-"+SESSION_ID, request), + IllegalArgumentException.class + ); + assertResponseExceptions(() -> client.updateConversation(CONVERSATION_ID, request)); + } + + @Test + public void testUpdateConversationEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.updateConversation; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.PUT; + } + + @Override + protected String expectedEndpointUri(Conversation request) { + return "/v1/conversations/"+request.getId(); + } + + @Override + protected Conversation sampleRequest() { + var request = Conversation.builder() + .imageUrl(CONVERSATION_IMAGE_URL_STR) + .displayName(CONVERSATION_DISPLAY_NAME) + .name(CONVERSATION_NAME).build(); + request.id = CONVERSATION_ID; + return request; + } + + @Override + protected String sampleRequestBodyString() { + return STR.""" + {"id":"\{CONVERSATION_ID}","name":"\{CONVERSATION_NAME}",\ + "display_name":"\{CONVERSATION_DISPLAY_NAME}",\ + "image_url":"\{CONVERSATION_IMAGE_URL_STR}"}\ + """; + } + } + .runTests(); + } + + + @Test + public void testDeleteConversation() throws Exception { + stubResponseAndRun(204, SAMPLE_CONVERSATION_RESPONSE, + () -> client.deleteConversation(CONVERSATION_ID) + ); + stubResponseAndAssertThrows(204, + () -> client.deleteConversation(null), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(204, + () -> client.deleteConversation("CON-"+INVALID_UUID_STR), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(404, + () -> client.deleteConversation(CONVERSATION_ID), + ConversationsResponseException.class + ); + assertResponseExceptions(() -> client.deleteConversation(CONVERSATION_ID)); + } + + @Test + public void testDeleteConversationEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.deleteConversation; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.DELETE; + } + + @Override + protected String expectedEndpointUri(String request) { + return "/v1/conversations/"+request; + } + + @Override + protected String sampleRequest() { + return CONVERSATION_ID; + } + } + .runTests(); + } + + // USERS + + @Test + public void testListUserConversations() throws Exception { + var request = ListUserConversationsRequest.builder().build(); + stubResponse(200, SAMPLE_LIST_USER_CONVERSATIONS_RESPONSE); + assertEqualsSampleListUserConversations(client.listUserConversations(USER_ID, request)); + + var userConversations = stubResponseAndGet(SAMPLE_LIST_USER_CONVERSATIONS_RESPONSE, + () -> client.listUserConversations(USER_ID) + ); + assertNotNull(userConversations); + assertEquals(2, userConversations.size()); + assertEqualsSampleUserConversation(userConversations.getFirst()); + + stubResponseAndAssertThrows(200, + () -> client.listUserConversations(null, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.listUserConversations(INVALID_UUID_STR+"-RUS", request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.listUserConversations(SESSION_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.listUserConversations(USER_ID, null), + NullPointerException.class + ); + stubResponseAndAssertThrows(404, + () -> client.listUserConversations(USER_ID, request), + ConversationsResponseException.class + ); + assertResponseExceptions(() -> client.listUserConversations(USER_ID, request)); + } + + @Test + public void testListUserConversationsEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.listUserConversations; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ListUserConversationsRequest request) { + return "/v1/users/"+request.userId+"/conversations"; + } + + @Override + protected ListUserConversationsRequest sampleRequest() { + var request = ListUserConversationsRequest.builder() + .state(MemberState.INVITED).includeCustomData(true) + .orderBy(OrderBy.CUSTOM_SORT_KEY).startDate(START_DATE) + .cursor(REQUEST_CURSOR).build(); + + assertEquals(REQUEST_CURSOR, request.getCursor()); + assertEquals(MemberState.INVITED, request.getState()); + assertEquals(OrderBy.CUSTOM_SORT_KEY, request.getOrderBy()); + assertTrue(request.getIncludeCustomData()); + assertEquals(START_DATE, request.getStartDate()); + + request.userId = USER_ID; + return request; + } + + @Override + protected Map sampleQueryParams() { + return Map.of( + "cursor", REQUEST_CURSOR, + "state", "INVITED", + "order_by", "custom_sort_key", + "include_custom_data", "true", + "date_start", START_DATE_STR + ); + } + } + .runTests(); + } + + // MEMBERS + + @Test + public void testListMembers() throws Exception { + var request = ListMembersRequest.builder().build(); + stubResponse(200, SAMPLE_LIST_MEMBERS_RESPONSE); + var fullResponse = client.listMembers(CONVERSATION_ID, request); + assertEqualsSampleListMembers(fullResponse); + stubResponse(200, SAMPLE_LIST_MEMBERS_RESPONSE); + assertEquals(fullResponse.getMembers(), client.listMembers(CONVERSATION_ID)); + + stubResponseAndAssertThrows(SAMPLE_LIST_MEMBERS_RESPONSE, + () -> client.listMembers(CONVERSATION_ID, null), + NullPointerException.class + ); + stubResponseAndAssertThrows(SAMPLE_LIST_MEMBERS_RESPONSE, + () -> client.listMembers(null, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_LIST_MEMBERS_RESPONSE, + () -> client.listMembers(MEMBER_ID, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_LIST_MEMBERS_RESPONSE, + () -> client.listMembers(KNOCKING_ID_STR), + IllegalArgumentException.class + ); + assertResponseExceptions(() -> client.listMembers(CONVERSATION_ID)); + } + + @Test + public void testListMembersEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.listMembers; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ListMembersRequest request) { + return "/v1/conversations/"+request.conversationId+"/members/"; + } + + @Override + protected ListMembersRequest sampleRequest() { + var request = ListMembersRequest.builder() + .pageSize(PAGE_SIZE).order(ORDER).cursor(REQUEST_CURSOR).build(); + request.conversationId = CONVERSATION_ID; + return request; + } + + @Override + protected Map sampleQueryParams() { + return Map.of( + "cursor", REQUEST_CURSOR, + "page_size", String.valueOf(PAGE_SIZE), + "order", ORDER_STR + ); + } + } + .runTests(); + } + + + @Test + public void testGetMember() throws Exception { + stubResponse(200, SAMPLE_MEMBER_RESPONSE); + assertEqualsSampleMember(client.getMember(CONVERSATION_ID, MEMBER_ID)); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.getMember(MEMBER_ID, CONVERSATION_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.getMember(INVALID_UUID_STR, KNOCKING_ID_STR), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.getMember(null, MEMBER_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.getMember(CONVERSATION_ID, null), + IllegalArgumentException.class + ); + assertResponseExceptions(() -> client.getMember(CONVERSATION_ID, MEMBER_ID)); + + stubResponse(200, "{\"state\":\"limbo\"}"); + assertEqualsEmptyMember(client.getMember(CONVERSATION_ID, MEMBER_ID)); + + stubResponse(200, "{\"channel\":{\"type\":\"app\",\"from\":{\"type\":\"pigeon\"}}}"); + try { + fail(STR."Expected exception but got: \{client.getMember(CONVERSATION_ID, MEMBER_ID)}"); + } + catch (VonageResponseParseException ex) { + assertEquals(IllegalStateException.class, ex.getCause().getCause().getClass()); + } + + for (var ct : ChannelType.values()) { + stubResponse(200, STR."{\"channel\":{\"type\":\"\{ct}\",\"from\":{}}}"); + try { + var member = client.getMember(CONVERSATION_ID, MEMBER_ID); + var fromChannel = member.getChannel().getFrom(); + assertEquals(fromChannel.getClass(), Channel.getConcreteClass(ct)); + assertNull(fromChannel.getType()); + fromChannel.setTypeField(); + assertEquals(ct, fromChannel.getType()); + fromChannel.removeTypeField(); + assertNull(fromChannel.getType()); + } + catch (VonageResponseParseException ex) { + if (ct != ChannelType.APP) { + fail(ex); + } + assertEquals(IllegalStateException.class, ex.getCause().getCause().getClass()); + } + } + } + + @Test + public void testGetMemberEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.getMember; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ConversationResourceRequestWrapper request) { + return "/v1/conversations/"+request.conversationId+"/members/"+request.resourceId; + } + + @Override + protected ConversationResourceRequestWrapper sampleRequest() { + return new ConversationResourceRequestWrapper(CONVERSATION_ID, MEMBER_ID); + } + } + .runTests(); + } + + + @Test + public void testCreateMember() throws Exception { + var request = MEMBER_REQUEST_BUILDER_FACTORY.get().build(); + assertEqualsMinimalMember(request); + stubResponse(201, SAMPLE_MEMBER_RESPONSE); + var response = client.createMember(CONVERSATION_ID, request); + assertEqualsSampleMember(response); + assertEquals(request, response); + + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.createMember(INVALID_UUID_STR, MEMBER_REQUEST_BUILDER_FACTORY.get().build()), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.createMember(CONVERSATION_ID, null), + NullPointerException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.createMember(MEMBER_ID, MEMBER_REQUEST_BUILDER_FACTORY.get().build()), + IllegalArgumentException.class + ); + assertResponseExceptions(() -> + client.createMember(CONVERSATION_ID, MEMBER_REQUEST_BUILDER_FACTORY.get().build()) + ); + } + + @Test + public void testCreateMemberEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.createMember; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.POST; + } + + @Override + protected String expectedEndpointUri(Member request) { + return "/v1/conversations/"+request.getConversationId()+"/members/"; + } + + @Override + protected Member sampleRequest() { + var request = MEMBER_REQUEST_BUILDER_FACTORY.get() + .user(USER_NAME).knockingId(KNOCKING_ID_STR) + .memberIdInviting(MEMBER_ID_INVITING).from(MEMBER_FROM) + .media(MemberMedia.builder() + .audio(AUDIO).audioEnabled(AUDIO_ENABLED) + .muted(AUDIO_MUTED).earmuffed(AUDIO_EARMUFFED) + .build() + ) + .build(); + request.setConversationId(CONVERSATION_ID); + return request; + } + + @Override + protected String sampleRequestBodyString() { + return STR.""" + {"state":"\{MEMBER_STATE}","user":{"name":"\{USER_NAME}"},\ + "member_id_inviting":"\{MEMBER_ID_INVITING}","from":"\{MEMBER_FROM}",\ + "knocking_id":"\{KNOCKING_ID_STR}","channel":{"type":"\{CHANNEL_TYPE_STR}",\ + "from":{"type":"\{CHANNEL_TYPE_FROM_STR}","number":"\{FROM_NUMBER}"},\ + "to":{"type":"\{CHANNEL_TYPE_TO_STR}","number":"\{TO_NUMBER}"}},\ + "media":{"audio":\{AUDIO},"audio_settings":{"enabled":\{AUDIO_ENABLED},\ + "earmuffed":\{AUDIO_EARMUFFED},"muted":\{AUDIO_MUTED}}}}"""; + } + + @Override + public void runTests() throws Exception { + super.runTests(); + testInvalidUser(); + testUserNameIsSelectedWhenPassingInvalidUserId(); + testStateIsRequired(); + assertRequestUriAndBody(MEMBER_REQUEST_BUILDER_FACTORY.get().build(), getMinimalRequestBodyString()); + } + + String getMinimalRequestBodyString() { + return STR.""" + {"state":"\{MEMBER_STATE}","user":{"id":"\{USER_ID}"},"channel":{"type":\ + "\{CHANNEL_TYPE}","from":{"type":"\{CHANNEL_TYPE_FROM}","number":"\{FROM_NUMBER}"},\ + "to":{"type":"\{CHANNEL_TYPE_TO}","number":"\{TO_NUMBER}"}}}"""; + } + + void testInvalidUser() { + assertThrows(IllegalArgumentException.class, () -> + MEMBER_REQUEST_BUILDER_FACTORY.get().user(null).build() + ); + assertThrows(IllegalArgumentException.class, () -> + MEMBER_REQUEST_BUILDER_FACTORY.get().user(" \t").build() + ); + } + + void testUserNameIsSelectedWhenPassingInvalidUserId() { + var minimal = MEMBER_REQUEST_BUILDER_FACTORY.get().user(SESSION_ID).build(); + assertEquals(SESSION_ID, minimal.getUser().getName()); + assertNull(minimal.getUser().getId()); + } + + void testStateIsRequired() { + var minimalBuilder = MEMBER_REQUEST_BUILDER_FACTORY.get().state(null); + assertThrows(NullPointerException.class, minimalBuilder::build); + } + } + .runTests(); + } + + + @Test + public void testUpdateMember() throws Exception { + final var builder = UpdateMemberRequest.builder() + .conversationId(CONVERSATION_ID) + .memberId(MEMBER_ID) + .state(MemberState.JOINED) + .from(MEMBER_FROM); + + stubResponse(200, SAMPLE_MEMBER_RESPONSE); + assertEqualsSampleMember(client.updateMember(builder.build())); + + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.updateMember(null), + NullPointerException.class + ); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.updateMember(builder.memberId(KNOCKING_ID_STR).build()), + IllegalArgumentException.class + ); + builder.memberId(MEMBER_ID); + stubResponseAndAssertThrows(SAMPLE_MEMBER_RESPONSE, + () -> client.updateMember(builder.conversationId(MEMBER_ID_INVITING).build()), + IllegalArgumentException.class + ); + builder.conversationId(CONVERSATION_ID); + assertResponseExceptions(() -> client.updateMember(builder.build())); + } + + @Test + public void testUpdateMemberEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + final Supplier builderFactoryNoState = () -> + UpdateMemberRequest.builder().conversationId(CONVERSATION_ID).memberId(MEMBER_ID); + + @Override + protected RestEndpoint endpoint() { + return client.updateMember; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.PATCH; + } + + @Override + protected String expectedEndpointUri(UpdateMemberRequest request) { + return "/v1/conversations/"+request.conversationId+"/members/"+request.resourceId; + } + + @Override + protected UpdateMemberRequest sampleRequest() { + return builderFactoryNoState.get() + .state(MemberState.LEFT).from(MEMBER_FROM) + .code(REASON_CODE).text(REASON_TEXT).build(); + } + + @Override + protected String sampleRequestBodyString() { + var req = sampleRequest(); + return STR.""" + {"state":"LEFT","from":"\{req.getFrom()}",\ + "reason":{"code":"\{req.getCode()}","text":"\{req.getText()}"}}"""; + } + + @Override + public void runTests() throws Exception { + super.runTests(); + testInvalidState(); + testReasonCodeIsValidForLeftStateOnly(); + testReasonTextIsValidForLeftStateOnly(); + } + + void testInvalidState() { + for (var state : MemberState.values()) { + var builder = builderFactoryNoState.get().state(state); + switch (state) { + case LEFT, JOINED -> assertEquals(state, builder.build().getState()); + default -> assertThrows(IllegalArgumentException.class, builder::build); + } + } + } + + void testReasonCodeIsValidForLeftStateOnly() { + var builder = builderFactoryNoState.get().code(REASON_CODE); + assertThrows(NullPointerException.class, builder::build); + var valid = builder.state(MemberState.LEFT).build(); + assertEquals(REASON_CODE, valid.getCode()); + assertEquals(MemberState.LEFT, valid.getState()); + assertEquals(MEMBER_ID, valid.getMemberId()); + assertEquals(CONVERSATION_ID, valid.getConversationId()); + assertNull(valid.getFrom()); + assertNull(valid.getText()); + builder.state(MemberState.JOINED); + assertThrows(IllegalStateException.class, builder::build); + } + + void testReasonTextIsValidForLeftStateOnly() { + var builder = builderFactoryNoState.get().text(REASON_TEXT); + assertThrows(NullPointerException.class, builder::build); + var valid = builder.state(MemberState.LEFT).build(); + assertEquals(REASON_TEXT, valid.getText()); + assertEquals(MemberState.LEFT, valid.getState()); + assertEquals(MEMBER_ID, valid.getMemberId()); + assertEquals(CONVERSATION_ID, valid.getConversationId()); + assertNull(valid.getFrom()); + assertNull(valid.getCode()); + builder.state(MemberState.JOINED); + assertThrows(IllegalStateException.class, builder::build); + } + } + .runTests(); + } + + + @Test + public void testDeleteEvent() throws Exception { + stubResponseAndRun(204, () -> client.deleteEvent(CONVERSATION_ID, EVENT_ID)); + stubResponseAndAssertThrows(204, + () -> client.deleteEvent(null, EVENT_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(204, + () -> client.deleteEvent(MEMBER_ID, EVENT_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(204, + () -> client.deleteEvent(CONVERSATION_ID, -1), + IllegalArgumentException.class + ); + assert401ResponseException(() -> client.deleteEvent(CONVERSATION_ID, EVENT_ID)); + } + + @Test + public void testDeleteEventEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.deleteEvent; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.DELETE; + } + + @Override + protected String expectedEndpointUri(ConversationResourceRequestWrapper request) { + return "/v1/conversations/"+request.conversationId+"/events/"+request.resourceId; + } + + @Override + protected ConversationResourceRequestWrapper sampleRequest() { + return new ConversationResourceRequestWrapper(CONVERSATION_ID, String.valueOf(EVENT_ID)); + } + } + .runTests(); + } + + + @Test + public void testGetEvent() throws Exception { + stubResponse(200, SAMPLE_EVENT_RESPONSE); + assertEqualsSampleEvent(client.getEvent(CONVERSATION_ID, EVENT_ID)); + stubResponseAndAssertThrows(200, + () -> client.getEvent(null, EVENT_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.getEvent(USER_ID, EVENT_ID), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, + () -> client.getEvent(CONVERSATION_ID, -EVENT_ID), + IllegalArgumentException.class + ); + assert401ResponseException(() -> client.getEvent(CONVERSATION_ID, EVENT_ID)); + } + + @Test + public void testGetEventEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.getEvent; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ConversationResourceRequestWrapper request) { + return "/v1/conversations/"+request.conversationId+"/events/"+request.resourceId; + } + + @Override + protected ConversationResourceRequestWrapper sampleRequest() { + return new ConversationResourceRequestWrapper(CONVERSATION_ID, String.valueOf(EVENT_ID)); + } + } + .runTests(); + } + + @Test + public void testListEvents() throws Exception { + var request = ListEventsRequest.builder().build(); + stubResponse(200, SAMPLE_LIST_EVENTS_RESPONSE); + var response = client.listEvents(CONVERSATION_ID, request); + assertEqualsSampleListEvents(response); + stubResponse(200, SAMPLE_LIST_EVENTS_RESPONSE); + var events = client.listEvents(CONVERSATION_ID); + assertEquals(response.getEvents(), events); + + stubResponseAndAssertThrows(200, SAMPLE_LIST_EVENTS_RESPONSE, + () -> client.listEvents(null, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(200, SAMPLE_LIST_EVENTS_RESPONSE, + () -> client.listEvents(CONVERSATION_ID, null), + NullPointerException.class + ); + stubResponseAndAssertThrows(200, SAMPLE_LIST_EVENTS_RESPONSE, + () -> client.listEvents(MEMBER_ID_INVITING, request), + IllegalArgumentException.class + ); + assert401ResponseException(() -> client.listEvents(CONVERSATION_ID)); + } + + @Test + public void testListEventsEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.listEvents; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.GET; + } + + @Override + protected String expectedEndpointUri(ListEventsRequest request) { + return "/v1/conversations/"+request.conversationId+"/events/"; + } + + @Override + protected ListEventsRequest sampleRequest() { + var request = ListEventsRequest.builder() + .excludeDeletedEvents(EXCLUDE_DELETED_EVENTS) + .startId(EVENT_START_ID).endId(EVENT_END_ID) + .eventType(KNOWN_EVENT_TYPE).build(); + + assertEquals(EXCLUDE_DELETED_EVENTS, request.getExcludeDeletedEvents()); + assertEquals(EVENT_START_ID, request.getStartId()); + assertEquals(EVENT_END_ID, request.getEndId()); + assertEquals(KNOWN_EVENT_TYPE, request.getEventType()); + + return request; + } + + @Override + protected Map sampleQueryParams() { + return Map.of( + "exclude_deleted_events", String.valueOf(EXCLUDE_DELETED_EVENTS), + "start_id", String.valueOf(EVENT_START_ID), + "end_id", String.valueOf(EVENT_END_ID), + "event_type", KNOWN_EVENT_TYPE_STR + ); + } + } + .runTests(); + } + + @Test + public void testCreateEvent() throws Exception { + Event request = CustomEvent.builder().from(MEMBER_FROM).body(CONVERSATION_CUSTOM_DATA).build(); + stubResponse(201, SAMPLE_EVENT_RESPONSE); + assertEqualsSampleEvent(client.createEvent(CONVERSATION_ID, request)); + + stubResponseAndAssertThrows(201, SAMPLE_EVENT_RESPONSE, + () -> client.createEvent(USER_ID, request), + IllegalArgumentException.class + ); + stubResponseAndAssertThrows(201, SAMPLE_EVENT_RESPONSE, + () -> client.createEvent(CONVERSATION_ID, null), + NullPointerException.class + ); + assert401ResponseException(() -> client.createEvent(CONVERSATION_ID, request)); + } + + @Test + public void testCreateEventEndpoint() throws Exception { + new ConversationsEndpointTestSpec() { + + @Override + protected RestEndpoint endpoint() { + return client.createEvent; + } + + @Override + protected HttpMethod expectedHttpMethod() { + return HttpMethod.POST; + } + + @Override + protected String expectedEndpointUri(Event request) { + return "/v1/conversations/"+request.conversationId+"/events/"; + } + + @Override + protected AudioPlayStopEvent sampleRequest() { + return AudioPlayStopEvent.builder().from(MEMBER_FROM).playId(RANDOM_UUID).build(); + } + + @Override + protected String sampleRequestBodyString() { + var request = sampleRequest(); + return STR.""" + {"type":"\{request.getType()}","from":"\{request.getFrom()}","body":\ + {"play_id":"\{request.getPlayId()}"}}\ + """; + } + } + .runTests(); + } +} \ No newline at end of file diff --git a/src/test/java/com/vonage/client/conversations/ConversationsEndpointTestSpec.java b/src/test/java/com/vonage/client/conversations/ConversationsEndpointTestSpec.java new file mode 100644 index 000000000..77c5c05e2 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/ConversationsEndpointTestSpec.java @@ -0,0 +1,39 @@ +/* + * 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.conversations; + +import com.vonage.client.*; +import com.vonage.client.auth.*; +import java.util.*; + +abstract class ConversationsEndpointTestSpec extends DynamicEndpointTestSpec { + + @Override + protected Collection> expectedAuthMethods() { + return Arrays.asList(JWTAuthMethod.class); + } + + @Override + protected Class expectedResponseExceptionType() { + return ConversationsResponseException.class; + } + + @Override + protected String expectedDefaultBaseUri() { + return "https://api.nexmo.com"; + } +} + diff --git a/src/test/java/com/vonage/client/conversations/EventDeleteEventTest.java b/src/test/java/com/vonage/client/conversations/EventDeleteEventTest.java new file mode 100644 index 000000000..4b683ba85 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/EventDeleteEventTest.java @@ -0,0 +1,63 @@ +/* + * 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.conversations; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.Random; + +public class EventDeleteEventTest extends AbstractEventTest { + + @Test + public void testParseDeleteEvent() { + int bodyId = new Random().nextInt(); + var event = parseEvent(EventType.EVENT_DELETE, EventDeleteEvent.class, STR.""" + { + "id": \{randomEventId}, + "type": "event:delete", + "from": "\{from}", + "body": { + "event_id": "\{bodyId}" + }, + "timestamp": "2020-01-01T14:00:00.00Z", + "_embedded": { + "from_user": { + "id": "USR-82e028d9-5201-4f1e-8188-604b2d3471ec", + "name": "my_user_name", + "display_name": "My User Name", + "image_url": "https://example.com/image.png", + "custom_data": {} + }, + "from_member": { + "id": "string" + } + }, + "_links": { + "self": { + "href": "string" + } + } + } + """ + ); + assertEquals(randomEventId, event.getId()); + assertEquals(bodyId, event.getEventId()); + assertNotNull(event.getTimestamp()); + assertNotNull(event.getFromUser()); + assertNotNull(event.getFromMember()); + } +} diff --git a/src/test/java/com/vonage/client/conversations/EventTypeTest.java b/src/test/java/com/vonage/client/conversations/EventTypeTest.java new file mode 100644 index 000000000..b40b48f65 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/EventTypeTest.java @@ -0,0 +1,56 @@ +/* + * 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.conversations; + +import static com.vonage.client.conversations.EventType.*; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class EventTypeTest extends AbstractEventTest { + + @Test + public void testCustomEvent() { + assertEquals(CUSTOM, fromString("CusToM:TesT:foo_bar-BAZ")); + assertEquals(CUSTOM, fromString("CUSTOM:")); + assertEquals(CUSTOM, fromString("custom")); + } + + @Test + public void testUnknownEvent() { + assertEquals(UNKNOWN, fromString("alien:invasion")); + assertEquals(UNKNOWN, fromString("customer_purchase")); + } + + @Test + public void testMachineDetection() { + assertEquals("sip:amd_machine", SIP_AMD_MACHINE.toString()); + assertEquals(SIP_AMD_MACHINE, fromString("sip:amd_machine")); + assertEquals(SIP_AMD_MACHINE, fromString("sip:amd:machine")); + assertEquals(SIP_AMD_MACHINE, fromString("sip_amd:machine")); + assertEquals(UNKNOWN, fromString("sip-amd:machine")); + assertEquals(UNKNOWN, fromString("sip:amd-machine")); + assertEquals(SIP_MACHINE, fromString("sip_machine")); + assertEquals(SIP_MACHINE, fromString("sip:machine")); + assertEquals(UNKNOWN, fromString("sip:amd")); + } + + @Test + public void testEmptyAndNull() { + assertNull(fromString(null)); + assertNull(fromString(" ")); + } +} diff --git a/src/test/java/com/vonage/client/conversations/GenericEventTest.java b/src/test/java/com/vonage/client/conversations/GenericEventTest.java new file mode 100644 index 000000000..d3ddad388 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/GenericEventTest.java @@ -0,0 +1,73 @@ +/* + * 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.conversations; + +import com.vonage.client.TestUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import java.util.List; +import java.util.Map; + +public class GenericEventTest extends AbstractEventTest { + + > void testGenericEvent( + B builder, EventType eventType, String eventTypeStr, + Class eventClass, Map body) { + + if (builder != null) { + var built = testBaseEvent(eventType, builder.body(body), body); + assertEquals(body, built.getBody()); + } + + GenericEvent parsed = parseEvent(eventType, eventClass, STR.""" + { + "id": \{randomEventId}, + "type": "\{eventTypeStr}", + "body": \{TestUtils.mapToJson(body)}, + "_links": {} + } + """ + ); + assertEquals(body, parsed.getBody()); + assertEquals(randomEventId, parsed.getId()); + } + + @Test + public void testEphemeralEvent() { + testGenericEvent(EphemeralEvent.builder(), + EventType.EPHEMERAL, + "ephemeral", + EphemeralEvent.class, + Map.of( + "foo", "Bar", + "bAz", 3, + "QUX", true, + "Cats", List.of("Lucy", "Jasper"), + "Table", Map.of("k1", "V1") + ) + ); + } + + @Test + public void testCustomEvent() { + testGenericEvent(CustomEvent.builder(), + EventType.CUSTOM, + "custom:test", + GenericEvent.class, + Map.of() + ); + } +} diff --git a/src/test/java/com/vonage/client/conversations/MessageEventTest.java b/src/test/java/com/vonage/client/conversations/MessageEventTest.java new file mode 100644 index 000000000..3897787bc --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/MessageEventTest.java @@ -0,0 +1,116 @@ +/* + * 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.conversations; + +import com.vonage.client.OrderedJsonMap; +import static com.vonage.client.OrderedJsonMap.entry; +import com.vonage.client.common.MessageType; +import static com.vonage.client.common.MessageType.*; +import static com.vonage.client.conversations.MessageEvent.builder; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; +import java.net.URI; + +public class MessageEventTest extends AbstractEventTest { + final String text = "Hello world", url = "https://www.example.com/path/to/media.flv"; + + MessageEvent testMessageEvent(MessageEvent.Builder builder, OrderedJsonMap fields) { + return testBaseEvent(EventType.MESSAGE, builder, fields); + } + + @Test + public void testUrlMediaEvents() { + MessageType[] messageTypes = {IMAGE, AUDIO, VIDEO, FILE, VCARD}; + + for (var messageType : messageTypes) { + var event = testMessageEvent( + MessageEvent.builder(messageType).url(url), + new OrderedJsonMap( + entry("message_type", messageType), + entry(messageType.toString(), entry("url", url)) + ) + ); + assertEquals(messageType, event.getMessageType()); + assertEquals(URI.create(url), event.getUrl()); + assertNull(event.getText()); + assertNull(event.getLocation()); + + assertThrows(IllegalStateException.class, () -> + applyBaseFields(builder(messageType).text(text)).build() + ); + } + } + + @Test + public void testTextEvent() { + var event = testMessageEvent( + builder(TEXT).text(text), + new OrderedJsonMap( + entry("message_type", "text"), + entry("text", text) + ) + ); + assertEquals(text, event.getText()); + assertNull(event.getUrl()); + assertNull(event.getLocation()); + + assertThrows(IllegalStateException.class, () -> + applyBaseFields(builder(TEXT).url(url)).build() + ); + } + + @Test + public void testLocationEvent() { + String name = "Vonage", address = "15 Bonhill St, London EC2A 4DN"; + double latitude = 51.52350767741431, longitude = -0.08532428836304748; + var location = Location.builder() + .latitude(latitude).longitude(longitude) + .name(name).address(address).build(); + + var event = testMessageEvent( + builder(LOCATION).location(location), + new OrderedJsonMap( + entry("message_type", "location"), + entry("location", new OrderedJsonMap( + entry("longitude", longitude), + entry("latitude", latitude), + entry("name", name), + entry("address", address) + )) + ) + ); + + assertNull(event.getUrl()); + assertNull(event.getText()); + var parsedLocation = event.getLocation(); + assertNotNull(parsedLocation); + assertEquals(location, parsedLocation); + assertEquals(name, parsedLocation.getName()); + assertEquals(address, parsedLocation.getAddress()); + assertEquals(longitude, parsedLocation.getLongitude()); + assertEquals(latitude, parsedLocation.getLatitude()); + + assertThrows(IllegalStateException.class, () -> + applyBaseFields(builder(LOCATION).text(text)).build() + ); + assertThrows(IllegalStateException.class, () -> + applyBaseFields(builder(LOCATION).url(url)).build() + ); + assertThrows(IllegalStateException.class, () -> + applyBaseFields(builder(TEXT).location(location)).build() + ); + } +} diff --git a/src/test/java/com/vonage/client/conversations/MessageStatusEventTest.java b/src/test/java/com/vonage/client/conversations/MessageStatusEventTest.java new file mode 100644 index 000000000..1d8a53029 --- /dev/null +++ b/src/test/java/com/vonage/client/conversations/MessageStatusEventTest.java @@ -0,0 +1,58 @@ +/* + * 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.conversations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.Map; + +public class MessageStatusEventTest extends AbstractEventTest { + + @SuppressWarnings("unchecked") + > void testMessageEvent( + B builder, EventType type) { + + var event = testBaseEvent(type, + builder.eventId(randomEventId), + Map.of("event_id", randomEventId) + ); + assertEquals(randomEventId, event.getEventId()); + var eventClass = (Class) builder.getClass().getEnclosingClass(); + E parsed = parseEvent(type, eventClass, STR.""" + { + "id": 123, + "body": {}, + "type": "\{type}" + } + """ + ); + assertEquals(123, parsed.getId()); + assertNull(parsed.getEventId()); + assertNull(parsed.getFrom()); + assertNull(parsed.getFromMember()); + assertNull(parsed.getTimestamp()); + } + + @Test + public void testMessageStatusEvents() { + testMessageEvent(MessageSeenEvent.builder(), EventType.MESSAGE_SEEN); + testMessageEvent(MessageSubmittedEvent.builder(), EventType.MESSAGE_SUBMITTED); + testMessageEvent(MessageRejectedEvent.builder(), EventType.MESSAGE_REJECTED); + testMessageEvent(MessageDeliveredEvent.builder(), EventType.MESSAGE_DELIVERED); + testMessageEvent(MessageUndeliverableEvent.builder(), EventType.MESSAGE_UNDELIVERABLE); + } +} diff --git a/src/test/java/com/vonage/client/incoming/NotifyEventTest.java b/src/test/java/com/vonage/client/incoming/NotifyEventTest.java index 63f3a339d..1bbbc2dd9 100644 --- a/src/test/java/com/vonage/client/incoming/NotifyEventTest.java +++ b/src/test/java/com/vonage/client/incoming/NotifyEventTest.java @@ -49,7 +49,7 @@ public void testDeserializeFromJson_fail() { try { NotifyEvent.fromJson(json); } catch (VonageUnexpectedException e) { - assertEquals("Failed to produce NotifyEvent from json.", e.getMessage()); + assertEquals("Failed to produce NotifyEvent from JSON.", e.getMessage()); } } diff --git a/src/test/java/com/vonage/client/proactiveconnect/ProactiveConnectClientTest.java b/src/test/java/com/vonage/client/proactiveconnect/ProactiveConnectClientTest.java index 91ce42b3f..82ecfe887 100644 --- a/src/test/java/com/vonage/client/proactiveconnect/ProactiveConnectClientTest.java +++ b/src/test/java/com/vonage/client/proactiveconnect/ProactiveConnectClientTest.java @@ -732,7 +732,7 @@ protected String expectedEndpointUri(HalRequestWrapper request) { @Override protected HalRequestWrapper sampleRequest() { - return new HalRequestWrapper(3, 25, SortOrder.ASC, null); + return new HalRequestWrapper(3, 25, com.vonage.client.common.SortOrder.ASCENDING, null); } @Override @@ -1029,7 +1029,7 @@ protected String expectedEndpointUri(HalRequestWrapper request) { @Override protected HalRequestWrapper sampleRequest() { - return new HalRequestWrapper(7, 30, SortOrder.DESC, SAMPLE_LIST_ID.toString()); + return new HalRequestWrapper(7, 30, com.vonage.client.common.SortOrder.DESCENDING, SAMPLE_LIST_ID.toString()); } @Override diff --git a/src/test/java/com/vonage/client/subaccounts/SubaccountsClientTest.java b/src/test/java/com/vonage/client/subaccounts/SubaccountsClientTest.java index 5ff648634..9651c1820 100644 --- a/src/test/java/com/vonage/client/subaccounts/SubaccountsClientTest.java +++ b/src/test/java/com/vonage/client/subaccounts/SubaccountsClientTest.java @@ -202,7 +202,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected String expectedEndpointUri(UpdateSubaccountRequest request) { - return "/accounts/"+apiKey+"/subaccounts/"+request.subaccountApiKey; + return "/accounts/"+ API_KEY +"/subaccounts/"+request.subaccountApiKey; } @Override @@ -251,7 +251,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected String expectedEndpointUri(Void request) { - return "/accounts/"+apiKey+"/subaccounts"; + return "/accounts/"+ API_KEY +"/subaccounts"; } @Override @@ -284,7 +284,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected String expectedEndpointUri(String request) { - return "/accounts/"+apiKey+"/subaccounts/"+request; + return "/accounts/"+ API_KEY +"/subaccounts/"+request; } @Override @@ -332,7 +332,7 @@ protected HttpMethod expectedHttpMethod() { protected String expectedEndpointUri(ListTransfersFilter request) { assertNotNull(request.getStartDate()); assertNotNull(request.getEndDate()); - return "/accounts/"+apiKey+"/credit-transfers"; + return "/accounts/"+ API_KEY +"/credit-transfers"; } @Override @@ -377,7 +377,7 @@ protected String expectedEndpointUri(ListTransfersFilter request) { assertNotNull(request.getStartDate()); assertNull(request.getEndDate()); assertNull(request.getSubaccount()); - return "/accounts/"+apiKey+"/balance-transfers"; + return "/accounts/"+ API_KEY +"/balance-transfers"; } @Override @@ -400,7 +400,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected String expectedEndpointUri(MoneyTransfer request) { - return "/accounts/"+apiKey+"/"+name()+"-transfers"; + return "/accounts/"+ API_KEY +"/"+name()+"-transfers"; } @Override @@ -514,7 +514,7 @@ protected HttpMethod expectedHttpMethod() { @Override protected String expectedEndpointUri(NumberTransfer request) { - return "/accounts/"+apiKey+"/transfer-number"; + return "/accounts/"+ API_KEY +"/transfer-number"; } @Override diff --git a/src/test/java/com/vonage/client/users/UserTest.java b/src/test/java/com/vonage/client/users/UserTest.java index 66a490c40..ee28f8493 100644 --- a/src/test/java/com/vonage/client/users/UserTest.java +++ b/src/test/java/com/vonage/client/users/UserTest.java @@ -41,7 +41,7 @@ public void testAllParamsEmptyChannels() throws Exception { User request = User.builder().channels() .name(name).displayName(displayName) .imageUrl(imageUrl).customData(customData).build(); - request.setId(id); + request.id = id; String json = request.toJson(); assertTrue(json.contains("\"id\":\""+id+"\"")); @@ -203,19 +203,20 @@ public void testAllChannelsOneOfEach() { @Test public void testAllChannelsEmpty() { - String json = "{\n" + - " \"channels\": {\n" + - " \"pstn\": [],\n" + - " \"sip\": [],\n" + - " \"vbc\": [],\n" + - " \"websocket\": [],\n" + - " \"sms\": [],\n" + - " \"mms\": [],\n" + - " \"whatsapp\": [],\n" + - " \"viber\": [],\n" + - " \"messenger\": []\n" + - " }\n" + - "}"; + String json = """ + { + "channels": { + "pstn": [], + "sip": [], + "vbc": [], + "websocket": [], + "sms": [], + "mms": [], + "whatsapp": [], + "viber": [], + "messenger": [] + } + }"""; User parsed = User.fromJson(json); TestUtils.testJsonableBaseObject(parsed); Channels channels = parsed.getChannels(); diff --git a/src/test/java/com/vonage/client/video/VideoClientTest.java b/src/test/java/com/vonage/client/video/VideoClientTest.java index 1a044e130..a941be557 100644 --- a/src/test/java/com/vonage/client/video/VideoClientTest.java +++ b/src/test/java/com/vonage/client/video/VideoClientTest.java @@ -813,10 +813,10 @@ protected String sampleRequestBodyString() { @Test public void testCreateSessionEndpoint() throws Exception { - new VideoEndpointTestSpec() { + new VideoEndpointTestSpec() { @Override - protected RestEndpoint endpoint() { + protected RestEndpoint endpoint() { return client.createSession; } diff --git a/src/test/java/com/vonage/client/voice/CallTest.java b/src/test/java/com/vonage/client/voice/CallTest.java index d0ae256af..f57bd0eea 100644 --- a/src/test/java/com/vonage/client/voice/CallTest.java +++ b/src/test/java/com/vonage/client/voice/CallTest.java @@ -231,7 +231,7 @@ public void testMalformedJson() throws Exception { Call.fromJson("{\n" + " \"unknownProperty\": \"unknown\"\n" + "}"); fail("Expected a VonageUnexpectedException to be thrown"); } catch (VonageResponseParseException e) { - assertEquals("Failed to produce Call from json.", e.getMessage()); + assertEquals("Failed to produce Call from JSON.", e.getMessage()); } }