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..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,6 +52,7 @@ name = UsersMultiSelectMenu.TYPE ), @JsonSubTypes.Type(value = UrlSource.class, name = UrlSource.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/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..bf31f96f --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/ExternalCitationDetailsIF.java @@ -0,0 +1,28 @@ +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 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; + } + + String getAppName(); + + 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 new file mode 100644 index 00000000..2c52e34c --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/FileCitationDetailsIF.java @@ -0,0 +1,28 @@ +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 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; + } + + String getDescriptor(); + + 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 new file mode 100644 index 00000000..e0625b1d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MemoryCitationDetailsIF.java @@ -0,0 +1,26 @@ +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 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; + } + + 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 new file mode 100644 index 00000000..c9aa599f --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/MessageCitationDetailsIF.java @@ -0,0 +1,28 @@ +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 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; + } + + String getChannel(); + + String getMessageTs(); +} 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/RichTextCanvasElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java new file mode 100644 index 00000000..c3b1f94c --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCanvasElementIF.java @@ -0,0 +1,69 @@ +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 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(); + + 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..d514e818 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextCitationElementIF.java @@ -0,0 +1,40 @@ +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; + +/** + * 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(); + + 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 0483e7b6..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 @@ -48,6 +48,55 @@ value = RichTextEmojiElement.class, name = RichTextEmojiElement.TYPE ), + @JsonSubTypes.Type( + value = RichTextAttachmentMentionElement.class, + name = RichTextAttachmentMentionElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextTeamElement.class, + name = RichTextTeamElement.TYPE + ), + @JsonSubTypes.Type( + value = RichTextMessageMentionElement.class, + name = RichTextMessageMentionElement.TYPE + ), + @JsonSubTypes.Type( + 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/RichTextFileElementIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java new file mode 100644 index 00000000..ff554fd8 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextFileElementIF.java @@ -0,0 +1,64 @@ +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 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(); + + 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/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/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/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/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/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..e4083ab5 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/UnknownCitationDetails.java @@ -0,0 +1,20 @@ +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"; + + 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..7526b38d --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/WebCitationDetailsIF.java @@ -0,0 +1,30 @@ +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 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; + } + + String getDisplayName(); + + String getTitle(); + + String 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..1a2fb66b --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowInputParameterIF.java @@ -0,0 +1,25 @@ +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 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 +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@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..08f35985 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/objects/WorkflowObjectIF.java @@ -0,0 +1,22 @@ +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 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 +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@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 47043716..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,17 +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; @@ -36,6 +50,19 @@ 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 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 @@ -134,6 +161,188 @@ 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 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 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 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 2086bb55..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 @@ -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,64 @@ 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 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 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); + } + + // 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 + .mapper() + .readValue( + JsonLoader.loadJsonFromFile("interactive_elements_batch.json"), + BlockElement[].class + ); + assertThat(elements[0]).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/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/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/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/interactive_elements_batch.json b/slack-base/src/test/resources/interactive_elements_batch.json new file mode 100644 index 00000000..03c8a4d6 --- /dev/null +++ b/slack-base/src/test/resources/interactive_elements_batch.json @@ -0,0 +1,20 @@ +[ + { + "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/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/minimal_elements.json b/slack-base/src/test/resources/minimal_elements.json new file mode 100644 index 00000000..3de08899 --- /dev/null +++ b/slack-base/src/test/resources/minimal_elements.json @@ -0,0 +1,172 @@ +[ + { + "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", + "descriptor": "notes.txt", + "file_id": "F123ABC456" + } + } + ] + } + ] + }, + { + "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-base/src/test/resources/rich_text_blocks.json b/slack-base/src/test/resources/rich_text_blocks.json index da26cfe0..59daaded 100644 --- a/slack-base/src/test/resources/rich_text_blocks.json +++ b/slack-base/src/test/resources/rich_text_blocks.json @@ -180,6 +180,57 @@ } ] }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "unsupported_rich_text_element", + "some_field": "some_value" + } + ] + } + ] + }, + { + "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 + } + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "team", + "team_id": "T123ABC456", + "style": { + "bold": true + } + } + ] + } + ] + }, { "type": "rich_text", "elements": [ @@ -188,7 +239,155 @@ "elements": [ { "type": "message_mention", - "message_id": "1234567890.123456" + "channel_id": "C123ABC456", + "message_ts": "1234567890.123456", + "author_id": "U123ABC456", + "text": "a message" + } + ] + } + ] + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "file", + "file_id": "F123ABC456", + "text": "the file", + "style": { "bold": true } + } + ] + } + ] + }, + { + "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 + } + } + ] + } + ] + } +] 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 + } + } + ] + } + ] + } +] diff --git a/slack-java-client/pom.xml b/slack-java-client/pom.xml index 9efb64c4..83f89a33 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..c68c0d14 --- /dev/null +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadFromFilesTest.java @@ -0,0 +1,93 @@ +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); + + // 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(); + } + } + + 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..d2fd16cf --- /dev/null +++ b/slack-java-client/src/test/java/com/hubspot/slack/client/blockkit/BlockKitPayloadTester.java @@ -0,0 +1,477 @@ +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) { + 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( + displayName, + "Could not read file: " + describe(e) + ); + } + + List blocks; + try { + blocks = blocksFromJson(json); + } catch (RuntimeException e) { + return PayloadSendResult.failure( + displayName, + "Could not parse/deserialize payload: " + describe(e) + ); + } + + try { + Result result = postBlocks( + blocks, + "Block Kit payload test: " + displayName + ); + if (result.isOk()) { + return PayloadSendResult.success(displayName, result.unwrapOrElseThrow().getTs()); + } + return PayloadSendResult.failure( + displayName, + reasonFor(result.unwrapErrOrElseThrow()) + ); + } catch (RuntimeException e) { + return PayloadSendResult.failure(displayName, "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 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.walk(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 walk folder " + folder, e); + } + + List results = new ArrayList<>(); + for (Path file : files) { + results.add(postFile(file, folder.relativize(file).toString())); + } + 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