Skip to content
Open
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
25 changes: 25 additions & 0 deletions docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ public class Example {

> `PresentationMLParser.getTextContent(message.getMessage())` can be used on incoming messages to extract the message content
> stripped of all tags.

### Update a message with an attachment
An existing message can also be updated with new content and file attachments:
```java
@Slf4j
public class Example {
public static final String STREAM_ID = "gXFV8vN37dNqjojYS_y2wX___o2KxfmUdA";
public static final String MESSAGE_ID = "j7-VAoCY_1pGAYSye_MtI3___oPXo8VHbQ";

public static void main(String[] args) throws Exception {
final SymphonyBdk bdk = new SymphonyBdk(loadFromClasspath("/config.yaml"));

final InputStream attachment = Example.class.getResourceAsStream("/attachment.pdf");
final Message updatedContent = Message.builder()
.content("Updated message content")
.addAttachment(attachment, "attachment.pdf")
.build();

final V4Message updatedMessage = bdk.messages().update(STREAM_ID, MESSAGE_ID, updatedContent);
log.info("Message updated, id: " + updatedMessage.getMessageId());
}
}
```
> :information_source: Attachments and previews on message update are supported only starting from SBE v24.1.

## Using templates
The `Message.Builder` also allows you to build a message from a template. So far, the BDK supports two different template
engine implementations:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-06
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Context

`MessageService.update(streamId, messageId, content)` builds its multipart form with the private `getForm(Message)`, the same helper used by `send` and `blast`. That helper already adds the `attachment` and `preview` parts, so update already forwards attachments. `OboMessageService` declares the same `update` methods and `MessageService` implements both, so OBO shares the code path. When a `MessageSenderOverride` is registered, update is delegated to it and the form is not built by the BDK.

The gap is documentation and test coverage, not behavior: the javadoc says attachments are unsupported, and existing update tests only send text.

## Goals / Non-Goals

**Goals:**
- Make javadoc and docs match actual behavior, including the SBE v24.1 requirement.
- Lock the behavior in with tests that inspect the multipart request body.

**Non-Goals:**
- Changing production logic or method signatures.
- Promoting `@API(status = EXPERIMENTAL)` to stable (maintainer decision).
- Documenting or enforcing replace-vs-add semantics for existing attachments; unverified against a real pod.
- Guarding against pods older than SBE v24.1; the server's response is surfaced as-is.

## Decisions

- **Javadoc only, no code change.** Reuse of `getForm` already satisfies the API. Alternative considered: a dedicated update form builder; rejected as duplication with no benefit.
- **Assert on the multipart body in tests.** Existing tests use `mockApiClient.onPost(...)`, which only matches path and returns a canned response. The new tests need to capture the request (via the mock client's request inspection, or a spy/captor on `ApiClient.invokeAPI`) and check the `attachment`/`preview` parts. The exact capture mechanism is decided during implementation based on what `mockApiClient` exposes.
- **Docs example in `docs/message.md`** under "How to use", mirroring the existing send-with-attachment snippet, with a note about SBE v24.1.
- **Version note, no runtime check.** The BDK has no pod-version detection for messages; a note in javadoc and docs is enough.

## Risks / Trade-offs

- Real-pod behavior (do attachments replace or add to existing ones; is `message` required alongside) is unverified. Mitigation: docs state only what the API reference states, and the proposal flags it as open.
- If the mock client cannot expose the request body, the test may need a small helper or a spy on `ApiClient`, adding a little test infrastructure.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Why

The Update Message v4 endpoint accepts `attachment` and `preview` parts since SBE v24.1, and `MessageService.update(...)` already forwards them (it builds its multipart form with the same `getForm(message)` used by `send`). However, the javadoc on `MessageService` and `OboMessageService` still says "attachments are not supported yet", no test covers update-with-attachment, and the docs show no example. Users reasonably conclude the feature is missing.

## What Changes

- Correct the javadoc of both `update(...)` overloads in `MessageService` and `OboMessageService`: attachments and previews are supported, and require a pod on SBE v24.1 or later.
- Add tests asserting that `update(...)` sends `attachment` and `preview` multipart parts (regular and OBO).
- Add an update-with-attachment example to `docs/message.md`.
- No production logic change, no signature change, no change to the `@API(status = EXPERIMENTAL)` annotation (promotion to stable is a maintainer decision, out of scope).

## Capabilities

### New Capabilities
- `message-update-attachments`: contract that `MessageService.update(...)` (and its OBO variant) forwards `Message` attachments and previews to the Update Message v4 endpoint.

### Modified Capabilities

## Impact

- `symphony-bdk-core`: `MessageService.java`, `OboMessageService.java` (javadoc only); `MessageServiceTest.java` (new tests).
- `docs/message.md` (example).
- Open question, not resolved by this change: server-side semantics of attachments on update (replace vs add) have not been verified against a real pod; the docs will describe only what the API reference states.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## Purpose

Defines how the BDK forwards file attachments and previews when updating an existing message, so users can rely on update supporting the same content as send.

## ADDED Requirements

### Requirement: Update forwards attachments and previews
When a `Message` passed to a message update operation (bot or OBO) contains attachments or previews, the BDK SHALL send each of them as `attachment` and `preview` parts of the multipart body of the Update Message v4 request, exactly as it does for message creation. Attachment support on update requires a pod running SBE v24.1 or later.

#### Scenario: Update with attachment and preview
- **WHEN** a user updates a message with a `Message` containing one attachment and its preview
- **THEN** the Update Message v4 request contains one `attachment` part and one `preview` part with the provided filename and content type

#### Scenario: Update with multiple attachments
- **WHEN** a user updates a message with a `Message` containing two attachments
- **THEN** the request contains two `attachment` parts

#### Scenario: Update through OBO
- **WHEN** a user updates a message through `OboMessageService` with a `Message` containing an attachment
- **THEN** the request contains the `attachment` part, using the delegated session tokens

#### Scenario: Update without attachments
- **WHEN** a user updates a message with a `Message` containing only text
- **THEN** the request contains no `attachment` or `preview` parts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## 1. Javadoc

- [x] 1.1 In `MessageService.java`, replace "(attachments are not supported yet)" on both `update(...)` overloads with a statement that attachments and previews are supported and require SBE v24.1 or later
- [x] 1.2 Apply the same javadoc fix to both `update(...)` declarations in `OboMessageService.java`

## 2. Tests

- [x] 2.1 In `MessageServiceTest`, find how to capture the multipart request body from `mockApiClient` (or spy `ApiClient.invokeAPI`) and add a small helper if needed
- [x] 2.2 Add a test: update with one attachment and its preview sends one `attachment` and one `preview` part with the expected filename and content type
- [x] 2.3 Add a test: update with two attachments sends two `attachment` parts
- [x] 2.4 Add a test: update with text only sends no `attachment` or `preview` parts
- [x] 2.5 Add a test: update with an attachment through `messageService.obo(authSession)` sends the `attachment` part
- [x] 2.6 Run `./gradlew :symphony-bdk-core:test --tests "com.symphony.bdk.core.service.message.MessageServiceTest"`

## 3. Docs

- [x] 3.1 Add an "update a message with an attachment" snippet to `docs/message.md` under "How to use", with a note that attachments on update require SBE v24.1 or later
26 changes: 26 additions & 0 deletions openspec/specs/message-update-attachments/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Message-Update-Attachments Specification

## Purpose

Defines how the BDK forwards file attachments and previews when updating an existing message, so users can rely on update supporting the same content as send.

## Requirements

### Requirement: Update forwards attachments and previews
When a `Message` passed to a message update operation (bot or OBO) contains attachments or previews, the BDK SHALL send each of them as `attachment` and `preview` parts of the multipart body of the Update Message v4 request, exactly as it does for message creation. Attachment support on update requires a pod running SBE v24.1 or later.

#### Scenario: Update with attachment and preview
- **WHEN** a user updates a message with a `Message` containing one attachment and its preview
- **THEN** the Update Message v4 request contains one `attachment` part and one `preview` part with the provided filename and content type

#### Scenario: Update with multiple attachments
- **WHEN** a user updates a message with a `Message` containing two attachments
- **THEN** the request contains two `attachment` parts

#### Scenario: Update through OBO
- **WHEN** a user updates a message through `OboMessageService` with a `Message` containing an attachment
- **THEN** the request contains the `attachment` part, using the delegated session tokens

#### Scenario: Update without attachments
- **WHEN** a user updates a message with a `Message` containing only text
- **THEN** the request contains no `attachment` or `preview` parts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ public List<String> getAttachmentTypes() {
* Update an existing message. The existing message must be a valid social message, that has not been deleted.
*
* @param messageToUpdate the message to be updated
* @param content the update content (attachments are not supported yet)
* @param content the update content (attachments and previews are supported only starting from SBE v24.1)
* @return a {@link V4Message} object containing the details of the sent message
* @see <a href="https://developers.symphony.com/restapi/reference#update-message-v4">Create Update v4</a>
*/
Expand All @@ -424,7 +424,7 @@ public V4Message update(V4Message messageToUpdate, Message content) {
*
* @param streamId the ID of the stream where the message to be updated comes from
* @param messageId the ID of the message to be updated
* @param content the update content (attachments are not supported yet)
* @param content the update content (attachments and previews are supported only starting from SBE v24.1)
* @return a {@link V4Message} object containing the details of the sent message
* @see <a href="https://developers.symphony.com/restapi/reference#update-message-v4">Create Update v4</a>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public interface OboMessageService {
* Update an existing message. The existing message must be a valid social message, that has not been deleted.
*
* @param messageToUpdate the message to be updated
* @param content the update content (attachments are not supported yet)
* @param content the update content (attachments and previews are supported only starting from SBE v24.1)
* @return a {@link V4Message} object containing the details of the sent message
* @see <a href="https://developers.symphony.com/restapi/reference#update-message-v4">Create Update v4</a>
*/
Expand All @@ -189,7 +189,7 @@ public interface OboMessageService {
*
* @param streamId the ID of the stream where the message to be updated comes from
* @param messageId the ID of the message to be updated
* @param content the update content (attachments are not supported yet)
* @param content the update content (attachments and previews are supported only starting from SBE v24.1)
* @return a {@link V4Message} object containing the details of the sent message
* @see <a href="https://developers.symphony.com/restapi/reference#update-message-v4">Create Update v4</a>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class MessageServiceTest {
private static final String TOKEN = "1234";

private MockApiClient mockApiClient;
private ApiClient agentClient;
private MessageService messageService;
private StreamsApi streamsApi;
private AttachmentsApi attachmentsApi;
Expand All @@ -116,7 +117,7 @@ void setUp() {

mockApiClient = new MockApiClient();
ApiClient podClient = mockApiClient.getApiClient("/pod");
ApiClient agentClient = mockApiClient.getApiClient("/agent");
agentClient = spy(mockApiClient.getApiClient("/agent"));

templateEngine = mock(TemplateEngine.class);
streamsApi = spy(new StreamsApi(podClient));
Expand Down Expand Up @@ -482,6 +483,133 @@ void testMessageSilentUpdate() throws IOException {
assertEquals(false, updateMessage.getSilent());
}

private Map<String, Object> captureUpdateFormParams(Message message) throws IOException, ApiException {
return captureUpdateFormParams(message, null);
}

private Map<String, Object> captureUpdateFormParams(Message message, AuthSession oboSession)
throws IOException, ApiException {
mockApiClient.onPost(V4_STREAM_MESSAGE_UPDATE.replace("{sid}", STREAM_ID).replace("{mid}", MESSAGE_ID),
JsonHelper.readFromClasspath("/message/update_message.json"));

final V4Message messageToUpdate = new V4Message().stream(new V4Stream().streamId(STREAM_ID)).messageId(MESSAGE_ID);

final V4Message updateMessage;
if (oboSession != null) {
updateMessage = this.messageService.obo(oboSession).update(messageToUpdate, message);
} else {
updateMessage = this.messageService.update(messageToUpdate, message);
}

assertNotNull(updateMessage);
assertEquals(MESSAGE_ID, updateMessage.getMessageId());

final Map<String, String> expectedHeaders = new HashMap<>();
expectedHeaders.put("sessionToken", oboSession != null ? oboSession.getSessionToken() : TOKEN);
expectedHeaders.put("keyManagerToken", oboSession != null ? oboSession.getKeyManagerToken() : TOKEN);

final ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);

verify(agentClient)
.invokeAPI(
eq(String.format("/v4/stream/%s/message/%s/update", STREAM_ID, MESSAGE_ID)),
eq("POST"),
eq(Collections.emptyList()),
isNull(),
eq(expectedHeaders),
eq(Collections.emptyMap()),
captor.capture(),
eq("application/json"),
eq("multipart/form-data"),
eq(new String[0]),
any());

return captor.getValue();
}

@Test
void testUpdateWithAttachmentAndPreview() throws IOException, ApiException {
final InputStream attachmentStream = IOUtils.toInputStream("Attached file", StandardCharsets.UTF_8);
final InputStream previewStream = IOUtils.toInputStream("Preview file", StandardCharsets.UTF_8);
final Message content = Message.builder()
.content("This is a message update")
.addAttachment(attachmentStream, previewStream, "file.txt", "text/plain")
.build();

final Map<String, Object> form = captureUpdateFormParams(content);

final ApiClientBodyPart[] attachments = (ApiClientBodyPart[]) form.get("attachment");
assertNotNull(attachments);
assertEquals(1, attachments.length);
assertEquals("file.txt", attachments[0].getFilename());
assertEquals("text/plain", attachments[0].getContentType());

final ApiClientBodyPart[] previews = (ApiClientBodyPart[]) form.get("preview");
assertNotNull(previews);
assertEquals(1, previews.length);
assertEquals("preview-file.txt", previews[0].getFilename());
assertEquals("text/plain", previews[0].getContentType());
}

@Test
void testUpdateWithMultipleAttachments() throws IOException, ApiException {
final InputStream firstAttachment = IOUtils.toInputStream("First attached file", StandardCharsets.UTF_8);
final InputStream secondAttachment = IOUtils.toInputStream("Second attached file", StandardCharsets.UTF_8);
final Message content = Message.builder()
.content("This is a message update")
.addAttachment(firstAttachment, "file1.txt")
.addAttachment(secondAttachment, "file2.txt")
.build();

final Map<String, Object> form = captureUpdateFormParams(content);

final ApiClientBodyPart[] attachments = (ApiClientBodyPart[]) form.get("attachment");
assertNotNull(attachments);
assertEquals(2, attachments.length);
assertEquals("file1.txt", attachments[0].getFilename());
assertEquals("file2.txt", attachments[1].getFilename());

final ApiClientBodyPart[] previews = (ApiClientBodyPart[]) form.get("preview");
assertNotNull(previews);
assertEquals(0, previews.length);
}

@Test
void testUpdateWithTextOnlySendsNoAttachmentOrPreviewParts() throws IOException, ApiException {
final Message content = Message.builder().content("This is a message update with text only").build();

final Map<String, Object> form = captureUpdateFormParams(content);

final ApiClientBodyPart[] attachments = (ApiClientBodyPart[]) form.get("attachment");
assertNotNull(attachments);
assertEquals(0, attachments.length);

final ApiClientBodyPart[] previews = (ApiClientBodyPart[]) form.get("preview");
assertNotNull(previews);
assertEquals(0, previews.length);
}

@Test
void testUpdateWithAttachmentObo() throws IOException, ApiException {
final AuthSession oboSession = mock(AuthSession.class);
when(oboSession.getSessionToken()).thenReturn("oboSessionToken");
when(oboSession.getKeyManagerToken()).thenReturn("oboKmToken");

final InputStream attachmentStream = IOUtils.toInputStream("Attached file", StandardCharsets.UTF_8);
final Message content = Message.builder()
.content("This is an OBO message update")
.addAttachment(attachmentStream, "obo-file.txt", "text/plain")
.build();

final Map<String, Object> form = captureUpdateFormParams(content, oboSession);

final ApiClientBodyPart[] attachments = (ApiClientBodyPart[]) form.get("attachment");
assertNotNull(attachments);
assertEquals(1, attachments.length);
assertEquals("obo-file.txt", attachments[0].getFilename());
assertEquals("text/plain", attachments[0].getContentType());
}

@Test
void testGetAttachment() throws ApiException {
final String attachmentId = "attachmentId";
Expand Down
Loading