From 0195c909ae4f0cb08e9a04ef5be20c667a53e26d Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 4 May 2026 09:26:44 -0400 Subject: [PATCH] feat: Add listConnections API (DEVX-10074) Add GET /v2/project/{apiKey}/session/{sessionId}/connection support: - Connection model with connectionId, createdAt, connectionState (enum) - ConnectionList response with count, applicationId, sessionId, items - HttpClient.listConnections() method - OpenTok.listConnections(sessionId) public API - Tests with WireMock stub and validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/java/com/opentok/Connection.java | 73 +++++++++++++++++++ src/main/java/com/opentok/ConnectionList.java | 62 ++++++++++++++++ src/main/java/com/opentok/OpenTok.java | 22 +++++- .../java/com/opentok/util/HttpClient.java | 28 +++++++ src/test/java/com/opentok/OpenTokTest.java | 48 ++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/opentok/Connection.java create mode 100644 src/main/java/com/opentok/ConnectionList.java diff --git a/src/main/java/com/opentok/Connection.java b/src/main/java/com/opentok/Connection.java new file mode 100644 index 00000000..16fad234 --- /dev/null +++ b/src/main/java/com/opentok/Connection.java @@ -0,0 +1,73 @@ +/** + * OpenTok Java SDK + * Copyright (C) 2026 Vonage. + * http://www.tokbox.com + * + * Licensed under The MIT License (MIT). See LICENSE file for more information. + */ +package com.opentok; + +import com.fasterxml.jackson.annotation.*; + +/** + * Represents a connection in an OpenTok session. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Connection { + + /** + * The state of a connection in a session. + */ + public enum ConnectionState { + CONNECTING, CONNECTED, DISCONNECTED; + + @JsonCreator + public static ConnectionState fromString(String state) { + try { + return ConnectionState.valueOf(state.toUpperCase()); + } + catch (NullPointerException | IllegalArgumentException e) { + return null; + } + } + + @JsonValue + @Override + public String toString() { + return name().charAt(0) + name().substring(1).toLowerCase(); + } + } + + @JsonProperty private String connectionId; + @JsonProperty private long createdAt; + @JsonProperty private ConnectionState connectionState; + + protected Connection() { + } + + @JsonCreator + public static Connection makeConnection() { + return new Connection(); + } + + /** + * The connection ID. + */ + public String getConnectionId() { + return connectionId; + } + + /** + * The time at which the client connected, in milliseconds since the Unix epoch. + */ + public long getCreatedAt() { + return createdAt; + } + + /** + * The current state of the connection. + */ + public ConnectionState getConnectionState() { + return connectionState; + } +} diff --git a/src/main/java/com/opentok/ConnectionList.java b/src/main/java/com/opentok/ConnectionList.java new file mode 100644 index 00000000..c41f00e3 --- /dev/null +++ b/src/main/java/com/opentok/ConnectionList.java @@ -0,0 +1,62 @@ +/** + * OpenTok Java SDK + * Copyright (C) 2026 Vonage. + * http://www.tokbox.com + * + * Licensed under The MIT License (MIT). See LICENSE file for more information. + */ +package com.opentok; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a list of connections in an OpenTok session. + */ +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public class ConnectionList extends ArrayList { + + private int count; + private String projectId; + private String sessionId; + + /** + * The total number of connections in the session. + */ + public int getCount() { + return count; + } + + /** + * The API key (project ID) for the session. + */ + public String getProjectId() { + return projectId; + } + + /** + * The session ID. + */ + public String getSessionId() { + return sessionId; + } + + private void setCount(int count) { + this.count = count; + } + + private void setProjectId(String projectId) { + this.projectId = projectId; + } + + private void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + private void setItems(List connections) { + this.clear(); + this.addAll(connections); + } +} diff --git a/src/main/java/com/opentok/OpenTok.java b/src/main/java/com/opentok/OpenTok.java index f248ed48..411a2cb8 100644 --- a/src/main/java/com/opentok/OpenTok.java +++ b/src/main/java/com/opentok/OpenTok.java @@ -55,7 +55,8 @@ public class OpenTok { renderReader = new ObjectMapper().readerFor(Render.class), renderListReader = new ObjectMapper().readerForListOf(Render.class), connectReader = new ObjectMapper().readerFor(AudioConnector.class), - captionReader = new ObjectMapper().readerFor(Caption.class); + captionReader = new ObjectMapper().readerFor(Caption.class), + connectionListReader = new ObjectMapper().readerFor(ConnectionList.class); /** * Creates an OpenTok object. @@ -833,6 +834,25 @@ public StreamList listStreams(String sessionId) throws OpenTokException { } } + /** + * Gets a list of {@link Connection} objects for the given session ID. + * + * @param sessionId The session ID. + * + * @return The {@link ConnectionList} containing connection details. + */ + public ConnectionList listConnections(String sessionId) throws OpenTokException { + if (sessionId == null || sessionId.isEmpty()) { + throw new InvalidArgumentException("Session ID is null or empty."); + } + String connections = client.listConnections(sessionId); + try { + return connectionListReader.readValue(connections); + } catch (JsonProcessingException e) { + throw new RequestException("Exception mapping json: " + e.getMessage()); + } + } + /** * Dials a SIP gateway to connect it an OpenTok session. * diff --git a/src/main/java/com/opentok/util/HttpClient.java b/src/main/java/com/opentok/util/HttpClient.java index 75782ce5..51ee414e 100644 --- a/src/main/java/com/opentok/util/HttpClient.java +++ b/src/main/java/com/opentok/util/HttpClient.java @@ -1098,6 +1098,34 @@ public String listStreams(String sessionId) throws RequestException { } } + public String listConnections(String sessionId) throws RequestException { + String url = this.apiUrl + "/v2/project/" + this.apiKey + "/session/" + sessionId + "/connection"; + Future request = this.prepareGet(url) + .setHeader("Accept", "application/json") + .execute(); + + try { + Response response = request.get(); + switch (response.getStatusCode()) { + case 200: + return response.getResponseBody(); + case 400: + throw new RequestException(response.getResponseBody()); + case 403: + throw new RequestException("You passed in an invalid OpenTok API key or JWT token."); + case 404: + throw new RequestException("Session not found: " + sessionId); + case 500: + throw new RequestException("Could not list connections. A server error occurred."); + default: + throw new RequestException("Could not list connections. The server response was invalid." + + " response code: " + response.getStatusCode()); + } + } catch (InterruptedException | ExecutionException e) { + throw new RequestException("Could not list connections", e); + } + } + public String connectAudioStream(String sessionId, String token, AudioConnectorProperties properties) throws OpenTokException { String url = this.apiUrl + "/v2/project/" + this.apiKey + "/connect"; diff --git a/src/test/java/com/opentok/OpenTokTest.java b/src/test/java/com/opentok/OpenTokTest.java index 11dfee0c..2473fb03 100644 --- a/src/test/java/com/opentok/OpenTokTest.java +++ b/src/test/java/com/opentok/OpenTokTest.java @@ -2942,4 +2942,52 @@ public void testVonageShim() throws Exception { assertTrue(iat.plus(86402, ChronoUnit.SECONDS).isAfter(exp)); assertTrue(iat.plus(86398, ChronoUnit.SECONDS).isBefore(exp)); } + + @Test + public void testListConnections() throws OpenTokException { + String sessionID = "SESSIONID"; + String url = "/v2/project/" + apiKey + "/session/" + sessionID + "/connection"; + stubFor(get(urlEqualTo(url)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"count\": 2,\n" + + " \"projectId\": \"" + apiKey + "\",\n" + + " \"sessionId\": \"" + sessionID + "\",\n" + + " \"items\": [\n" + + " {\n" + + " \"connectionId\": \"527775e1-626e-42c3-b0e8-e2122d20661a\",\n" + + " \"createdAt\": 1747655658197,\n" + + " \"connectionState\": \"Connected\"\n" + + " },\n" + + " {\n" + + " \"connectionId\": \"c6db93f0-8692-438c-944b-cfbaf577c878\",\n" + + " \"createdAt\": 1747655658227,\n" + + " \"connectionState\": \"Disconnected\"\n" + + " }\n" + + " ]\n" + + "}"))); + ConnectionList connections = sdk.listConnections(sessionID); + assertNotNull(connections); + assertEquals(2, connections.getCount()); + assertEquals(String.valueOf(apiKey), connections.getProjectId()); + assertEquals(sessionID, connections.getSessionId()); + assertEquals(2, connections.size()); + Connection conn1 = connections.get(0); + assertEquals("527775e1-626e-42c3-b0e8-e2122d20661a", conn1.getConnectionId()); + assertEquals(1747655658197L, conn1.getCreatedAt()); + assertEquals(Connection.ConnectionState.CONNECTED, conn1.getConnectionState()); + Connection conn2 = connections.get(1); + assertEquals("c6db93f0-8692-438c-944b-cfbaf577c878", conn2.getConnectionId()); + assertEquals(1747655658227L, conn2.getCreatedAt()); + assertEquals(Connection.ConnectionState.DISCONNECTED, conn2.getConnectionState()); + + verify(getRequestedFor(urlMatching(url))); + assertTrue(TestHelpers.verifyTokenAuth(apiKey, apiSecret, + findAll(getRequestedFor(urlMatching(url))))); + TestHelpers.verifyUserAgent(); + assertThrows(InvalidArgumentException.class, () -> sdk.listConnections("")); + assertThrows(InvalidArgumentException.class, () -> sdk.listConnections(null)); + } } \ No newline at end of file