From 69cb8a6302be55820b40b4baf840dce44ce2329a Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 09:35:51 +0300 Subject: [PATCH 1/9] Implement attachment_mention element. --- .../RichTextAttachmentMentionElementIF.java | 77 +++++++++++++++++++ .../richtextelements/RichTextElement.java | 4 + .../RichTextBlockDeserializationTest.java | 22 ++++++ .../BlockElementSerializationTest.java | 6 ++ .../attachment_mention_rich_text.json | 35 +++++++++ .../src/test/resources/rich_text_blocks.json | 20 +++++ 6 files changed, 164 insertions(+) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextAttachmentMentionElementIF.java create mode 100644 slack-base/src/test/resources/attachment_mention_rich_text.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextAttachmentMentionElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextAttachmentMentionElementIF.java new file mode 100644 index 00000000..21959480 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextAttachmentMentionElementIF.java @@ -0,0 +1,77 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's attachment mention element renders as a rich app attachment or entity reference. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Attachment mention element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextAttachmentMentionElementIF extends RichTextElement { + String TYPE = "attachment_mention"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getUrl(); + + Optional getText(); + + Optional getAppId(); + + Optional getEntityId(); + + Optional getIconUrl(); + + Optional getChannelId(); + + Optional getTs(); + + Optional getFullSizePreviewEnabled(); + + Optional getIconName(); + + Optional getReferenceObjectType(); + + Optional getProductName(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextAttachmentMentionElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java index 0483e7b6..446d9dc6 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java @@ -48,6 +48,10 @@ value = RichTextEmojiElement.class, name = RichTextEmojiElement.TYPE ), + @JsonSubTypes.Type( + value = RichTextAttachmentMentionElement.class, + name = RichTextAttachmentMentionElement.TYPE + ), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java index 47043716..036dada7 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hubspot.slack.client.jackson.ObjectMapperUtils; import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextAttachmentMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBroadcastElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBroadcastRange; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextChannelElement; @@ -36,6 +37,7 @@ public class RichTextBlockDeserializationTest { private static final int SECTION_WITH_USERGROUP_ELEMENT_INDEX = 8; private static final int SECTION_WITH_MULTIPLE_ELEMENTS_INDEX = 9; private static final int SECTION_WITH_UNKNOWN_ELEMENT_INDEX = 10; + private static final int SECTION_WITH_ATTACHMENT_MENTION_ELEMENT_INDEX = 11; private static RichTextSection[] sections; @BeforeClass @@ -134,6 +136,26 @@ public void itDeserializesUsergroupElement() { ); } + @Test + public void itDeserializesAttachmentMentionElement() { + assertThat(sections[SECTION_WITH_ATTACHMENT_MENTION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextAttachmentMentionElement + .builder() + .setUrl("https://example.com/attachment") + .setText("Fallback text") + .setAppId("A123ABC456") + .setStyle( + RichTextAttachmentMentionElement.Style + .builder() + .setBold(true) + .setClientHighlight(true) + .build() + ) + .build() + ); + } + @Test public void itDeserializesUnknownElementAsUnknownBlockElement() { assertThat(sections[SECTION_WITH_UNKNOWN_ELEMENT_INDEX].getElements()) diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java index 2086bb55..41a22a85 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java @@ -5,6 +5,7 @@ import com.hubspot.slack.client.SerializationTestBase; import com.hubspot.slack.client.jackson.ObjectMapperUtils; import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.Block; import com.hubspot.slack.client.models.blocks.Section; import java.io.IOException; import org.junit.Test; @@ -16,6 +17,11 @@ public void testBlockSerialization() throws IOException { testSerialization("block_elements.json", BlockElement[].class); } + @Test + public void testAttachmentMentionRichTextSerialization() throws IOException { + testSerialization("attachment_mention_rich_text.json", Block[].class); + } + @Test public void testUnknownBlockSerialization() throws IOException { String rawJson = JsonLoader.loadJsonFromFile("unknown_block_element.json"); diff --git a/slack-base/src/test/resources/attachment_mention_rich_text.json b/slack-base/src/test/resources/attachment_mention_rich_text.json new file mode 100644 index 00000000..92a95b6a --- /dev/null +++ b/slack-base/src/test/resources/attachment_mention_rich_text.json @@ -0,0 +1,35 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "attachment_mention", + "url": "https://example.com/attachment", + "text": "Fallback text", + "app_id": "A123ABC456", + "entity_id": "E123ABC456", + "icon_url": "https://example.com/icon.png", + "channel_id": "C123ABC456", + "ts": "1234567890.123456", + "full_size_preview_enabled": true, + "icon_name": "sf-account", + "reference_object_type": "record", + "product_name": "Google Docs", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + } +] diff --git a/slack-base/src/test/resources/rich_text_blocks.json b/slack-base/src/test/resources/rich_text_blocks.json index da26cfe0..65e989db 100644 --- a/slack-base/src/test/resources/rich_text_blocks.json +++ b/slack-base/src/test/resources/rich_text_blocks.json @@ -193,5 +193,25 @@ ] } ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "attachment_mention", + "url": "https://example.com/attachment", + "text": "Fallback text", + "app_id": "A123ABC456", + "style": { + "bold": true, + "client_highlight": true + } + } + ] + } + ] } ] From 20992be5f3c337c7c1c764ef021d9d6de74ec31d Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 10:41:02 +0300 Subject: [PATCH 2/9] Implement team and message mention elements. --- .../richtextelements/RichTextElement.java | 8 +++ .../RichTextMessageMentionElementIF.java | 66 +++++++++++++++++++ .../RichTextTeamElementIF.java | 65 ++++++++++++++++++ .../RichTextBlockDeserializationTest.java | 30 +++++++++ .../BlockElementSerializationTest.java | 10 +++ .../resources/message_mention_rich_text.json | 30 +++++++++ .../src/test/resources/rich_text_blocks.json | 38 ++++++++++- .../src/test/resources/team_rich_text.json | 25 +++++++ 8 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextMessageMentionElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTeamElementIF.java create mode 100644 slack-base/src/test/resources/message_mention_rich_text.json create mode 100644 slack-base/src/test/resources/team_rich_text.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java index 446d9dc6..2e47602a 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java @@ -52,6 +52,14 @@ value = RichTextAttachmentMentionElement.class, name = RichTextAttachmentMentionElement.TYPE ), + @JsonSubTypes.Type( + value = RichTextTeamElement.class, + name = RichTextTeamElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextMessageMentionElement.class, + name = RichTextMessageMentionElement.TYPE + ), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextMessageMentionElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextMessageMentionElementIF.java new file mode 100644 index 00000000..c9205921 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextMessageMentionElementIF.java @@ -0,0 +1,66 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's message mention element renders as a link to a Slack message. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Message mention element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextMessageMentionElementIF extends RichTextElement { + String TYPE = "message_mention"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + String getChannelId(); + + String getMessageTs(); + + Optional getAuthorId(); + + Optional getThreadTs(); + + Optional getText(); + + Optional getUrl(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextMessageMentionElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTeamElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTeamElementIF.java new file mode 100644 index 00000000..0d587666 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTeamElementIF.java @@ -0,0 +1,65 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's team element renders as a mention of a workspace or team. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * + *

WARNING — documented but not accepted by Slack at runtime. As of 2026-10-02, pasting + * the official docs example into the Block Kit Builder and previewing it in Slack fails: the Slack + * API rejects it with "unknown rich text element type team". The {@code team} element is + * therefore modeled here for completeness/round-trip support, but should NOT be sent to Slack until + * Slack actually supports it — doing so will cause the message to be rejected. Verify in the Block + * Kit Builder before relying on it. + * + * @see Team element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextTeamElementIF extends RichTextElement { + String TYPE = "team"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getTeamId(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextTeamElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java index 036dada7..b47a2788 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java @@ -14,7 +14,9 @@ import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextDateElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextEmojiElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextLinkElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextMessageMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSection; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextTeamElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextTextElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextUserElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextUserGroupElement; @@ -38,6 +40,8 @@ public class RichTextBlockDeserializationTest { private static final int SECTION_WITH_MULTIPLE_ELEMENTS_INDEX = 9; private static final int SECTION_WITH_UNKNOWN_ELEMENT_INDEX = 10; private static final int SECTION_WITH_ATTACHMENT_MENTION_ELEMENT_INDEX = 11; + private static final int SECTION_WITH_TEAM_ELEMENT_INDEX = 12; + private static final int SECTION_WITH_MESSAGE_MENTION_ELEMENT_INDEX = 13; private static RichTextSection[] sections; @BeforeClass @@ -156,6 +160,32 @@ public void itDeserializesAttachmentMentionElement() { ); } + @Test + public void itDeserializesTeamElement() { + assertThat(sections[SECTION_WITH_TEAM_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextTeamElement + .builder() + .setTeamId("T123ABC456") + .setStyle(RichTextTeamElement.Style.builder().setBold(true).build()) + .build() + ); + } + + @Test + public void itDeserializesMessageMentionElement() { + assertThat(sections[SECTION_WITH_MESSAGE_MENTION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextMessageMentionElement + .builder() + .setChannelId("C123ABC456") + .setMessageTs("1234567890.123456") + .setAuthorId("U123ABC456") + .setText("a message") + .build() + ); + } + @Test public void itDeserializesUnknownElementAsUnknownBlockElement() { assertThat(sections[SECTION_WITH_UNKNOWN_ELEMENT_INDEX].getElements()) diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java index 41a22a85..cba01ef5 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java @@ -22,6 +22,16 @@ public void testAttachmentMentionRichTextSerialization() throws IOException { testSerialization("attachment_mention_rich_text.json", Block[].class); } + @Test + public void testTeamRichTextSerialization() throws IOException { + testSerialization("team_rich_text.json", Block[].class); + } + + @Test + public void testMessageMentionRichTextSerialization() throws IOException { + testSerialization("message_mention_rich_text.json", Block[].class); + } + @Test public void testUnknownBlockSerialization() throws IOException { String rawJson = JsonLoader.loadJsonFromFile("unknown_block_element.json"); diff --git a/slack-base/src/test/resources/message_mention_rich_text.json b/slack-base/src/test/resources/message_mention_rich_text.json new file mode 100644 index 00000000..695b2cf7 --- /dev/null +++ b/slack-base/src/test/resources/message_mention_rich_text.json @@ -0,0 +1,30 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "message_mention", + "channel_id": "C123ABC456", + "message_ts": "1234567890.123456", + "author_id": "U123ABC456", + "thread_ts": "1234567890.000001", + "text": "a message", + "url": "https://example.com/message", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + } +] diff --git a/slack-base/src/test/resources/rich_text_blocks.json b/slack-base/src/test/resources/rich_text_blocks.json index 65e989db..886a702d 100644 --- a/slack-base/src/test/resources/rich_text_blocks.json +++ b/slack-base/src/test/resources/rich_text_blocks.json @@ -187,8 +187,8 @@ "type": "rich_text_section", "elements": [ { - "type": "message_mention", - "message_id": "1234567890.123456" + "type": "unsupported_rich_text_element", + "some_field": "some_value" } ] } @@ -213,5 +213,39 @@ ] } ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "team", + "team_id": "T123ABC456", + "style": { + "bold": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "message_mention", + "channel_id": "C123ABC456", + "message_ts": "1234567890.123456", + "author_id": "U123ABC456", + "text": "a message" + } + ] + } + ] } ] diff --git a/slack-base/src/test/resources/team_rich_text.json b/slack-base/src/test/resources/team_rich_text.json new file mode 100644 index 00000000..2800696d --- /dev/null +++ b/slack-base/src/test/resources/team_rich_text.json @@ -0,0 +1,25 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "team", + "team_id": "T123ABC456", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + } +] From 749aae65252eba52a0169c5f5e1e7b233b6abc0e Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 14:45:25 +0300 Subject: [PATCH 3/9] Implement payload tester to test the payload with actual post to channel message. --- slack-java-client/pom.xml | 6 + .../BlockKitPayloadFromFilesTest.java | 81 +++ .../blockkit/BlockKitPayloadTester.java | 471 ++++++++++++++++++ .../BlockKitPayloadTesterLiveTest.java | 99 ++++ .../resources/blockkit-payloads/README.md | 36 ++ .../blockkit-payloads/example_section.json | 9 + .../test/resources/simplelogger.properties | 9 + 7 files changed, 711 insertions(+) create mode 100644 slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java create mode 100644 slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java create mode 100644 slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTesterLiveTest.java create mode 100644 slack-java-client/src/test/resources/blockkit-payloads/README.md create mode 100644 slack-java-client/src/test/resources/blockkit-payloads/example_section.json create mode 100644 slack-java-client/src/test/resources/simplelogger.properties diff --git a/slack-java-client/pom.xml b/slack-java-client/pom.xml index 88ad56f3..1d8ebb3b 100644 --- a/slack-java-client/pom.xml +++ b/slack-java-client/pom.xml @@ -100,5 +100,11 @@ assertj-core test + + + org.slf4j + slf4j-simple + test + diff --git a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java new file mode 100644 index 00000000..0ff25dbc --- /dev/null +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java @@ -0,0 +1,81 @@ +package com.hubspot.slack.client.blockkit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +import com.google.common.base.Strings; +import com.hubspot.slack.client.blockkit.BlockKitPayloadTester.PayloadSendSummary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Live, manual harness that sends Block Kit payload file(s) to a real channel and prints a summary. + * Skipped (JUnit {@code Assume}) unless a token, channel, and payload path are supplied, so it never + * runs in CI or a plain {@code mvn test}. + * + *

Point it at a single JSON file or a folder of {@code *.json} payloads: + *

{@code
+ * mvn -pl slack-java-client test -Dtest=BlockKitPayloadFromFilesTest \
+ *   -Dslack.test.token=xoxb-... -Dslack.test.channel=C0123456789 \
+ *   -Dslack.test.payload=/abs/path/to/payloads_dir \
+ *   -Dsurefire.failIfNoSpecifiedTests=false
+ * }
+ * (or set {@code SLACK_TEST_TOKEN} / {@code SLACK_TEST_CHANNEL} / {@code SLACK_TEST_PAYLOAD}). + * + *

Each payload file must be a JSON array of blocks (the same shape as the + * {@code *_rich_text.json} test fixtures). The summary reports how many were sent and, for each + * failure, the reason (e.g. {@code unknown rich text element type team}). + */ +public class BlockKitPayloadFromFilesTest { + + private static final Logger LOG = LoggerFactory.getLogger( + BlockKitPayloadFromFilesTest.class + ); + + private String token; + private String channelId; + private String payloadPath; + + @Before + public void setUp() { + token = config("slack.test.token", "SLACK_TEST_TOKEN"); + channelId = config("slack.test.channel", "SLACK_TEST_CHANNEL"); + payloadPath = config("slack.test.payload", "SLACK_TEST_PAYLOAD"); + assumeTrue( + "Set -Dslack.test.token, -Dslack.test.channel and -Dslack.test.payload (file or folder) " + + "to run the file-driven Block Kit payload test", + !Strings.isNullOrEmpty(token) && + !Strings.isNullOrEmpty(channelId) && + !Strings.isNullOrEmpty(payloadPath) + ); + } + + @Test + public void itSendsPayloadFileOrFolderAndPrintsSummary() throws Exception { + Path path = Paths.get(payloadPath); + assertThat(Files.exists(path)).as("payload path does not exist: %s", path).isTrue(); + + try (BlockKitPayloadTester tester = new BlockKitPayloadTester(token, channelId)) { + PayloadSendSummary summary = tester.postFromPath(path); + + LOG.info("Block Kit payload send results:\n{}", summary.render()); + + assertThat(summary.getTotal()) + .as("expected at least one *.json payload under %s", path) + .isGreaterThan(0); + } + } + + private static String config(String sysProp, String envVar) { + String fromProp = System.getProperty(sysProp); + if (!Strings.isNullOrEmpty(fromProp)) { + return fromProp; + } + return System.getenv(envVar); + } +} diff --git a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java new file mode 100644 index 00000000..8556ff54 --- /dev/null +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java @@ -0,0 +1,471 @@ +package com.hubspot.slack.client.blockkit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Stopwatch; +import com.google.common.io.Resources; +import com.hubspot.algebra.Result; +import com.hubspot.horizon.HttpRequest; +import com.hubspot.horizon.HttpResponse; +import com.hubspot.slack.client.SlackClient; +import com.hubspot.slack.client.SlackClientFactory; +import com.hubspot.slack.client.SlackClientRuntimeConfig; +import com.hubspot.slack.client.interceptors.http.ResponseDebugger; +import com.hubspot.slack.client.jackson.ObjectMapperUtils; +import com.hubspot.slack.client.methods.SlackMethod; +import com.hubspot.slack.client.methods.params.chat.ChatPostMessageParams; +import com.hubspot.slack.client.models.blocks.Block; +import com.hubspot.slack.client.models.response.SlackError; +import com.hubspot.slack.client.models.response.chat.ChatPostMessageResponse; +import java.io.Closeable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Test-only harness for runtime-validating Block Kit payloads against the real Slack API. + * + *

Our (de)serialization round-trip tests only prove that {@code slack-client} can marshal a + * block/element to and from JSON — they do not prove that Slack actually accepts and renders + * it (see SISLACK-738 implementation rules, Rule 8). Some elements are documented but rejected at + * runtime, e.g. Slack returns {@code "unknown rich text element type team"} for the {@code team} + * element. This service closes that gap: give it a token + channel, hand it a blocks JSON payload + * (or pre-built {@link Block} objects), and it posts a real message and returns the raw Slack + * {@link Result} so a test can assert acceptance or inspect the rejection error. + * + *

Typical use (the Java object is built from the JSON, as required): + *

{@code
+ * try (BlockKitPayloadTester tester = new BlockKitPayloadTester(token, channelId)) {
+ *   List blocks = BlockKitPayloadTester.blocksFromJson(payloadJson);
+ *   Result result = tester.postBlocks(blocks);
+ *   assertThat(result.isOk()).isTrue();
+ * }
+ * }
+ */ +public class BlockKitPayloadTester implements Closeable { + + private static final String DEFAULT_FALLBACK_TEXT = "Block Kit payload test"; + + private final SlackClient slackClient; + private final String channelId; + private final boolean ownsClient; + private final CapturingResponseDebugger errorCapture; + + /** + * Builds a {@link SlackClient} from the given token and targets the given channel. + * + * @param token a Slack token with {@code chat:write} (bot {@code xoxb-} or user {@code xoxp-}) + * @param channelId the channel/conversation ID to post into (the token must be able to post there) + */ + public BlockKitPayloadTester(String token, String channelId) { + this.errorCapture = new CapturingResponseDebugger(); + this.slackClient = + SlackClientFactory + .defaultFactory() + .create( + SlackClientRuntimeConfig + .builder() + .setTokenSupplier(() -> token) + .setResponseDebugger(errorCapture) + .build() + ); + this.channelId = channelId; + this.ownsClient = true; + } + + /** + * Uses a caller-supplied client (e.g. with custom config); caller retains ownership/closing. + * Note: detailed failure reasons from {@code response_metadata.messages} are only captured when + * this class builds the client itself (the other constructor) — a caller-supplied client would + * need its own {@code ResponseDebugger} for that detail. + */ + public BlockKitPayloadTester(SlackClient slackClient, String channelId) { + this.slackClient = slackClient; + this.channelId = channelId; + this.ownsClient = false; + this.errorCapture = null; + } + + /** + * Deserializes a JSON array of blocks into {@code slack-client} {@link Block} objects, + * using the same {@link ObjectMapperUtils#mapper()} the library itself uses. + */ + public static List blocksFromJson(String blocksJson) { + try { + return Arrays.asList( + ObjectMapperUtils.mapper().readValue(blocksJson, Block[].class) + ); + } catch (IOException e) { + throw new RuntimeException("Could not parse blocks JSON into Block[]", e); + } + } + + /** Loads a classpath resource (a JSON array of blocks) and deserializes it. */ + public static List blocksFromResource(String resourceName) { + try { + String json = Resources.toString( + Resources.getResource(resourceName), + StandardCharsets.UTF_8 + ); + return blocksFromJson(json); + } catch (IOException e) { + throw new RuntimeException("Could not read blocks resource " + resourceName, e); + } + } + + /** Posts pre-built blocks with the default fallback text. */ + public Result postBlocks(List blocks) { + return postBlocks(blocks, DEFAULT_FALLBACK_TEXT); + } + + /** + * Posts pre-built blocks and returns the raw Slack result. + * + * @return {@code ok} if Slack accepted the payload, {@code err} (with the Slack error string) + * if it was rejected — e.g. {@code invalid_blocks} / {@code unknown rich text element type ...} + */ + public Result postBlocks( + List blocks, + String fallbackText + ) { + if (errorCapture != null) { + errorCapture.reset(); + } + return slackClient + .postMessage( + ChatPostMessageParams + .builder() + .setChannelId(channelId) + .setText(fallbackText) + .setBlocks(blocks) + .build() + ) + .join(); + } + + /** + * The detailed Slack error messages (from {@code response_metadata.messages}) for the most recent + * failed post, when available. Slack's top-level error is often just {@code invalid_blocks}; the + * actionable reason (e.g. {@code unknown rich text element type team}) lives here. + */ + public Optional getLastErrorDetail() { + return errorCapture == null ? Optional.empty() : errorCapture.getDetail(); + } + + /** Convenience: build blocks from a JSON array string and post them. */ + public Result postBlocksJson(String blocksJson) { + return postBlocks(blocksFromJson(blocksJson)); + } + + /** + * Reads a single JSON payload file (a JSON array of blocks), deserializes it, and posts it. + * Never throws for an individual payload — any read / parse / Slack-rejection problem is captured + * as a {@link PayloadSendResult} carrying the failure reason, so it reads the same whether used + * standalone or from {@link #postFromFolder(Path)}. + */ + public PayloadSendResult postFromFile(Path jsonFile) { + String fileName = jsonFile.getFileName().toString(); + + String json; + try { + json = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + } catch (IOException e) { + return PayloadSendResult.failure(fileName, "Could not read file: " + describe(e)); + } + + List blocks; + try { + blocks = blocksFromJson(json); + } catch (RuntimeException e) { + return PayloadSendResult.failure( + fileName, + "Could not parse/deserialize payload: " + describe(e) + ); + } + + try { + Result result = postBlocks( + blocks, + "Block Kit payload test: " + fileName + ); + if (result.isOk()) { + return PayloadSendResult.success(fileName, result.unwrapOrElseThrow().getTs()); + } + return PayloadSendResult.failure( + fileName, + reasonFor(result.unwrapErrOrElseThrow()) + ); + } catch (RuntimeException e) { + return PayloadSendResult.failure(fileName, "Post failed: " + describe(e)); + } + } + + /** + * Builds a human-readable failure reason: Slack's top-level error, enriched with the detailed + * {@code response_metadata.messages} when the client captured them (see {@link #getLastErrorDetail()}). + */ + private String reasonFor(SlackError error) { + String base = error.getError(); + return getLastErrorDetail().map(detail -> base + " — " + detail).orElse(base); + } + + /** + * Posts every {@code *.json} payload in the given folder (non-recursive, sorted by file name), + * reusing {@link #postFromFile(Path)} for each, and returns an aggregate {@link PayloadSendSummary}. + */ + public PayloadSendSummary postFromFolder(Path folder) { + List files; + try (Stream stream = Files.list(folder)) { + files = + stream + .filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().toLowerCase().endsWith(".json")) + .sorted() + .collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException("Could not list folder " + folder, e); + } + + List results = new ArrayList<>(); + for (Path file : files) { + results.add(postFromFile(file)); + } + return new PayloadSendSummary(results); + } + + /** + * Convenience: if {@code path} is a directory, behaves like {@link #postFromFolder(Path)}; + * otherwise posts the single file and wraps the one result in a summary. + */ + public PayloadSendSummary postFromPath(Path path) { + if (Files.isDirectory(path)) { + return postFromFolder(path); + } + return new PayloadSendSummary(Collections.singletonList(postFromFile(path))); + } + + private static String describe(Throwable t) { + Throwable root = t; + while (root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + String message = root.getMessage(); + return message == null || message.isEmpty() + ? root.getClass().getSimpleName() + : message; + } + + @Override + public void close() throws IOException { + if (ownsClient) { + slackClient.close(); + } + } + + /** + * Captures the detailed error messages Slack returns under {@code response_metadata.messages} on a + * failed API call, which the {@code slack-client} {@link SlackError} model otherwise discards + * (it keeps only the top-level {@code error}, e.g. {@code invalid_blocks}). + */ + private static final class CapturingResponseDebugger implements ResponseDebugger { + + private final AtomicReference lastDetail = new AtomicReference<>(); + + void reset() { + lastDetail.set(null); + } + + Optional getDetail() { + return Optional.ofNullable(lastDetail.get()); + } + + @Override + public void debugSlackApiError( + long requestId, + SlackMethod method, + HttpRequest request, + HttpResponse failure + ) { + String detail = extractMessages(failure); + if (detail != null) { + lastDetail.set(detail); + } + } + + @Override + public void debugProcessingFailure( + long requestId, + SlackMethod method, + HttpRequest request, + HttpResponse response, + Throwable ex + ) { + String detail = extractMessages(response); + if (detail != null) { + lastDetail.set(detail); + } + } + + @Override + public void debug( + long requestId, + SlackMethod method, + Stopwatch timer, + HttpRequest request, + HttpResponse response + ) {} + + @Override + public void debugTransportException( + long requestId, + SlackMethod method, + HttpRequest request, + Throwable exception + ) {} + + @Override + public void debugProactiveRateLimit( + long requestId, + SlackMethod method, + HttpRequest request + ) {} + + private static String extractMessages(HttpResponse response) { + try { + JsonNode root = response.getAsJsonNode(); + JsonNode messages = root.path("response_metadata").path("messages"); + if (messages.isArray() && messages.size() > 0) { + List parts = new ArrayList<>(); + messages.forEach(node -> parts.add(node.asText())); + return String.join("; ", parts); + } + JsonNode warning = root.path("warning"); + if (warning.isTextual() && !warning.asText().isEmpty()) { + return warning.asText(); + } + return null; + } catch (RuntimeException e) { + return null; + } + } + } + + /** Outcome of sending one payload file: either success (with the message ts) or failure (reason). */ + public static final class PayloadSendResult { + + private final String fileName; + private final boolean success; + private final String detail; + + private PayloadSendResult(String fileName, boolean success, String detail) { + this.fileName = fileName; + this.success = success; + this.detail = detail; + } + + static PayloadSendResult success(String fileName, String messageTs) { + return new PayloadSendResult(fileName, true, messageTs); + } + + static PayloadSendResult failure(String fileName, String reason) { + return new PayloadSendResult(fileName, false, reason); + } + + public String getFileName() { + return fileName; + } + + public boolean isSuccess() { + return success; + } + + /** The posted message's {@code ts}, present only when {@link #isSuccess()}. */ + public Optional getMessageTs() { + return success ? Optional.of(detail) : Optional.empty(); + } + + /** The failure reason, present only when the send failed. */ + public Optional getError() { + return success ? Optional.empty() : Optional.of(detail); + } + + @Override + public String toString() { + return success + ? "OK " + fileName + " (ts=" + detail + ")" + : "FAIL " + fileName + " — " + detail; + } + } + + /** Aggregate of a batch of {@link PayloadSendResult}s with a human-readable {@link #render()}. */ + public static final class PayloadSendSummary { + + private final List results; + + PayloadSendSummary(List results) { + this.results = Collections.unmodifiableList(new ArrayList<>(results)); + } + + public List getResults() { + return results; + } + + public int getTotal() { + return results.size(); + } + + public int getSentCount() { + return Math.toIntExact( + results.stream().filter(PayloadSendResult::isSuccess).count() + ); + } + + public int getFailedCount() { + return getTotal() - getSentCount(); + } + + public List getFailures() { + return results.stream().filter(r -> !r.isSuccess()).collect(Collectors.toList()); + } + + /** Multi-line summary: a header count line, then one line per payload (sent first, then failures). */ + public String render() { + StringBuilder sb = new StringBuilder(); + sb + .append("Block Kit payload send summary: ") + .append(getTotal()) + .append(" total, ") + .append(getSentCount()) + .append(" sent, ") + .append(getFailedCount()) + .append(" failed"); + results + .stream() + .filter(PayloadSendResult::isSuccess) + .forEach(r -> + sb + .append("\n ✅ ") + .append(r.getFileName()) + .append(" (ts=") + .append(r.getMessageTs().orElse("")) + .append(")") + ); + for (PayloadSendResult failure : getFailures()) { + sb + .append("\n ❌ ") + .append(failure.getFileName()) + .append(" — ") + .append(failure.getError().orElse("unknown error")); + } + return sb.toString(); + } + } +} diff --git a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTesterLiveTest.java b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTesterLiveTest.java new file mode 100644 index 00000000..d4ca5eb6 --- /dev/null +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTesterLiveTest.java @@ -0,0 +1,99 @@ +package com.hubspot.slack.client.blockkit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +import com.google.common.base.Strings; +import com.hubspot.algebra.Result; +import com.hubspot.slack.client.models.response.SlackError; +import com.hubspot.slack.client.models.response.chat.ChatPostMessageResponse; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Live, manual runtime-validation of Block Kit payloads against the real Slack API via + * {@link BlockKitPayloadTester}. Skipped automatically (JUnit {@code Assume}) unless a token and + * channel are supplied, so it never runs in CI or a plain {@code mvn test}. + * + *

Run it yourself with: + *

{@code
+ * mvn -pl slack-java-client test -Dtest=BlockKitPayloadTesterLiveTest \
+ *   -Dslack.test.token=xoxb-... -Dslack.test.channel=C0123456789
+ * }
+ * (or set {@code SLACK_TEST_TOKEN} / {@code SLACK_TEST_CHANNEL} environment variables). + */ +public class BlockKitPayloadTesterLiveTest { + + private static final Logger LOG = LoggerFactory.getLogger( + BlockKitPayloadTesterLiveTest.class + ); + + private String token; + private String channelId; + + @Before + public void setUp() { + token = config("slack.test.token", "SLACK_TEST_TOKEN"); + channelId = config("slack.test.channel", "SLACK_TEST_CHANNEL"); + assumeTrue( + "Set -Dslack.test.token and -Dslack.test.channel (or SLACK_TEST_TOKEN / " + + "SLACK_TEST_CHANNEL) to run the live Block Kit payload tests", + !Strings.isNullOrEmpty(token) && !Strings.isNullOrEmpty(channelId) + ); + } + + /** Sanity check: a trivially-valid payload is accepted — proves the harness itself works. */ + @Test + public void itPostsAKnownGoodSectionPayload() throws Exception { + String payloadJson = + "[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\"," + + "\"text\":\"BlockKitPayloadTester :white_check_mark: known-good payload\"}}]"; + + try (BlockKitPayloadTester tester = new BlockKitPayloadTester(token, channelId)) { + Result result = tester.postBlocksJson( + payloadJson + ); + LOG.info("known-good section result: {}", result); + assertThat(result.isOk()) + .as("Slack should accept a simple section payload") + .isTrue(); + } + } + + /** + * Documents the observed runtime rejection of the documented-but-unsupported {@code team} + * element (Slack returns {@code "unknown rich text element type team"}). If this ever starts + * passing, Slack has shipped support and the WARNING on {@code RichTextTeamElementIF} / + * {@code block-kit-elements.md} should be removed. + */ + @Test + public void itReportsSlackRejectionForTeamElement() throws Exception { + String payloadJson = + "[{\"type\":\"rich_text\",\"elements\":[{\"type\":\"rich_text_section\"," + + "\"elements\":[{\"type\":\"team\",\"team_id\":\"T123ABC456\"}]}]}]"; + + try (BlockKitPayloadTester tester = new BlockKitPayloadTester(token, channelId)) { + Result result = tester.postBlocksJson( + payloadJson + ); + LOG.info("team-element result: {}", result); + assertThat(result.isErr()) + .as("Slack is expected to reject the documented-but-unsupported `team` element") + .isTrue(); + LOG.warn( + "Slack rejected `team` element as expected: {}", + result.unwrapErrOrElseThrow().getError() + ); + } + } + + private static String config(String sysProp, String envVar) { + String fromProp = System.getProperty(sysProp); + if (!Strings.isNullOrEmpty(fromProp)) { + return fromProp; + } + return System.getenv(envVar); + } +} diff --git a/slack-java-client/src/test/resources/blockkit-payloads/README.md b/slack-java-client/src/test/resources/blockkit-payloads/README.md new file mode 100644 index 00000000..462f5cd5 --- /dev/null +++ b/slack-java-client/src/test/resources/blockkit-payloads/README.md @@ -0,0 +1,36 @@ +# blockkit-payloads + +An example Block Kit payload (`example_section.json`) for the `BlockKitPayloadFromFilesTest` / +`BlockKitPayloadTester` harness. + +Each payload file is a **JSON array of blocks** — exactly the `blocks` array you'd pass to +`chat.postMessage`: + +```json +[ { "type": "section", "text": { "type": "mrkdwn", "text": "hello" } } ] +``` + +## How to use + +Run the harness and point it at a payload file or a folder of `*.json` payloads. Supply your own +bot token (needs `chat:write`) and the target channel id. Run from the repo root: + +```bash +mvn -pl slack-java-client -am test \ + -Dtest=BlockKitPayloadFromFilesTest \ + -Dslack.test.token=xoxb-your-bot-token \ + -Dslack.test.channel=C0123456789 \ + -Dslack.test.payload="$(pwd)/slack-java-client/src/test/resources/blockkit-payloads/example_section.json" \ + -Dsurefire.failIfNoSpecifiedTests=false +``` + +- Use an **absolute** path for `-Dslack.test.payload` (the test runs with the module as its working + directory, so a repo-relative path won't resolve — the `$(pwd)/...` above makes it absolute). +- `-Dslack.test.payload` may be a single file or a directory (all `*.json` in it are posted, sorted by name). +- Token/channel/payload can also be given as the `SLACK_TEST_TOKEN` / `SLACK_TEST_CHANNEL` / + `SLACK_TEST_PAYLOAD` environment variables. +- `-am` is required so the harness compiles against the local `slack-base` (which has the in-progress + elements); otherwise new element types deserialize to `unknown` and Slack rejects the payload. + +The harness posts each payload and prints a summary of how many were sent, with the Slack error +detail for any failures. diff --git a/slack-java-client/src/test/resources/blockkit-payloads/example_section.json b/slack-java-client/src/test/resources/blockkit-payloads/example_section.json new file mode 100644 index 00000000..989da75d --- /dev/null +++ b/slack-java-client/src/test/resources/blockkit-payloads/example_section.json @@ -0,0 +1,9 @@ +[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Example payload* — a single Block Kit message. This file is a JSON array of blocks (the `blocks` you'd pass to chat.postMessage)." + } + } +] diff --git a/slack-java-client/src/test/resources/simplelogger.properties b/slack-java-client/src/test/resources/simplelogger.properties new file mode 100644 index 00000000..9f8b278f --- /dev/null +++ b/slack-java-client/src/test/resources/simplelogger.properties @@ -0,0 +1,9 @@ +# slf4j-simple config for test scope. +# Keep the general test output quiet, but show the Block Kit payload harness at INFO on stdout. +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.log.com.hubspot.slack.client.blockkit=info +org.slf4j.simpleLogger.logFile=System.out +org.slf4j.simpleLogger.showDateTime=false +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=false +org.slf4j.simpleLogger.levelInBrackets=true From 852b34c4233716a84c574dabc4c76fa815c0505a Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 15:06:43 +0300 Subject: [PATCH 4/9] Implement FileElement --- .../richtextelements/RichTextElement.java | 4 ++ .../RichTextFileElementIF.java | 66 +++++++++++++++++++ .../RichTextBlockDeserializationTest.java | 15 +++++ .../BlockElementSerializationTest.java | 5 ++ .../src/test/resources/file_rich_text.json | 27 ++++++++ .../src/test/resources/rich_text_blocks.json | 16 +++++ 6 files changed, 133 insertions(+) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java create mode 100644 slack-base/src/test/resources/file_rich_text.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java index 2e47602a..df2f9051 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java @@ -60,6 +60,10 @@ value = RichTextMessageMentionElement.class, name = RichTextMessageMentionElement.TYPE ), + @JsonSubTypes.Type( + value = RichTextFileElement.class, + name = RichTextFileElement.TYPE + ), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java new file mode 100644 index 00000000..b8651174 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java @@ -0,0 +1,66 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's file element renders as a link to a Slack file. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + *

Note: this is distinct from the {@code file} block (the {@code File} block class); both + * use {@code type: "file"} but the block references a remote file via {@code external_id}/{@code source}, + * whereas this element links a Slack file via {@code file_id} inside rich text. + * @see File element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextFileElementIF extends RichTextElement { + String TYPE = "file"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getFileId(); + + Optional getText(); + + Optional getUrl(); + + @JsonProperty("is_skill_invocation") + Optional getIsSkillInvocation(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextFileElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + } +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java index b47a2788..30f36e86 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java @@ -13,6 +13,7 @@ import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextColorElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextDateElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextEmojiElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextFileElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextLinkElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextMessageMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSection; @@ -42,6 +43,7 @@ public class RichTextBlockDeserializationTest { private static final int SECTION_WITH_ATTACHMENT_MENTION_ELEMENT_INDEX = 11; private static final int SECTION_WITH_TEAM_ELEMENT_INDEX = 12; private static final int SECTION_WITH_MESSAGE_MENTION_ELEMENT_INDEX = 13; + private static final int SECTION_WITH_FILE_ELEMENT_INDEX = 14; private static RichTextSection[] sections; @BeforeClass @@ -186,6 +188,19 @@ public void itDeserializesMessageMentionElement() { ); } + @Test + public void itDeserializesFileElement() { + assertThat(sections[SECTION_WITH_FILE_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextFileElement + .builder() + .setFileId("F123ABC456") + .setText("the file") + .setStyle(RichTextFileElement.Style.builder().setBold(true).build()) + .build() + ); + } + @Test public void itDeserializesUnknownElementAsUnknownBlockElement() { assertThat(sections[SECTION_WITH_UNKNOWN_ELEMENT_INDEX].getElements()) diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java index cba01ef5..03461023 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java @@ -32,6 +32,11 @@ public void testMessageMentionRichTextSerialization() throws IOException { testSerialization("message_mention_rich_text.json", Block[].class); } + @Test + public void testFileRichTextSerialization() throws IOException { + testSerialization("file_rich_text.json", Block[].class); + } + @Test public void testUnknownBlockSerialization() throws IOException { String rawJson = JsonLoader.loadJsonFromFile("unknown_block_element.json"); diff --git a/slack-base/src/test/resources/file_rich_text.json b/slack-base/src/test/resources/file_rich_text.json new file mode 100644 index 00000000..a89254d3 --- /dev/null +++ b/slack-base/src/test/resources/file_rich_text.json @@ -0,0 +1,27 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "file", + "file_id": "F123ABC456", + "text": "the file", + "url": "https://example.com/file", + "is_skill_invocation": true, + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true + } + } + ] + } + ] + } +] diff --git a/slack-base/src/test/resources/rich_text_blocks.json b/slack-base/src/test/resources/rich_text_blocks.json index 886a702d..57a75b7a 100644 --- a/slack-base/src/test/resources/rich_text_blocks.json +++ b/slack-base/src/test/resources/rich_text_blocks.json @@ -247,5 +247,21 @@ ] } ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "file", + "file_id": "F123ABC456", + "text": "the file", + "style": { "bold": true } + } + ] + } + ] } ] From fd5af5ae48e6776a0057abba66ebbd60124dc466 Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 18:29:07 +0300 Subject: [PATCH 5/9] Implement all missing elements. --- .../models/blocks/elements/BlockElement.java | 3 + .../models/blocks/elements/FileInputIF.java | 35 +++ .../blocks/elements/RichTextInputIF.java | 56 +++++ .../blocks/elements/WorkflowButtonIF.java | 41 ++++ .../richtextelements/CitationDetails.java | 45 ++++ .../ExternalCitationDetailsIF.java | 29 +++ .../FileCitationDetailsIF.java | 29 +++ .../MemoryCitationDetailsIF.java | 27 +++ .../MessageCitationDetailsIF.java | 29 +++ .../RichTextCanvasElementIF.java | 71 ++++++ .../RichTextCanvasMessageUnfurlElementIF.java | 56 +++++ .../RichTextCanvasUserMentionElementIF.java | 59 +++++ .../RichTextCitationElementIF.java | 42 ++++ .../richtextelements/RichTextElement.java | 33 +++ .../RichTextListRecordElementIF.java | 65 ++++++ .../RichTextSalesforceDataFieldElementIF.java | 63 ++++++ .../RichTextTagElementIF.java | 60 +++++ .../RichTextWorkObjectMentionElementIF.java | 68 ++++++ .../RichTextWorkflowMentionElementIF.java | 66 ++++++ .../UnknownCitationDetails.java | 13 ++ .../WebCitationDetailsIF.java | 31 +++ .../blocks/objects/TriggerObjectIF.java | 24 ++ .../objects/WorkflowInputParameterIF.java | 23 ++ .../blocks/objects/WorkflowObjectIF.java | 20 ++ .../RichTextBlockDeserializationTest.java | 142 ++++++++++++ .../BlockElementSerializationTest.java | 28 +++ .../test/resources/citation_rich_text.json | 69 ++++++ .../resources/interactive_elements_batch.json | 49 ++++ .../src/test/resources/rich_text_blocks.json | 129 +++++++++++ .../resources/rich_text_elements_batch.json | 212 ++++++++++++++++++ 30 files changed, 1617 insertions(+) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/WorkflowButtonIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/CitationDetails.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasMessageUnfurlElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasUserMentionElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextListRecordElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextSalesforceDataFieldElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTagElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkObjectMentionElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkflowMentionElementIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java create mode 100644 slack-base/src/test/resources/citation_rich_text.json create mode 100644 slack-base/src/test/resources/interactive_elements_batch.json create mode 100644 slack-base/src/test/resources/rich_text_elements_batch.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java index a2daa028..4f71aa11 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java @@ -52,6 +52,9 @@ name = UsersMultiSelectMenu.TYPE ), @JsonSubTypes.Type(value = UrlSource.class, name = UrlSource.TYPE), + @JsonSubTypes.Type(value = FileInput.class, name = FileInput.TYPE), + @JsonSubTypes.Type(value = RichTextInput.class, name = RichTextInput.TYPE), + @JsonSubTypes.Type(value = WorkflowButton.class, name = WorkflowButton.TYPE), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java new file mode 100644 index 00000000..23ebe72f --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java @@ -0,0 +1,35 @@ +package com.hubspot.slack.client.models.blocks.elements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.List; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's file input element lets a user upload files. Works with the {@code input} block. + * Available on the Modals surface. Requires the {@code files:read} scope; 100MB per-file limit. + * @see File input element + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface FileInputIF extends BlockElement { + String TYPE = "file_input"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + Optional getActionId(); + + /** Valid file extensions accepted; all extensions accepted if empty. */ + List getFiletypes(); + + /** Max number of files, between 1 and 10 (defaults to 10). */ + Optional getMaxFiles(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java new file mode 100644 index 00000000..910bf55a --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java @@ -0,0 +1,56 @@ +package com.hubspot.slack.client.models.blocks.elements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBlock; +import com.hubspot.slack.client.models.blocks.objects.Text; +import java.util.List; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's rich text input element lets users enter formatted text in a WYSIWYG composer. + * Works with the {@code input} and {@code table} blocks; available on Modals and Home tabs. + * @see Rich text input element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextInputIF extends BlockElement, HasActionId { + String TYPE = "rich_text_input"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getActionId(); + + Optional getInitialValue(); + + Optional getDispatchActionConfig(); + + Optional getFocusOnLoad(); + + Optional getPlaceholder(); + + Optional getMinLines(); + + Optional getMaxLines(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonDeserialize(as = RichTextInput.DispatchActionConfig.class) + interface DispatchActionConfigIF { + List getTriggerActionsOn(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/WorkflowButtonIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/WorkflowButtonIF.java new file mode 100644 index 00000000..2d1692f8 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/WorkflowButtonIF.java @@ -0,0 +1,41 @@ +package com.hubspot.slack.client.models.blocks.elements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.objects.Text; +import com.hubspot.slack.client.models.blocks.objects.WorkflowObject; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's workflow button runs a link trigger with customizable inputs when clicked. + * Works with the {@code section} and {@code actions} blocks; available on the Messages surface. + * @see Workflow button element + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface WorkflowButtonIF extends BlockElement, HasActionId { + String TYPE = "workflow_button"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter(order = 1) + Text getText(); + + @Value.Parameter(order = 2) + WorkflowObject getWorkflow(); + + @Value.Parameter(order = 3) + String getActionId(); + + Optional getStyle(); + + Optional getAccessibilityLabel(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/CitationDetails.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/CitationDetails.java new file mode 100644 index 00000000..42545d0d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/CitationDetails.java @@ -0,0 +1,45 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Polymorphic {@code details} object of a {@link RichTextCitationElement}. The concrete type is + * discriminated by {@code citation_type}: {@code file}, {@code external}, {@code web}, + * {@code message}, or {@code memory}. + * @see Citation element + */ +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "citation_type", + defaultImpl = UnknownCitationDetails.class +) +@JsonSubTypes( + { + @JsonSubTypes.Type( + value = FileCitationDetails.class, + name = FileCitationDetails.CITATION_TYPE + ), + @JsonSubTypes.Type( + value = ExternalCitationDetails.class, + name = ExternalCitationDetails.CITATION_TYPE + ), + @JsonSubTypes.Type( + value = WebCitationDetails.class, + name = WebCitationDetails.CITATION_TYPE + ), + @JsonSubTypes.Type( + value = MessageCitationDetails.class, + name = MessageCitationDetails.CITATION_TYPE + ), + @JsonSubTypes.Type( + value = MemoryCitationDetails.class, + name = MemoryCitationDetails.CITATION_TYPE + ), + } +) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public interface CitationDetails { + String getCitationType(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java new file mode 100644 index 00000000..cae5bec9 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java @@ -0,0 +1,29 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * An {@code external} citation details object (the {@code details} of a citation element). + * @see Citation element — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface ExternalCitationDetailsIF extends CitationDetails { + String CITATION_TYPE = "external"; + + @Override + @Value.Derived + default String getCitationType() { + return CITATION_TYPE; + } + + Optional getAppName(); + + Optional getAppIconUrl(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java new file mode 100644 index 00000000..b4e83262 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java @@ -0,0 +1,29 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A {@code file} citation details object (the {@code details} of a citation element). + * @see Citation element — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface FileCitationDetailsIF extends CitationDetails { + String CITATION_TYPE = "file"; + + @Override + @Value.Derived + default String getCitationType() { + return CITATION_TYPE; + } + + Optional getDescriptor(); + + Optional getFileId(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java new file mode 100644 index 00000000..3972abc0 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java @@ -0,0 +1,27 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A {@code memory} citation details object (the {@code details} of a citation element). + * @see Citation element — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface MemoryCitationDetailsIF extends CitationDetails { + String CITATION_TYPE = "memory"; + + @Override + @Value.Derived + default String getCitationType() { + return CITATION_TYPE; + } + + Optional getMemoryId(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java new file mode 100644 index 00000000..f39cd83d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java @@ -0,0 +1,29 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A {@code message} citation details object (the {@code details} of a citation element). + * @see Citation element — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface MessageCitationDetailsIF extends CitationDetails { + String CITATION_TYPE = "message"; + + @Override + @Value.Derived + default String getCitationType() { + return CITATION_TYPE; + } + + Optional getChannel(); + + Optional getMessageTs(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java new file mode 100644 index 00000000..1ef5da4b --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java @@ -0,0 +1,71 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's canvas element renders as a link to a canvas. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Canvas element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextCanvasElementIF extends RichTextElement { + String TYPE = "canvas"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getFileId(); + + Optional getLabel(); + + Optional getHideTitle(); + + Optional getSectionId(); + + Optional getText(); + + Optional getUrl(); + + @JsonProperty("is_skill_invocation") + Optional getIsSkillInvocation(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextCanvasElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasMessageUnfurlElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasMessageUnfurlElementIF.java new file mode 100644 index 00000000..031cabc6 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasMessageUnfurlElementIF.java @@ -0,0 +1,56 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's canvas message unfurl element renders as an inline preview of a message inside a canvas. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Canvas message unfurl element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextCanvasMessageUnfurlElementIF extends RichTextElement { + String TYPE = "canvas_message_unfurl"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + String getRootMessageTs(); + + String getRootMessageChannel(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextCanvasMessageUnfurlElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasUserMentionElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasUserMentionElementIF.java new file mode 100644 index 00000000..596da2a4 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasUserMentionElementIF.java @@ -0,0 +1,59 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's canvas user mention element renders as a user mention in canvas content. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Canvas user mention element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextCanvasUserMentionElementIF extends RichTextElement { + String TYPE = "canvas_user_mention"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getUserId(); + + Optional getThreadId(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextCanvasUserMentionElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java new file mode 100644 index 00000000..a2dd5ec2 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java @@ -0,0 +1,42 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's citation element renders as an AI citation (file, external, web, message, or memory). + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Citation element + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextCitationElementIF extends RichTextElement { + String TYPE = "citation"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + String getUrl(); + + String getText(); + + int getIndex(); + + CitationDetails getDetails(); + + Optional getFromLlm(); + + @JsonProperty("is_slack_url") + Optional getIsSlackUrl(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java index df2f9051..ccda4f66 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextElement.java @@ -64,6 +64,39 @@ value = RichTextFileElement.class, name = RichTextFileElement.TYPE ), + @JsonSubTypes.Type(value = RichTextTagElement.class, name = RichTextTagElement.TYPE), + @JsonSubTypes.Type( + value = RichTextListRecordElement.class, + name = RichTextListRecordElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextWorkObjectMentionElement.class, + name = RichTextWorkObjectMentionElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextWorkflowMentionElement.class, + name = RichTextWorkflowMentionElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextSalesforceDataFieldElement.class, + name = RichTextSalesforceDataFieldElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextCanvasElement.class, + name = RichTextCanvasElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextCanvasUserMentionElement.class, + name = RichTextCanvasUserMentionElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextCanvasMessageUnfurlElement.class, + name = RichTextCanvasMessageUnfurlElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextCitationElement.class, + name = RichTextCitationElement.TYPE + ), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextListRecordElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextListRecordElementIF.java new file mode 100644 index 00000000..6facaf79 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextListRecordElementIF.java @@ -0,0 +1,65 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's list record element renders as a link to a Slack list record. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see List record element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextListRecordElementIF extends RichTextElement { + String TYPE = "list_record"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getFileId(); + + Optional getRecordId(); + + Optional getViewId(); + + Optional getText(); + + Optional getUrl(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextListRecordElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextSalesforceDataFieldElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextSalesforceDataFieldElementIF.java new file mode 100644 index 00000000..d34c287c --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextSalesforceDataFieldElementIF.java @@ -0,0 +1,63 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's Salesforce data field element renders as a Salesforce data field reference. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Salesforce data field element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextSalesforceDataFieldElementIF extends RichTextElement { + String TYPE = "salesforce_data_field"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getSalesforceRecordId(); + + Optional getSalesforceFieldLabel(); + + Optional getSalesforceFieldApiName(); + + Optional getSalesforceIncludeFieldLabel(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextSalesforceDataFieldElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTagElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTagElementIF.java new file mode 100644 index 00000000..326a68fd --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextTagElementIF.java @@ -0,0 +1,60 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's tag element renders as a colored tag or pill. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Tag element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextTagElementIF extends RichTextElement { + String TYPE = "tag"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + @Value.Parameter + String getText(); + + /** One of: gray, brown, purple, indigo, blue, green, yellow, orange, red. */ + Optional getColor(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextTagElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkObjectMentionElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkObjectMentionElementIF.java new file mode 100644 index 00000000..511c83e0 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkObjectMentionElementIF.java @@ -0,0 +1,68 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's work object mention element renders as a Work Object reference. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Work object mention element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextWorkObjectMentionElementIF extends RichTextElement { + String TYPE = "work_object_mention"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + String getEntityId(); + + String getAppId(); + + String getText(); + + String getUrl(); + + Optional getIconUrl(); + + Optional getFullSizePreviewEnabled(); + + Optional getProductName(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextWorkObjectMentionElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkflowMentionElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkflowMentionElementIF.java new file mode 100644 index 00000000..4abef942 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextWorkflowMentionElementIF.java @@ -0,0 +1,66 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's workflow mention element renders as a link to a workflow. + * This is a rich text element, compatible only with the {@code rich_text} block. It must be + * used within the {@code rich_text_list}, {@code rich_text_quote}, or {@code rich_text_section} + * block element within the {@code rich_text} block's {@code elements} array. + * @see Workflow mention element + */ +@Immutable +@HubSpotStyle +@Value.Enclosing +@JsonNaming(SnakeCaseStrategy.class) +public interface RichTextWorkflowMentionElementIF extends RichTextElement { + String TYPE = "workflow_mention"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + String getWorkflowId(); + + String getFunctionTriggerId(); + + String getText(); + + Optional getUrl(); + + Optional getChannelId(); + + Optional getTs(); + + Optional getStyle(); + + @Immutable + @HubSpotStyle + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonDeserialize(as = RichTextWorkflowMentionElement.Style.class) + interface StyleIF { + Optional getBold(); + + Optional getItalic(); + + Optional getStrike(); + + Optional getHighlight(); + + Optional getClientHighlight(); + + Optional getUnderline(); + + Optional getUnlink(); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java new file mode 100644 index 00000000..b9d5da46 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java @@ -0,0 +1,13 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +public class UnknownCitationDetails implements CitationDetails { + + public static final String CITATION_TYPE = "unknown"; + + protected UnknownCitationDetails() {} + + @Override + public String getCitationType() { + return CITATION_TYPE; + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java new file mode 100644 index 00000000..7942e016 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java @@ -0,0 +1,31 @@ +package com.hubspot.slack.client.models.blocks.elements.richtextelements; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A {@code web} citation details object (the {@code details} of a citation element). + * @see Citation element — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface WebCitationDetailsIF extends CitationDetails { + String CITATION_TYPE = "web"; + + @Override + @Value.Derived + default String getCitationType() { + return CITATION_TYPE; + } + + Optional getDisplayName(); + + Optional getTitle(); + + Optional getSnippet(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java new file mode 100644 index 00000000..19f3f337 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java @@ -0,0 +1,24 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.List; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's trigger composition object — a link trigger referenced by a {@link WorkflowObject}. + * @see Trigger object + */ +@Immutable +@HubSpotStyle +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonNaming(SnakeCaseStrategy.class) +public interface TriggerObjectIF { + @Value.Parameter + String getUrl(); + + List getCustomizableInputParameters(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java new file mode 100644 index 00000000..3f73fe29 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java @@ -0,0 +1,23 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A single customizable input parameter ({@code name}/{@code value}) in a {@link TriggerObject}'s + * {@code customizable_input_parameters} array. + * @see Trigger object — fields + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface WorkflowInputParameterIF { + @Value.Parameter(order = 1) + String getName(); + + @Value.Parameter(order = 2) + String getValue(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java new file mode 100644 index 00000000..0b9cfea8 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java @@ -0,0 +1,20 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * Slack's workflow composition object — describes the workflow that runs when a workflow button is + * clicked. Contains a single required {@link TriggerObject}. + * @see Workflow object + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface WorkflowObjectIF { + @Value.Parameter + TriggerObject getTrigger(); +} diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java index 30f36e86..06138658 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/RichTextBlockDeserializationTest.java @@ -6,21 +6,31 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hubspot.slack.client.jackson.ObjectMapperUtils; import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.FileCitationDetails; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextAttachmentMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBroadcastElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBroadcastRange; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextCanvasElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextCanvasMessageUnfurlElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextCanvasUserMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextChannelElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextCitationElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextColorElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextDateElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextEmojiElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextFileElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextLinkElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextListRecordElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextMessageMentionElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSalesforceDataFieldElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextSection; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextTagElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextTeamElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextTextElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextUserElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextUserGroupElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextWorkObjectMentionElement; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextWorkflowMentionElement; import com.hubspot.slack.client.models.blocks.elements.richtextelements.UnknownRichTextElement; import java.io.IOException; import org.junit.BeforeClass; @@ -44,6 +54,15 @@ public class RichTextBlockDeserializationTest { private static final int SECTION_WITH_TEAM_ELEMENT_INDEX = 12; private static final int SECTION_WITH_MESSAGE_MENTION_ELEMENT_INDEX = 13; private static final int SECTION_WITH_FILE_ELEMENT_INDEX = 14; + private static final int SECTION_WITH_TAG_ELEMENT_INDEX = 15; + private static final int SECTION_WITH_LIST_RECORD_ELEMENT_INDEX = 16; + private static final int SECTION_WITH_WORK_OBJECT_MENTION_ELEMENT_INDEX = 17; + private static final int SECTION_WITH_WORKFLOW_MENTION_ELEMENT_INDEX = 18; + private static final int SECTION_WITH_SALESFORCE_DATA_FIELD_ELEMENT_INDEX = 19; + private static final int SECTION_WITH_CANVAS_ELEMENT_INDEX = 20; + private static final int SECTION_WITH_CANVAS_USER_MENTION_ELEMENT_INDEX = 21; + private static final int SECTION_WITH_CANVAS_MESSAGE_UNFURL_ELEMENT_INDEX = 22; + private static final int SECTION_WITH_CITATION_ELEMENT_INDEX = 23; private static RichTextSection[] sections; @BeforeClass @@ -201,6 +220,129 @@ public void itDeserializesFileElement() { ); } + @Test + public void itDeserializesTagElement() { + assertThat(sections[SECTION_WITH_TAG_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextTagElement + .builder() + .setText("Urgent") + .setColor("red") + .setStyle(RichTextTagElement.Style.builder().setBold(true).build()) + .build() + ); + } + + @Test + public void itDeserializesListRecordElement() { + assertThat(sections[SECTION_WITH_LIST_RECORD_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextListRecordElement + .builder() + .setFileId("F123ABC456") + .setRecordId("Rec1") + .setText("a record") + .build() + ); + } + + @Test + public void itDeserializesWorkObjectMentionElement() { + assertThat(sections[SECTION_WITH_WORK_OBJECT_MENTION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextWorkObjectMentionElement + .builder() + .setEntityId("E123") + .setAppId("A123") + .setText("the object") + .setUrl("https://example.com/object") + .build() + ); + } + + @Test + public void itDeserializesWorkflowMentionElement() { + assertThat(sections[SECTION_WITH_WORKFLOW_MENTION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextWorkflowMentionElement + .builder() + .setWorkflowId("Wf123") + .setFunctionTriggerId("Ft123") + .setText("the workflow") + .build() + ); + } + + @Test + public void itDeserializesSalesforceDataFieldElement() { + assertThat(sections[SECTION_WITH_SALESFORCE_DATA_FIELD_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextSalesforceDataFieldElement + .builder() + .setSalesforceRecordId("SR123") + .setSalesforceFieldLabel("Stage") + .build() + ); + } + + @Test + public void itDeserializesCanvasElement() { + assertThat(sections[SECTION_WITH_CANVAS_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextCanvasElement + .builder() + .setFileId("F_canvas") + .setLabel("Doc") + .setText("the canvas") + .build() + ); + } + + @Test + public void itDeserializesCanvasUserMentionElement() { + assertThat(sections[SECTION_WITH_CANVAS_USER_MENTION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextCanvasUserMentionElement + .builder() + .setUserId("U123ABC456") + .setThreadId("Th123") + .build() + ); + } + + @Test + public void itDeserializesCanvasMessageUnfurlElement() { + assertThat(sections[SECTION_WITH_CANVAS_MESSAGE_UNFURL_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextCanvasMessageUnfurlElement + .builder() + .setRootMessageTs("1234567890.123456") + .setRootMessageChannel("C123ABC456") + .build() + ); + } + + @Test + public void itDeserializesCitationElement() { + assertThat(sections[SECTION_WITH_CITATION_ELEMENT_INDEX].getElements()) + .containsExactly( + RichTextCitationElement + .builder() + .setUrl("https://example.com/doc") + .setText("Source") + .setIndex(1) + .setDetails( + FileCitationDetails + .builder() + .setDescriptor("notes.txt") + .setFileId("F999") + .build() + ) + .setFromLlm(true) + .build() + ); + } + @Test public void itDeserializesUnknownElementAsUnknownBlockElement() { assertThat(sections[SECTION_WITH_UNKNOWN_ELEMENT_INDEX].getElements()) diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java index 03461023..5e668ac7 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java @@ -37,6 +37,34 @@ public void testFileRichTextSerialization() throws IOException { testSerialization("file_rich_text.json", Block[].class); } + @Test + public void testRichTextElementsBatchSerialization() throws IOException { + testSerialization("rich_text_elements_batch.json", Block[].class); + } + + @Test + public void testInteractiveElementsBatchSerialization() throws IOException { + testSerialization("interactive_elements_batch.json", BlockElement[].class); + } + + @Test + public void testCitationRichTextSerialization() throws IOException { + testSerialization("citation_rich_text.json", Block[].class); + } + + @Test + public void testInteractiveElementsDeserializeToExpectedTypes() throws IOException { + BlockElement[] elements = ObjectMapperUtils + .mapper() + .readValue( + JsonLoader.loadJsonFromFile("interactive_elements_batch.json"), + BlockElement[].class + ); + assertThat(elements[0]).isInstanceOf(FileInput.class); + assertThat(elements[1]).isInstanceOf(RichTextInput.class); + assertThat(elements[2]).isInstanceOf(WorkflowButton.class); + } + @Test public void testUnknownBlockSerialization() throws IOException { String rawJson = JsonLoader.loadJsonFromFile("unknown_block_element.json"); diff --git a/slack-base/src/test/resources/citation_rich_text.json b/slack-base/src/test/resources/citation_rich_text.json new file mode 100644 index 00000000..b9358e14 --- /dev/null +++ b/slack-base/src/test/resources/citation_rich_text.json @@ -0,0 +1,69 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "citation", + "url": "https://example.com/doc", + "text": "File source", + "index": 1, + "details": { + "citation_type": "file", + "descriptor": "notes.txt", + "file_id": "F999" + }, + "from_llm": true, + "is_slack_url": false + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "citation", + "url": "https://example.com/web", + "text": "Web source", + "index": 2, + "details": { + "citation_type": "web", + "display_name": "Example", + "title": "Example Page", + "snippet": "a snippet" + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "citation", + "url": "https://slack.com/archives/C1/p123", + "text": "Message source", + "index": 3, + "details": { + "citation_type": "message", + "channel": "C123ABC456", + "message_ts": "1234567890.123456" + }, + "is_slack_url": true + } + ] + } + ] + } +] diff --git a/slack-base/src/test/resources/interactive_elements_batch.json b/slack-base/src/test/resources/interactive_elements_batch.json new file mode 100644 index 00000000..28bd9f8a --- /dev/null +++ b/slack-base/src/test/resources/interactive_elements_batch.json @@ -0,0 +1,49 @@ +[ + { + "type": "file_input", + "action_id": "file_input_1", + "filetypes": ["pdf", "png"], + "max_files": 3 + }, + { + "type": "rich_text_input", + "action_id": "rich_text_input_1", + "initial_value": { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "text", "text": "hi" }] + } + ] + }, + "dispatch_action_config": { + "trigger_actions_on": ["on_enter_pressed"] + }, + "focus_on_load": true, + "placeholder": { + "type": "plain_text", + "text": "Type something" + }, + "min_lines": 2, + "max_lines": 10 + }, + { + "type": "workflow_button", + "text": { + "type": "plain_text", + "text": "Run" + }, + "workflow": { + "trigger": { + "url": "https://slack.com/shortcuts/Ft0000000000/abc123", + "customizable_input_parameters": [ + { "name": "input_a", "value": "value_a" } + ] + } + }, + "action_id": "workflow_button_1", + "style": "primary", + "accessibility_label": "Run the workflow" + } +] diff --git a/slack-base/src/test/resources/rich_text_blocks.json b/slack-base/src/test/resources/rich_text_blocks.json index 57a75b7a..59daaded 100644 --- a/slack-base/src/test/resources/rich_text_blocks.json +++ b/slack-base/src/test/resources/rich_text_blocks.json @@ -263,5 +263,134 @@ ] } ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "tag", "text": "Urgent", "color": "red", "style": { "bold": true } } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "list_record", "file_id": "F123ABC456", "record_id": "Rec1", "text": "a record" } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "work_object_mention", + "entity_id": "E123", + "app_id": "A123", + "text": "the object", + "url": "https://example.com/object" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "workflow_mention", + "workflow_id": "Wf123", + "function_trigger_id": "Ft123", + "text": "the workflow" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "salesforce_data_field", + "salesforce_record_id": "SR123", + "salesforce_field_label": "Stage" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "canvas", "file_id": "F_canvas", "label": "Doc", "text": "the canvas" } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "canvas_user_mention", "user_id": "U123ABC456", "thread_id": "Th123" } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "canvas_message_unfurl", + "root_message_ts": "1234567890.123456", + "root_message_channel": "C123ABC456" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "citation", + "url": "https://example.com/doc", + "text": "Source", + "index": 1, + "details": { + "citation_type": "file", + "descriptor": "notes.txt", + "file_id": "F999" + }, + "from_llm": true + } + ] + } + ] } ] diff --git a/slack-base/src/test/resources/rich_text_elements_batch.json b/slack-base/src/test/resources/rich_text_elements_batch.json new file mode 100644 index 00000000..ac52a7a8 --- /dev/null +++ b/slack-base/src/test/resources/rich_text_elements_batch.json @@ -0,0 +1,212 @@ +[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "tag", + "text": "Urgent", + "color": "red", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "list_record", + "file_id": "F123ABC456", + "record_id": "Rec1", + "view_id": "View1", + "text": "a record", + "url": "https://example.com/record", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "work_object_mention", + "entity_id": "E123", + "app_id": "A123", + "text": "the object", + "url": "https://example.com/object", + "icon_url": "https://example.com/icon.png", + "full_size_preview_enabled": true, + "product_name": "Acme", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "workflow_mention", + "workflow_id": "Wf123", + "function_trigger_id": "Ft123", + "text": "the workflow", + "url": "https://example.com/workflow", + "channel_id": "C123ABC456", + "ts": "1234567890.123456", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "salesforce_data_field", + "salesforce_record_id": "SR123", + "salesforce_field_label": "Stage", + "salesforce_field_api_name": "StageName", + "salesforce_include_field_label": true, + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "canvas", + "file_id": "F_canvas", + "label": "Doc", + "hide_title": true, + "section_id": "Sec1", + "text": "the canvas", + "url": "https://example.com/canvas", + "is_skill_invocation": true, + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "canvas_user_mention", + "user_id": "U123ABC456", + "thread_id": "Th123", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true, + "unlink": true + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "canvas_message_unfurl", + "root_message_ts": "1234567890.123456", + "root_message_channel": "C123ABC456", + "style": { + "bold": true, + "italic": true, + "strike": true, + "highlight": true, + "client_highlight": true, + "underline": true + } + } + ] + } + ] + } +] From 84b2e9273c58eb2b8e721f74c44d0b02f6296545 Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 19:31:48 +0300 Subject: [PATCH 6/9] Remove unsupported type, create test for null values. --- .../models/blocks/elements/BlockElement.java | 2 - .../models/blocks/elements/FileInputIF.java | 35 ---- .../blocks/elements/RichTextInputIF.java | 56 ------ .../UnknownCitationDetails.java | 7 + .../blocks/objects/TriggerObjectIF.java | 4 +- .../objects/WorkflowInputParameterIF.java | 2 + .../blocks/objects/WorkflowObjectIF.java | 2 + .../BlockElementSerializationTest.java | 16 +- .../resources/interactive_elements_batch.json | 29 --- .../src/test/resources/minimal_elements.json | 168 ++++++++++++++++++ .../blockkit/BlockKitPayloadTester.java | 30 ++-- 11 files changed, 212 insertions(+), 139 deletions(-) delete mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java delete mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java create mode 100644 slack-base/src/test/resources/minimal_elements.json diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java index 4f71aa11..8328979e 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/BlockElement.java @@ -52,8 +52,6 @@ name = UsersMultiSelectMenu.TYPE ), @JsonSubTypes.Type(value = UrlSource.class, name = UrlSource.TYPE), - @JsonSubTypes.Type(value = FileInput.class, name = FileInput.TYPE), - @JsonSubTypes.Type(value = RichTextInput.class, name = RichTextInput.TYPE), @JsonSubTypes.Type(value = WorkflowButton.class, name = WorkflowButton.TYPE), } ) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java deleted file mode 100644 index 23ebe72f..00000000 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/FileInputIF.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.hubspot.slack.client.models.blocks.elements; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.hubspot.immutables.style.HubSpotStyle; -import java.util.List; -import java.util.Optional; -import org.immutables.value.Value; -import org.immutables.value.Value.Immutable; - -/** - * Slack's file input element lets a user upload files. Works with the {@code input} block. - * Available on the Modals surface. Requires the {@code files:read} scope; 100MB per-file limit. - * @see File input element - */ -@Immutable -@HubSpotStyle -@JsonNaming(SnakeCaseStrategy.class) -public interface FileInputIF extends BlockElement { - String TYPE = "file_input"; - - @Override - @Value.Derived - default String getType() { - return TYPE; - } - - Optional getActionId(); - - /** Valid file extensions accepted; all extensions accepted if empty. */ - List getFiletypes(); - - /** Max number of files, between 1 and 10 (defaults to 10). */ - Optional getMaxFiles(); -} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java deleted file mode 100644 index 910bf55a..00000000 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/RichTextInputIF.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.hubspot.slack.client.models.blocks.elements; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.hubspot.immutables.style.HubSpotStyle; -import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBlock; -import com.hubspot.slack.client.models.blocks.objects.Text; -import java.util.List; -import java.util.Optional; -import org.immutables.value.Value; -import org.immutables.value.Value.Immutable; - -/** - * Slack's rich text input element lets users enter formatted text in a WYSIWYG composer. - * Works with the {@code input} and {@code table} blocks; available on Modals and Home tabs. - * @see Rich text input element - */ -@Immutable -@HubSpotStyle -@Value.Enclosing -@JsonNaming(SnakeCaseStrategy.class) -public interface RichTextInputIF extends BlockElement, HasActionId { - String TYPE = "rich_text_input"; - - @Override - @Value.Derived - default String getType() { - return TYPE; - } - - @Value.Parameter - String getActionId(); - - Optional getInitialValue(); - - Optional getDispatchActionConfig(); - - Optional getFocusOnLoad(); - - Optional getPlaceholder(); - - Optional getMinLines(); - - Optional getMaxLines(); - - @Immutable - @HubSpotStyle - @JsonNaming(SnakeCaseStrategy.class) - @JsonInclude(JsonInclude.Include.NON_EMPTY) - @JsonDeserialize(as = RichTextInput.DispatchActionConfig.class) - interface DispatchActionConfigIF { - List getTriggerActionsOn(); - } -} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java index b9d5da46..e4083ab5 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java @@ -1,5 +1,12 @@ package com.hubspot.slack.client.models.blocks.elements.richtextelements; +/** + * Read-only deserialization fallback for a {@link CitationDetails} whose {@code citation_type} we + * don't model (mirrors {@code UnknownRichTextElement} / {@code UnknownBlock}). It drops the original + * payload, so it must NOT be re-serialized and sent back to Slack — re-serializing emits + * {@code citation_type: "unknown"}, which Slack rejects. Use it only to tolerate unknown inbound + * citation types without failing deserialization. + */ public class UnknownCitationDetails implements CitationDetails { public static final String CITATION_TYPE = "unknown"; diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java index 19f3f337..aa47b41d 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.google.common.collect.ImmutableList; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.List; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -20,5 +20,5 @@ public interface TriggerObjectIF { @Value.Parameter String getUrl(); - List getCustomizableInputParameters(); + ImmutableList getCustomizableInputParameters(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java index 3f73fe29..1a2fb66b 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java @@ -1,5 +1,6 @@ package com.hubspot.slack.client.models.blocks.objects; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; @@ -13,6 +14,7 @@ */ @Immutable @HubSpotStyle +@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonNaming(SnakeCaseStrategy.class) public interface WorkflowInputParameterIF { @Value.Parameter(order = 1) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java index 0b9cfea8..08f35985 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java @@ -1,5 +1,6 @@ package com.hubspot.slack.client.models.blocks.objects; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; @@ -13,6 +14,7 @@ */ @Immutable @HubSpotStyle +@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonNaming(SnakeCaseStrategy.class) public interface WorkflowObjectIF { @Value.Parameter diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java index 5e668ac7..ec3876ed 100644 --- a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/elements/BlockElementSerializationTest.java @@ -52,6 +52,18 @@ public void testCitationRichTextSerialization() throws IOException { testSerialization("citation_rich_text.json", Block[].class); } + // Null-omission guard for all 14 Messages-surface elements (Slack rejects null-valued fields). + // Each fixture entry is required-fields-only, so every optional is ABSENT in the raw JSON. The + // round-trip asserts readTree(raw).equals(readTree(reserialized)); since a JSON null is a present + // NullNode (not the same as an absent key), if serialization emitted "field": null for any absent + // optional the generated tree would gain a key the raw tree lacks and this assertion would fail. + // The mapper sets no global serialization inclusion, so this genuinely exercises each model's + // @JsonInclude(NON_EMPTY). + @Test + public void testMinimalElementsOmitNulls() throws IOException { + testSerialization("minimal_elements.json", Block[].class); + } + @Test public void testInteractiveElementsDeserializeToExpectedTypes() throws IOException { BlockElement[] elements = ObjectMapperUtils @@ -60,9 +72,7 @@ public void testInteractiveElementsDeserializeToExpectedTypes() throws IOExcepti JsonLoader.loadJsonFromFile("interactive_elements_batch.json"), BlockElement[].class ); - assertThat(elements[0]).isInstanceOf(FileInput.class); - assertThat(elements[1]).isInstanceOf(RichTextInput.class); - assertThat(elements[2]).isInstanceOf(WorkflowButton.class); + assertThat(elements[0]).isInstanceOf(WorkflowButton.class); } @Test diff --git a/slack-base/src/test/resources/interactive_elements_batch.json b/slack-base/src/test/resources/interactive_elements_batch.json index 28bd9f8a..03c8a4d6 100644 --- a/slack-base/src/test/resources/interactive_elements_batch.json +++ b/slack-base/src/test/resources/interactive_elements_batch.json @@ -1,33 +1,4 @@ [ - { - "type": "file_input", - "action_id": "file_input_1", - "filetypes": ["pdf", "png"], - "max_files": 3 - }, - { - "type": "rich_text_input", - "action_id": "rich_text_input_1", - "initial_value": { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [{ "type": "text", "text": "hi" }] - } - ] - }, - "dispatch_action_config": { - "trigger_actions_on": ["on_enter_pressed"] - }, - "focus_on_load": true, - "placeholder": { - "type": "plain_text", - "text": "Type something" - }, - "min_lines": 2, - "max_lines": 10 - }, { "type": "workflow_button", "text": { diff --git a/slack-base/src/test/resources/minimal_elements.json b/slack-base/src/test/resources/minimal_elements.json new file mode 100644 index 00000000..4db382f4 --- /dev/null +++ b/slack-base/src/test/resources/minimal_elements.json @@ -0,0 +1,168 @@ +[ + { + "type": "rich_text", + "elements": [ + { "type": "rich_text_section", "elements": [{ "type": "tag", "text": "Urgent" }] } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "canvas", "file_id": "F0C6AVAQFC7" }] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "citation", + "url": "https://example.com/doc", + "text": "Source", + "index": 1, + "details": { "citation_type": "file" } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "salesforce_data_field", "salesforce_record_id": "SR123" } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "work_object_mention", + "entity_id": "E123", + "app_id": "A123", + "text": "the object", + "url": "https://example.com/object" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { "type": "attachment_mention", "url": "https://example.com/attachment" } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "message_mention", + "channel_id": "C123ABC456", + "message_ts": "1234567890.123456" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "file", "file_id": "F0C6AVAQFC7" }] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "team", "team_id": "T123ABC456" }] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "list_record", "file_id": "F123ABC456" }] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "workflow_mention", + "workflow_id": "Wf123", + "function_trigger_id": "Ft123", + "text": "the workflow" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{ "type": "canvas_user_mention", "user_id": "U123ABC456" }] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "canvas_message_unfurl", + "root_message_ts": "1234567890.123456", + "root_message_channel": "C123ABC456" + } + ] + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "workflow_button", + "text": { "type": "plain_text", "text": "Run" }, + "workflow": { + "trigger": { "url": "https://slack.com/shortcuts/Ft0000000000/abc123" } + }, + "action_id": "wb_1" + } + ] + } +] diff --git a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java index 8556ff54..d2fd16cf 100644 --- a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java @@ -172,13 +172,18 @@ public Result postBlocksJson(String blocksJ * standalone or from {@link #postFromFolder(Path)}. */ public PayloadSendResult postFromFile(Path jsonFile) { - String fileName = jsonFile.getFileName().toString(); + return postFile(jsonFile, jsonFile.getFileName().toString()); + } + private PayloadSendResult postFile(Path jsonFile, String displayName) { String json; try { json = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); } catch (IOException e) { - return PayloadSendResult.failure(fileName, "Could not read file: " + describe(e)); + return PayloadSendResult.failure( + displayName, + "Could not read file: " + describe(e) + ); } List blocks; @@ -186,7 +191,7 @@ public PayloadSendResult postFromFile(Path jsonFile) { blocks = blocksFromJson(json); } catch (RuntimeException e) { return PayloadSendResult.failure( - fileName, + displayName, "Could not parse/deserialize payload: " + describe(e) ); } @@ -194,17 +199,17 @@ public PayloadSendResult postFromFile(Path jsonFile) { try { Result result = postBlocks( blocks, - "Block Kit payload test: " + fileName + "Block Kit payload test: " + displayName ); if (result.isOk()) { - return PayloadSendResult.success(fileName, result.unwrapOrElseThrow().getTs()); + return PayloadSendResult.success(displayName, result.unwrapOrElseThrow().getTs()); } return PayloadSendResult.failure( - fileName, + displayName, reasonFor(result.unwrapErrOrElseThrow()) ); } catch (RuntimeException e) { - return PayloadSendResult.failure(fileName, "Post failed: " + describe(e)); + return PayloadSendResult.failure(displayName, "Post failed: " + describe(e)); } } @@ -218,12 +223,13 @@ private String reasonFor(SlackError error) { } /** - * Posts every {@code *.json} payload in the given folder (non-recursive, sorted by file name), - * reusing {@link #postFromFile(Path)} for each, and returns an aggregate {@link PayloadSendSummary}. + * Posts every {@code *.json} payload found under the given folder, recursively (so per-element + * subfolders are included), sorted by relative path. Each result is labelled with its path + * relative to {@code folder} (e.g. {@code tag/full.json}). Returns an aggregate summary. */ public PayloadSendSummary postFromFolder(Path folder) { List files; - try (Stream stream = Files.list(folder)) { + try (Stream stream = Files.walk(folder)) { files = stream .filter(Files::isRegularFile) @@ -231,12 +237,12 @@ public PayloadSendSummary postFromFolder(Path folder) { .sorted() .collect(Collectors.toList()); } catch (IOException e) { - throw new RuntimeException("Could not list folder " + folder, e); + throw new RuntimeException("Could not walk folder " + folder, e); } List results = new ArrayList<>(); for (Path file : files) { - results.add(postFromFile(file)); + results.add(postFile(file, folder.relativize(file).toString())); } return new PayloadSendSummary(results); } From 4a157d7c6409e946e772feaf2793514cd4d6da29 Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Mon, 5 Oct 2026 19:32:44 +0300 Subject: [PATCH 7/9] Resolve conflict --- .../models/blocks/objects/WorkflowIF.java | 19 +++++++++++++++ .../blocks/objects/WorkflowTriggerIF.java | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java create mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java new file mode 100644 index 00000000..10e45527 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java @@ -0,0 +1,19 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A workflow object describing the workflow that runs when a workflow button is clicked. + * @see Workflow button element + */ +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +public interface WorkflowIF { + @Value.Parameter + WorkflowTrigger getTrigger(); +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java new file mode 100644 index 00000000..9053daa7 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java @@ -0,0 +1,24 @@ +package com.hubspot.slack.client.models.blocks.objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.hubspot.immutables.style.HubSpotStyle; +import java.util.List; +import org.immutables.value.Value; +import org.immutables.value.Value.Immutable; + +/** + * A link trigger for a workflow, referenced by the {@link Workflow} object inside a workflow button. + * @see Workflow button element + */ +@Immutable +@HubSpotStyle +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonNaming(SnakeCaseStrategy.class) +public interface WorkflowTriggerIF { + @Value.Parameter + String getUrl(); + + List getCustomizableInputParameters(); +} From 65cb40d091f5854e717034171454919b22697d53 Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Tue, 6 Oct 2026 10:44:05 +0300 Subject: [PATCH 8/9] Fix issue with the serialization and validation. --- .../richtextelements/ExternalCitationDetailsIF.java | 5 ++--- .../richtextelements/FileCitationDetailsIF.java | 5 ++--- .../richtextelements/MemoryCitationDetailsIF.java | 3 +-- .../richtextelements/MessageCitationDetailsIF.java | 5 ++--- .../richtextelements/WebCitationDetailsIF.java | 7 +++---- slack-base/src/test/resources/minimal_elements.json | 6 +++++- .../blockkit/BlockKitPayloadFromFilesTest.java | 12 ++++++++++++ 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java index cae5bec9..bf31f96f 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -23,7 +22,7 @@ default String getCitationType() { return CITATION_TYPE; } - Optional getAppName(); + String getAppName(); - Optional getAppIconUrl(); + String getAppIconUrl(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java index b4e83262..2c52e34c 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -23,7 +22,7 @@ default String getCitationType() { return CITATION_TYPE; } - Optional getDescriptor(); + String getDescriptor(); - Optional getFileId(); + String getFileId(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java index 3972abc0..e0625b1d 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -23,5 +22,5 @@ default String getCitationType() { return CITATION_TYPE; } - Optional getMemoryId(); + String getMemoryId(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java index f39cd83d..c9aa599f 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -23,7 +22,7 @@ default String getCitationType() { return CITATION_TYPE; } - Optional getChannel(); + String getChannel(); - Optional getMessageTs(); + String getMessageTs(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java index 7942e016..7526b38d 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; -import java.util.Optional; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -23,9 +22,9 @@ default String getCitationType() { return CITATION_TYPE; } - Optional getDisplayName(); + String getDisplayName(); - Optional getTitle(); + String getTitle(); - Optional getSnippet(); + String getSnippet(); } diff --git a/slack-base/src/test/resources/minimal_elements.json b/slack-base/src/test/resources/minimal_elements.json index 4db382f4..3de08899 100644 --- a/slack-base/src/test/resources/minimal_elements.json +++ b/slack-base/src/test/resources/minimal_elements.json @@ -25,7 +25,11 @@ "url": "https://example.com/doc", "text": "Source", "index": 1, - "details": { "citation_type": "file" } + "details": { + "citation_type": "file", + "descriptor": "notes.txt", + "file_id": "F123ABC456" + } } ] } diff --git a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java index 0ff25dbc..c68c0d14 100644 --- a/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java @@ -68,6 +68,18 @@ public void itSendsPayloadFileOrFolderAndPrintsSummary() throws Exception { assertThat(summary.getTotal()) .as("expected at least one *.json payload under %s", path) .isGreaterThan(0); + + // Fail the test (and the build) if Slack rejected any payload, so a bad payload is not + // silently buried in a green build. The assertion message carries the full summary with the + // per-payload Slack error reasons. + assertThat(summary.getFailedCount()) + .as( + "Slack rejected %s of %s payload(s):%n%s", + summary.getFailedCount(), + summary.getTotal(), + summary.render() + ) + .isZero(); } } From c5da54b8d18ec270ad8af1f76f6c20c3dcb55125 Mon Sep 17 00:00:00 2001 From: Yurii Yakhnytsia Date: Tue, 6 Oct 2026 17:32:22 +0300 Subject: [PATCH 9/9] Remove redundant classes and annotatins. --- .../RichTextCanvasElementIF.java | 2 -- .../RichTextCitationElementIF.java | 2 -- .../RichTextFileElementIF.java | 2 -- .../blocks/objects/TriggerObjectIF.java | 4 ++-- .../models/blocks/objects/WorkflowIF.java | 19 --------------- .../blocks/objects/WorkflowTriggerIF.java | 24 ------------------- 6 files changed, 2 insertions(+), 51 deletions(-) delete mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java delete mode 100644 slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java index 1ef5da4b..c3b1f94c 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java @@ -1,7 +1,6 @@ package com.hubspot.slack.client.models.blocks.elements.richtextelements; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -43,7 +42,6 @@ default String getType() { Optional getUrl(); - @JsonProperty("is_skill_invocation") Optional getIsSkillInvocation(); Optional getStyle(); diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java index a2dd5ec2..d514e818 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java @@ -1,6 +1,5 @@ package com.hubspot.slack.client.models.blocks.elements.richtextelements; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.hubspot.immutables.style.HubSpotStyle; @@ -37,6 +36,5 @@ default String getType() { Optional getFromLlm(); - @JsonProperty("is_slack_url") Optional getIsSlackUrl(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java index b8651174..ff554fd8 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java @@ -1,7 +1,6 @@ package com.hubspot.slack.client.models.blocks.elements.richtextelements; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -40,7 +39,6 @@ default String getType() { Optional getUrl(); - @JsonProperty("is_skill_invocation") Optional getIsSkillInvocation(); Optional getStyle(); diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java index aa47b41d..19f3f337 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/TriggerObjectIF.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.google.common.collect.ImmutableList; import com.hubspot.immutables.style.HubSpotStyle; +import java.util.List; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -20,5 +20,5 @@ public interface TriggerObjectIF { @Value.Parameter String getUrl(); - ImmutableList getCustomizableInputParameters(); + List getCustomizableInputParameters(); } diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java deleted file mode 100644 index 10e45527..00000000 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowIF.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.hubspot.slack.client.models.blocks.objects; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.hubspot.immutables.style.HubSpotStyle; -import org.immutables.value.Value; -import org.immutables.value.Value.Immutable; - -/** - * A workflow object describing the workflow that runs when a workflow button is clicked. - * @see Workflow button element - */ -@Immutable -@HubSpotStyle -@JsonNaming(SnakeCaseStrategy.class) -public interface WorkflowIF { - @Value.Parameter - WorkflowTrigger getTrigger(); -} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java deleted file mode 100644 index 9053daa7..00000000 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowTriggerIF.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.hubspot.slack.client.models.blocks.objects; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.hubspot.immutables.style.HubSpotStyle; -import java.util.List; -import org.immutables.value.Value; -import org.immutables.value.Value.Immutable; - -/** - * A link trigger for a workflow, referenced by the {@link Workflow} object inside a workflow button. - * @see Workflow button element - */ -@Immutable -@HubSpotStyle -@JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonNaming(SnakeCaseStrategy.class) -public interface WorkflowTriggerIF { - @Value.Parameter - String getUrl(); - - List getCustomizableInputParameters(); -}