Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-08
28 changes: 28 additions & 0 deletions openspec/changes/archive/2026-10-21-client-data-service/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Context

The `GET /v5/users/current/clientdata` endpoint returns personal client data for the authenticated user, aggregated across custom folders, multi-chat workspaces, and distribution lists for application `symphonyPrime`.

This design introduces a first-class `ClientDataService` and its OBO counterpart `OboClientDataService` in `symphony-bdk-core`, wired into `ServiceFactory`, `SymphonyBdk`, `OboServices`, and Spring Boot auto-configurations.

## Goals / Non-Goals

**Goals:**
- Provide a first-class service for retrieving client data (`ClientDataResponse`), folders, workspaces, and distribution lists.
- Support both bot service account sessions and On-Behalf-Of (OBO) user sessions.
- Provide automatic retry with recovery on 401 unauthorized (token refresh) via `RetryWithRecoveryBuilder`.
- Provide Spring Boot starter auto-configurations for bot and OBO modes.
- Align with upstream FINOS `symphony-api-spec` updates (`MessagesApi` replacing `StreamContextApi`).

**Non-Goals:**
- Modifying or updating client data (endpoint is read-only).
- Creating or managing distribution list recipients.

## Decisions

**D1 — Package Structure.** The service is placed in its own dedicated package, `com.symphony.bdk.core.service.clientdata`, alongside other functional service packages (`stream`, `message`, `user`, etc.). It includes `package-info.java` annotated with `@NullMarked`.

**D2 — Service Interface and Ergonomics.** Following BDK conventions (e.g., `UserService` and `OboUserService`), `ClientDataService` implements `OboClientDataService` and `OboService<OboClientDataService>`. In addition to the full aggregated `getClientData()` method, convenience methods (`getUserFolders()`, `getUserWorkspaces()`, `getUserDistributionLists()`) extract sub-components directly.

**D3 — OBO Support.** Calling `.obo(AuthSession)` on `ClientDataService` creates a new instance scoped to the user session, while maintaining the same retry policies. `OboServices.clientData()` provides entry-point access.

**D4 — Upstream MessagesApi Alignment.** OpenAPI specification commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c` on FINOS includes PR #238, which changed the OpenAPI tag for `/v1/stream/{sid}/context` from `StreamContext` to `Messages`. Consequently, `StreamContextApi` was removed by OpenAPI generator and its operation consolidated into `MessagesApi`. `StreamService`, `ServiceFactory`, and Spring starter configurations are updated to use `MessagesApi` directly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Why

Symphony users organize personal workflows and conversation spaces in the client through custom folders (grouping conversation streams), multi-chat workspaces, and distribution lists. Previously, the BDK Java SDK had no service to access this client-side organization data, requiring bots or extension applications to either manage state externally or call underlying REST endpoints without BDK retry, auth session, or pagination support.

With the addition of the `GET /v5/users/current/clientdata` endpoint in SBE Users and its formal specification in `symphony-api-spec`, BDK Java needs a first-class service to expose this data cleanly to bots and OBO user sessions.

## What Changes

- **New package `com.symphony.bdk.core.service.clientdata`** containing:
- `ClientDataService`: Service implementing `OboClientDataService` and `OboService<OboClientDataService>` with automatic retry and 401 session token recovery.
- `OboClientDataService`: Service interface exposing OBO-enabled client data methods.
- `package-info.java`: Annotated with `@NullMarked`.
- **Client Data Methods**:
- `getClientData()`: Retrieves aggregated client data (`ClientDataResponse`) containing folders, workspaces, and distribution lists for application `symphonyPrime`.
- `getUserFolders()`: Convenience accessor returning list of `ClientDataStructure` folders.
- `getUserWorkspaces()`: Convenience accessor returning list of `ClientDataStructure` workspaces.
- `getUserDistributionLists()`: Convenience accessor returning list of `DistributionLists`.
- **BDK Entry Point Access**:
- `SymphonyBdk.clientData()` and `SymphonyBdk.getClientDataService()` accessors.
- `OboServices.clientData()` and `OboServices.getClientDataService()` accessors.
- `ServiceFactory.getClientDataService()` factory method.
- **Spring Boot Starter Configuration**:
- `@Bean clientDataService(...)` auto-configured in `BdkServiceConfig`.
- `@Bean oboClientDataService(...)` auto-configured in `BdkOboServiceConfig`.
- **OpenAPI Specification Bump & Upstream Alignment**:
- Bumped `apiBaseUrl` in `symphony-bdk-core/build.gradle` to FINOS `symphony-api-spec` commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c`.
- Aligned with upstream PR #238 (`BE-10206`): migrated `StreamService`, `ServiceFactory`, and Spring starter configurations from `StreamContextApi` to `MessagesApi` following the `/v1/stream/{sid}/context` OpenAPI tag update.
- **Testing**:
- Comprehensive unit test coverage in `ClientDataServiceTest` covering successful responses, empty responses, recipient details, OBO execution, and unauthorized state exceptions.
- Updated `StreamServiceTest`, `ServiceFactoryTest`, and `OboServicesTest`.

## Capabilities

### New Capabilities
- `client-data-service`: Access to the authenticated user's personal client data structures (custom folders, workspaces, and distribution lists) via bot and OBO sessions.

### Modified Capabilities
*(none)*

## Impact

**Code**:
- New service package `com.symphony.bdk.core.service.clientdata`
- `SymphonyBdk.java`, `OboServices.java`, `ServiceFactory.java`: added client data accessors
- `StreamService.java`, `ServiceFactory.java`: updated to use `MessagesApi`
- `BdkServiceConfig.java`, `BdkOboServiceConfig.java`, `BdkApiClientsConfig.java`: Spring starter bean configurations updated
- `symphony-bdk-core/build.gradle`: bumped `apiBaseUrl`

**Dependencies**:
- No new external runtime dependencies.

**Docs**:
- Updated documentation and capability specs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## ADDED Requirements

### Requirement: Client Data Retrieval
`ClientDataService` and `OboClientDataService` SHALL retrieve the authenticated user's aggregated client data from the `GET /v5/users/current/clientdata` endpoint, returning a `ClientDataResponse` containing the user's folders, workspaces, and distribution lists for application `symphonyPrime`.

#### Scenario: Successful client data retrieval
- **WHEN** `getClientData()` is called with an active bot or OBO session
- **THEN** the client data endpoint is queried and a populated `ClientDataResponse` is returned

#### Scenario: Unauthenticated invocation
- **WHEN** `getClientData()` is called on an unauthenticated service instance without an active `AuthSession`
- **THEN** an `IllegalStateException` is thrown before any network call is initiated

### Requirement: Folders and Workspaces Access
`ClientDataService` and `OboClientDataService` SHALL provide convenience accessors `getUserFolders()` and `getUserWorkspaces()` that return lists of `ClientDataStructure` elements from the aggregated client data.

#### Scenario: Retrieve user folders
- **WHEN** `getUserFolders()` is called
- **THEN** the list of `ClientDataStructure` folders is returned, each containing `id`, `name`, and conversation `streamIds`

#### Scenario: Retrieve user workspaces
- **WHEN** `getUserWorkspaces()` is called
- **THEN** the list of `ClientDataStructure` workspaces is returned, each containing `id`, `name`, and multi-chat `streamIds`

### Requirement: Distribution Lists Access
`ClientDataService` and `OboClientDataService` SHALL provide a convenience accessor `getUserDistributionLists()` returning the list of `DistributionLists` from the aggregated client data, each containing recipients of type user, stream, or folder.

#### Scenario: Retrieve distribution lists
- **WHEN** `getUserDistributionLists()` is called
- **THEN** the list of `DistributionLists` is returned with recipient IDs, types, user names, and folder stream IDs

### Requirement: OBO Session Support
`ClientDataService` SHALL implement `OboService<OboClientDataService>` such that calling `.obo(AuthSession)` produces an instance scoped to the specified user session, using the user session token for authentication.

#### Scenario: OBO execution
- **WHEN** `.obo(oboSession)` is invoked with an OBO auth session
- **THEN** subsequent calls execute against `/v5/users/current/clientdata` using the OBO session token

### Requirement: BDK Entry Point Availability
`SymphonyBdk` and `OboServices` SHALL expose `clientData()` and `getClientDataService()` accessors returning `ClientDataService` and `OboClientDataService` respectively. `ServiceFactory` SHALL provide `getClientDataService()`.

#### Scenario: Entry point retrieval
- **WHEN** `bdk.clientData()` or `oboServices.clientData()` is invoked
- **THEN** a fully initialized client data service is returned
26 changes: 26 additions & 0 deletions openspec/changes/archive/2026-10-21-client-data-service/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## 1. OpenAPI Specification Bump & Code Generation

- [x] 1.1 Bump `apiBaseUrl` in `symphony-bdk-core/build.gradle` to FINOS `symphony-api-spec` commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c`
- [x] 1.2 Generate OpenAPI client and models: `ClientDataApi`, `ClientDataResponse`, `ClientDataStructure`, `DistributionLists`, `DistributionListRecipient`
- [x] 1.3 Align `StreamService` and `ServiceFactory` with `MessagesApi` replacing `StreamContextApi`

## 2. Core Service Implementation

- [x] 2.1 Create `com.symphony.bdk.core.service.clientdata.OboClientDataService` interface declaring `getClientData()`, `getUserFolders()`, `getUserWorkspaces()`, `getUserDistributionLists()`
- [x] 2.2 Create `com.symphony.bdk.core.service.clientdata.ClientDataService` implementing `OboClientDataService` and `OboService<OboClientDataService>` with `RetryWithRecoveryBuilder`
- [x] 2.3 Add `getClientDataService()` factory method to `ServiceFactory`
- [x] 2.4 Add `clientData()` and `getClientDataService()` accessors to `SymphonyBdk`
- [x] 2.5 Add `clientData()` and `getClientDataService()` accessors to `OboServices`
- [x] 2.6 Add `package-info.java` annotated with `@NullMarked`

## 3. Spring Boot Auto-Configuration

- [x] 3.1 Register `@Bean clientDataService` in `BdkServiceConfig`
- [x] 3.2 Register `@Bean oboClientDataService` in `BdkOboServiceConfig`
- [x] 3.3 Update `BdkApiClientsConfig`, `BdkServiceConfig`, and `BdkOboServiceConfig` for `MessagesApi`

## 4. Testing & Verification

- [x] 4.1 Unit tests in `ClientDataServiceTest` covering `getClientData`, `getUserFolders`, `getUserWorkspaces`, `getUserDistributionLists`, empty response, OBO mode, and unauthenticated errors
- [x] 4.2 Update `StreamServiceTest`, `ServiceFactoryTest`, and `OboServicesTest`
- [x] 4.3 Run full Gradle build, checks, and test coverage verification
49 changes: 49 additions & 0 deletions openspec/specs/client-data-service/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# client-data-service Specification

## Purpose
Provides access to personal client data (custom folders, multi-chat workspaces, and distribution lists) for authenticated bot and On-Behalf-Of (OBO) user sessions via the `GET /v5/users/current/clientdata` Symphony endpoint.

## Requirements

### Requirement: Client Data Retrieval
`ClientDataService` and `OboClientDataService` SHALL retrieve the authenticated user's aggregated client data from the `GET /v5/users/current/clientdata` endpoint, returning a `ClientDataResponse` containing the user's folders, workspaces, and distribution lists for application `symphonyPrime`.

#### Scenario: Successful client data retrieval
- **WHEN** `getClientData()` is called with an active bot or OBO session
- **THEN** the client data endpoint is queried and a populated `ClientDataResponse` is returned

#### Scenario: Unauthenticated invocation
- **WHEN** `getClientData()` is called on an unauthenticated service instance without an active `AuthSession`
- **THEN** an `IllegalStateException` is thrown before any network call is initiated

### Requirement: Folders and Workspaces Access
`ClientDataService` and `OboClientDataService` SHALL provide convenience accessors `getUserFolders()` and `getUserWorkspaces()` that return lists of `ClientDataStructure` elements from the aggregated client data.

#### Scenario: Retrieve user folders
- **WHEN** `getUserFolders()` is called
- **THEN** the list of `ClientDataStructure` folders is returned, each containing `id`, `name`, and conversation `streamIds`

#### Scenario: Retrieve user workspaces
- **WHEN** `getUserWorkspaces()` is called
- **THEN** the list of `ClientDataStructure` workspaces is returned, each containing `id`, `name`, and multi-chat `streamIds`

### Requirement: Distribution Lists Access
`ClientDataService` and `OboClientDataService` SHALL provide a convenience accessor `getUserDistributionLists()` returning the list of `DistributionLists` from the aggregated client data, each containing recipients of type user, stream, or folder.

#### Scenario: Retrieve distribution lists
- **WHEN** `getUserDistributionLists()` is called
- **THEN** the list of `DistributionLists` is returned with recipient IDs, types, user names, and folder stream IDs

### Requirement: OBO Session Support
`ClientDataService` SHALL implement `OboService<OboClientDataService>` such that calling `.obo(AuthSession)` produces an instance scoped to the specified user session, using the user session token for authentication.

#### Scenario: OBO execution
- **WHEN** `.obo(oboSession)` is invoked with an OBO auth session
- **THEN** subsequent calls execute against `/v5/users/current/clientdata` using the OBO session token

### Requirement: BDK Entry Point Availability
`SymphonyBdk` and `OboServices` SHALL expose `clientData()` and `getClientDataService()` accessors returning `ClientDataService` and `OboClientDataService` respectively. `ServiceFactory` SHALL provide `getClientDataService()`.

#### Scenario: Entry point retrieval
- **WHEN** `bdk.clientData()` or `oboServices.clientData()` is invoked
- **THEN** a fully initialized client data service is returned
2 changes: 1 addition & 1 deletion symphony-bdk-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ dependencies {
}

// OpenAPI code generation
def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a8efe8a6193769d0657fc57758918138aa4483bb"
def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a0b152881a0405b7510cfc7e3a9698e8cb8c566c"
def generatedFolder = "${layout.buildDirectory.get()}/generated/openapi"
def apisToGenerate = [
Agent: 'agent/agent-api-public-deprecated.yaml',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.symphony.bdk.core.service.session.OboSessionService;
import com.symphony.bdk.core.service.signal.OboSignalService;
import com.symphony.bdk.core.service.stream.OboStreamService;
import com.symphony.bdk.core.service.clientdata.OboClientDataService;
import com.symphony.bdk.core.service.user.OboUserService;

import org.apiguardian.api.API;
Expand All @@ -25,6 +26,7 @@ public class OboServices {

private final OboStreamService oboStreamService;
private final OboUserService oboUserService;
private final OboClientDataService oboClientDataService;
private final OboMessageService oboMessageService;
private final OboPresenceService oboPresenceService;
private final OboConnectionService oboConnectionService;
Expand All @@ -46,6 +48,7 @@ public OboServices(

oboStreamService = serviceFactory.getStreamService();
oboUserService = serviceFactory.getUserService();
oboClientDataService = serviceFactory.getClientDataService();
oboMessageService = serviceFactory.getMessageService();
oboPresenceService = serviceFactory.getPresenceService();
oboConnectionService = serviceFactory.getConnectionService();
Expand All @@ -71,6 +74,24 @@ public OboUserService users() {
return oboUserService;
}

/**
* Get the {@link OboClientDataService} using the provided OBO session in constructor.
*
* @return an {@link OboClientDataService} instance with the provided OBO session.
*/
public OboClientDataService clientData() {
return oboClientDataService;
}

/**
* Get the {@link OboClientDataService} using the provided OBO session in constructor.
*
* @return an {@link OboClientDataService} instance with the provided OBO session.
*/
public OboClientDataService getClientDataService() {
return oboClientDataService;
}

/**
* Get the {@link OboPresenceService} using the provided OBO session in constructor.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.symphony.bdk.core.service.session.SessionService;
import com.symphony.bdk.core.service.signal.SignalService;
import com.symphony.bdk.core.service.stream.StreamService;
import com.symphony.bdk.core.service.clientdata.ClientDataService;
import com.symphony.bdk.core.service.user.UserService;
import com.symphony.bdk.gen.api.AppEntitlementApi;
import com.symphony.bdk.gen.api.ApplicationApi;
Expand All @@ -45,7 +46,6 @@
import com.symphony.bdk.gen.api.SessionApi;
import com.symphony.bdk.gen.api.ShareApi;
import com.symphony.bdk.gen.api.SignalsApi;
import com.symphony.bdk.gen.api.StreamContextApi;
import com.symphony.bdk.gen.api.StreamsApi;
import com.symphony.bdk.gen.api.SystemApi;
import com.symphony.bdk.gen.api.UserApi;
Expand Down Expand Up @@ -75,6 +75,7 @@
class ServiceFactory {

private final ApiClient podClient;
private final ApiClient usersClient;
private final ApiClient agentClient;
private final ApiClient datafeedAgentClient;
private final ApiClient datahoseAgentClient;
Expand All @@ -100,6 +101,7 @@ public ServiceFactory(
) {
this.config = config;
this.podClient = apiClientFactory.getPodClient();
this.usersClient = apiClientFactory.getUsersClient();
this.agentClient = apiClientFactory.getAgentClient();
this.datafeedAgentClient = apiClientFactory.getDatafeedAgentClient();
this.datahoseAgentClient = apiClientFactory.getDatahoseAgentClient();
Expand All @@ -120,6 +122,11 @@ public ServiceFactory(
this.podClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken));
this.podClient.addEnforcedAuthenticationScheme(BEARER_AUTH);

if (this.usersClient != null) {
this.usersClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken));
this.usersClient.addEnforcedAuthenticationScheme(BEARER_AUTH);
}

this.agentClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken));
this.agentClient.addEnforcedAuthenticationScheme(BEARER_AUTH);

Expand All @@ -142,14 +149,23 @@ public UserService getUserService() {
retryBuilder);
}

/**
* Returns a fully initialized {@link ClientDataService}.
*
* @return a new {@link ClientDataService} instance.
*/
public ClientDataService getClientDataService() {
return new ClientDataService(this.usersClient, this.authSession, this.retryBuilder);
}

/**
* Returns a fully initialized {@link StreamService}.
*
* @return a new {@link StreamService} instance.
*/
public StreamService getStreamService() {
return new StreamService(new StreamsApi(podClient), new RoomMembershipApi(podClient), new ShareApi(agentClient),
new StreamContextApi(agentClient), authSession, retryBuilder);
new MessagesApi(agentClient), authSession, retryBuilder);
}

public DisclaimerService getDisclaimerService() {
Expand Down
Loading
Loading