diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/AlertBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/AlertBlock.java
new file mode 100644
index 000000000..80003adb3
--- /dev/null
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/AlertBlock.java
@@ -0,0 +1,41 @@
+package com.slack.api.model.block;
+
+import com.slack.api.model.block.composition.TextObject;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * https://docs.slack.dev/reference/block-kit/blocks/alert-block
+ *
+ * Displays an inline alert message. Alert blocks are currently only supported in modals.
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AlertBlock implements LayoutBlock {
+ public static final String TYPE = "alert";
+ private final String type = TYPE;
+
+ /**
+ * The message content of the alert, as a plain_text or mrkdwn text object.
+ * Maximum length for the text in this field is 200 characters.
+ */
+ private TextObject text;
+
+ /**
+ * The severity level of the alert. One of default, info, warning, error, or success.
+ * Will be default if omitted.
+ */
+ private String level;
+
+ /**
+ * A string acting as a unique identifier for a block. If not specified, one will be generated.
+ * Maximum length for this field is 255 characters.
+ * block_id should be unique for each message and each iteration of a message.
+ * If a message is updated, use a new block_id.
+ */
+ private String blockId;
+}
diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java
index fd451d8d2..9abe88458 100644
--- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java
@@ -129,4 +129,22 @@ public static ShareShortcutBlock shareShortcut() {
return ShareShortcutBlock.builder().build();
}
+ // AlertBlock
+
+ public static AlertBlock alert(ModelConfigurator configurator) {
+ return configurator.configure(AlertBlock.builder()).build();
+ }
+
+ // CardBlock
+
+ public static CardBlock card(ModelConfigurator configurator) {
+ return configurator.configure(CardBlock.builder()).build();
+ }
+
+ // CarouselBlock
+
+ public static CarouselBlock carousel(ModelConfigurator configurator) {
+ return configurator.configure(CarouselBlock.builder()).build();
+ }
+
}
diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java
new file mode 100644
index 000000000..b02c38811
--- /dev/null
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java
@@ -0,0 +1,75 @@
+package com.slack.api.model.block;
+
+import com.slack.api.model.block.composition.SlackIconObject;
+import com.slack.api.model.block.composition.TextObject;
+import com.slack.api.model.block.element.BlockElement;
+import com.slack.api.model.block.element.ImageElement;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * A card is a layout block used to display a compact, structured summary of content. It can be used on
+ * its own or grouped together inside a {@link CarouselBlock carousel}. At least one of {@code heroImage},
+ * {@code title}, {@code actions}, or {@code body} must be provided.
+ *
+ * https://docs.slack.dev/reference/block-kit/blocks/card-block
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CardBlock implements LayoutBlock {
+ public static final String TYPE = "card";
+ private final String type = TYPE;
+
+ /**
+ * A top banner image for the card in the form of an image element.
+ * The image URL may be up to 3000 characters and the alt text up to 2000 characters.
+ */
+ private ImageElement heroImage;
+
+ /**
+ * A small icon displayed next to the title and subtitle in the form of an image element.
+ * The image URL may be up to 3000 characters and the alt text up to 2000 characters.
+ * Mutually exclusive with {@code slackIcon}.
+ */
+ private ImageElement icon;
+
+ /**
+ * A built-in Slack icon displayed next to the title and subtitle.
+ * Mutually exclusive with {@code icon}.
+ */
+ private SlackIconObject slackIcon;
+
+ /**
+ * The title of the card. Maximum length for the text in this field is 150 characters.
+ */
+ private TextObject title;
+
+ /**
+ * The subtitle of the card. Maximum length for the text in this field is 150 characters.
+ */
+ private TextObject subtitle;
+
+ /**
+ * The body text of the card. Maximum length for the text in this field is 200 characters.
+ */
+ private TextObject body;
+
+ /**
+ * Secondary text displayed beneath the body of the card. Maximum length for the text in this field
+ * is 200 characters.
+ */
+ private TextObject subtext;
+
+ /**
+ * Interactive elements (such as buttons) displayed at the bottom of the card. Up to 3 buttons.
+ */
+ private List actions;
+
+ private String blockId;
+}
diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java
new file mode 100644
index 000000000..23c0462bf
--- /dev/null
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java
@@ -0,0 +1,30 @@
+package com.slack.api.model.block;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * A carousel is a layout block used to display a horizontally scrollable collection of
+ * {@link CardBlock cards}. It must contain between 1 and 10 cards.
+ *
+ * https://docs.slack.dev/reference/block-kit/blocks/carousel-block
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CarouselBlock implements LayoutBlock {
+ public static final String TYPE = "carousel";
+ private final String type = TYPE;
+
+ /**
+ * An array of {@link CardBlock card} blocks. Must contain between 1 and 10 cards.
+ */
+ private List elements;
+
+ private String blockId;
+}
diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java
index c8cb8c218..f6413c5f0 100644
--- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java
@@ -87,4 +87,14 @@ public static FeedbackButtonObject feedbackButton(ModelConfigurator configurator) {
return configurator.configure(SlackFileObject.builder()).build();
}
+
+ // SlackIconObject
+
+ public static SlackIconObject slackIcon(ModelConfigurator configurator) {
+ return configurator.configure(SlackIconObject.builder()).build();
+ }
+
+ public static SlackIconObject slackIcon(String name) {
+ return SlackIconObject.builder().name(name).build();
+ }
}
diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/SlackIconObject.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/SlackIconObject.java
new file mode 100644
index 000000000..f7b15e21a
--- /dev/null
+++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/SlackIconObject.java
@@ -0,0 +1,25 @@
+package com.slack.api.model.block.composition;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Defines a built-in Slack icon that can be displayed next to the title and subtitle of a card block.
+ *
+ * @see document
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SlackIconObject {
+ public static final String TYPE = "icon";
+ private final String type = TYPE;
+
+ /**
+ * The name of the built-in Slack icon.
+ */
+ private String name;
+}
diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java
index 9286e6b51..d97b36d28 100644
--- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java
+++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java
@@ -61,6 +61,12 @@ private Class extends LayoutBlock> getLayoutClassInstance(String typeName) {
return RichTextBlock.class;
case ShareShortcutBlock.TYPE:
return ShareShortcutBlock.class;
+ case AlertBlock.TYPE:
+ return AlertBlock.class;
+ case CardBlock.TYPE:
+ return CardBlock.class;
+ case CarouselBlock.TYPE:
+ return CarouselBlock.class;
default:
if (failOnUnknownProperties) {
throw new JsonParseException("Unsupported layout block type: " + typeName);
diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java
index 1fc12d58d..f398c781f 100644
--- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java
+++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java
@@ -1742,6 +1742,63 @@ public void parseVideoBlocks() {
assertEquals("https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", block.getVideoUrl());
}
+ @Test
+ public void parseAlertBlocks() {
+ // https://docs.slack.dev/reference/block-kit/blocks/alert-block
+ String json = "{\n" +
+ " \"blocks\": [\n" +
+ " {\n" +
+ " \"type\": \"alert\",\n" +
+ " \"block_id\": \"alert-1\",\n" +
+ " \"text\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Something went *wrong*.\"\n" +
+ " },\n" +
+ " \"level\": \"error\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"alert\",\n" +
+ " \"text\": {\n" +
+ " \"type\": \"plain_text\",\n" +
+ " \"text\": \"A plain text alert with no markup.\"\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ View view = GsonFactory.createSnakeCase().fromJson(json, View.class);
+ assertNotNull(view);
+ assertEquals(2, view.getBlocks().size());
+
+ AlertBlock errorAlert = (AlertBlock) view.getBlocks().get(0);
+ assertEquals("alert", errorAlert.getType());
+ assertEquals("alert-1", errorAlert.getBlockId());
+ assertEquals("error", errorAlert.getLevel());
+ assertEquals("mrkdwn", errorAlert.getText().getType());
+ assertEquals("Something went *wrong*.", errorAlert.getText().getText());
+
+ AlertBlock defaultAlert = (AlertBlock) view.getBlocks().get(1);
+ assertEquals("plain_text", defaultAlert.getText().getType());
+ assertNull(defaultAlert.getLevel());
+ assertNull(defaultAlert.getBlockId());
+ }
+
+ @Test
+ public void buildAlertBlock() {
+ AlertBlock block = alert(a -> a
+ .blockId("alert-1")
+ .level("info")
+ .text(plainText("Heads up!")));
+ assertNotNull(block);
+ assertEquals("alert", block.getType());
+
+ Gson gson = GsonFactory.createSnakeCase();
+ String output = gson.toJson(block);
+ AlertBlock parsed = gson.fromJson(output, AlertBlock.class);
+ assertEquals("alert-1", parsed.getBlockId());
+ assertEquals("info", parsed.getLevel());
+ assertEquals("Heads up!", parsed.getText().getText());
+ }
+
@Test
public void parseLinkTriggerMessages() {
// https://tools.slack.dev/deno-slack-sdk/
@@ -1940,4 +1997,209 @@ public void parseRichTextElements() {
RichTextSectionElement section = (RichTextSectionElement) ((RichTextBlock) message.getBlocks().get(0)).getElements().get(0);
assertThat(section.getElements().size(), is(10));
}
+
+ @Test
+ public void parseCardBlock() {
+ // https://docs.slack.dev/reference/block-kit/blocks/card-block
+ String json = "{\"blocks\":[\n" +
+ " {\n" +
+ " \"type\": \"card\",\n" +
+ " \"block_id\": \"card1\",\n" +
+ " \"icon\": {\n" +
+ " \"type\": \"image\",\n" +
+ " \"image_url\": \"https://picsum.photos/36/36\",\n" +
+ " \"alt_text\": \"Icon\"\n" +
+ " },\n" +
+ " \"hero_image\": {\n" +
+ " \"type\": \"image\",\n" +
+ " \"image_url\": \"https://picsum.photos/400/300\",\n" +
+ " \"alt_text\": \"Sample hero image\"\n" +
+ " },\n" +
+ " \"title\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Lumon Industries\"\n" +
+ " },\n" +
+ " \"subtitle\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Committed to work-life balance\"\n" +
+ " },\n" +
+ " \"body\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Please enjoy each card equally.\"\n" +
+ " },\n" +
+ " \"actions\": [\n" +
+ " {\n" +
+ " \"type\": \"button\",\n" +
+ " \"text\": {\n" +
+ " \"type\": \"plain_text\",\n" +
+ " \"text\": \"Action Button\"\n" +
+ " },\n" +
+ " \"action_id\": \"button_action\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "]}";
+ Gson gson = GsonFactory.createSnakeCase();
+ Message message = gson.fromJson(json, Message.class);
+ assertThat(message, is(notNullValue()));
+ assertThat(message.getBlocks().size(), is(1));
+
+ CardBlock card = (CardBlock) message.getBlocks().get(0);
+ assertThat(card.getType(), is("card"));
+ assertThat(card.getBlockId(), is("card1"));
+ assertThat(card.getIcon().getImageUrl(), is("https://picsum.photos/36/36"));
+ assertThat(card.getIcon().getAltText(), is("Icon"));
+ assertThat(card.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300"));
+ assertThat(card.getHeroImage().getAltText(), is("Sample hero image"));
+ assertThat(card.getTitle().getType(), is("mrkdwn"));
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getTitle()).getText(), is("Lumon Industries"));
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getSubtitle()).getText(), is("Committed to work-life balance"));
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getBody()).getText(), is("Please enjoy each card equally."));
+ assertThat(card.getActions().size(), is(1));
+ ButtonElement button = (ButtonElement) card.getActions().get(0);
+ assertThat(button.getActionId(), is("button_action"));
+ assertThat(button.getText().getText(), is("Action Button"));
+
+ // Round-trip back to JSON and ensure the card survives serialization.
+ Message roundTrip = gson.fromJson(gson.toJson(message), Message.class);
+ CardBlock reparsed = (CardBlock) roundTrip.getBlocks().get(0);
+ assertThat(reparsed.getBlockId(), is("card1"));
+ assertThat(reparsed.getActions().size(), is(1));
+ assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action"));
+ }
+
+ @Test
+ public void buildCardBlock() {
+ CardBlock card = card(c -> c
+ .blockId("card2")
+ .title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Title").build())
+ .body(com.slack.api.model.block.composition.PlainTextObject.builder().text("Body").build())
+ .actions(Arrays.asList(
+ ButtonElement.builder()
+ .actionId("a")
+ .text(plainText("Click"))
+ .build()
+ ))
+ );
+ assertThat(card.getType(), is("card"));
+ assertThat(card.getBlockId(), is("card2"));
+ Gson gson = GsonFactory.createSnakeCase();
+ Message message = new Message();
+ message.setBlocks(asBlocks(card));
+ Message reparsed = gson.fromJson(gson.toJson(message), Message.class);
+ CardBlock parsedCard = (CardBlock) reparsed.getBlocks().get(0);
+ assertThat(parsedCard.getBlockId(), is("card2"));
+ assertThat(parsedCard.getActions().size(), is(1));
+ }
+
+ @Test
+ public void parseCarouselBlock() {
+ // https://docs.slack.dev/reference/block-kit/blocks/carousel-block
+ String json = "{\"blocks\":[\n" +
+ " {\n" +
+ " \"type\": \"carousel\",\n" +
+ " \"block_id\": \"carousel1\",\n" +
+ " \"elements\": [\n" +
+ " {\n" +
+ " \"type\": \"card\",\n" +
+ " \"block_id\": \"card1\",\n" +
+ " \"title\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"First card\"\n" +
+ " },\n" +
+ " \"body\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Body of the first card.\"\n" +
+ " },\n" +
+ " \"hero_image\": {\n" +
+ " \"type\": \"image\",\n" +
+ " \"image_url\": \"https://picsum.photos/400/300\",\n" +
+ " \"alt_text\": \"hero\"\n" +
+ " },\n" +
+ " \"actions\": [\n" +
+ " {\n" +
+ " \"type\": \"button\",\n" +
+ " \"text\": {\n" +
+ " \"type\": \"plain_text\",\n" +
+ " \"text\": \"Open\"\n" +
+ " },\n" +
+ " \"action_id\": \"open\"\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"card\",\n" +
+ " \"title\": {\n" +
+ " \"type\": \"mrkdwn\",\n" +
+ " \"text\": \"Second card\"\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "]}";
+ Gson gson = GsonFactory.createSnakeCase();
+ Message message = gson.fromJson(json, Message.class);
+ assertThat(message, is(notNullValue()));
+ assertThat(message.getBlocks().size(), is(1));
+
+ CarouselBlock carousel = (CarouselBlock) message.getBlocks().get(0);
+ assertThat(carousel.getType(), is("carousel"));
+ assertThat(carousel.getBlockId(), is("carousel1"));
+ assertThat(carousel.getElements().size(), is(2));
+
+ CardBlock firstCard = carousel.getElements().get(0);
+ assertThat(firstCard.getType(), is("card"));
+ assertThat(firstCard.getBlockId(), is("card1"));
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) firstCard.getTitle()).getText(), is("First card"));
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) firstCard.getBody()).getText(), is("Body of the first card."));
+ assertThat(firstCard.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300"));
+ assertThat(firstCard.getActions().size(), is(1));
+ ButtonElement button = (ButtonElement) firstCard.getActions().get(0);
+ assertThat(button.getActionId(), is("open"));
+
+ CardBlock secondCard = carousel.getElements().get(1);
+ assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) secondCard.getTitle()).getText(), is("Second card"));
+ assertThat(secondCard.getBlockId(), is(nullValue()));
+
+ // Round-trip back to JSON and ensure the carousel survives serialization.
+ Message roundTrip = gson.fromJson(gson.toJson(message), Message.class);
+ CarouselBlock reparsed = (CarouselBlock) roundTrip.getBlocks().get(0);
+ assertThat(reparsed.getBlockId(), is("carousel1"));
+ assertThat(reparsed.getElements().size(), is(2));
+ assertThat(reparsed.getElements().get(0).getType(), is("card"));
+ }
+
+ @Test
+ public void buildCarouselBlock() {
+ CarouselBlock carousel = carousel(c -> c
+ .blockId("carousel1")
+ .elements(Arrays.asList(
+ card(card -> card
+ .blockId("card1")
+ .title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Promo").build())
+ .body(com.slack.api.model.block.composition.PlainTextObject.builder().text("Check this out").build())
+ .actions(Arrays.asList(
+ ButtonElement.builder()
+ .actionId("open")
+ .text(plainText("Open"))
+ .build()
+ ))),
+ card(card -> card
+ .title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Second").build()))
+ ))
+ );
+ assertThat(carousel.getType(), is("carousel"));
+ assertThat(carousel.getElements().size(), is(2));
+
+ Gson gson = GsonFactory.createSnakeCase();
+ Message message = new Message();
+ message.setBlocks(asBlocks(carousel));
+ Message reparsed = gson.fromJson(gson.toJson(message), Message.class);
+ CarouselBlock parsedCarousel = (CarouselBlock) reparsed.getBlocks().get(0);
+ assertThat(parsedCarousel.getBlockId(), is("carousel1"));
+ assertThat(parsedCarousel.getElements().size(), is(2));
+ assertThat(parsedCarousel.getElements().get(0).getType(), is("card"));
+ assertThat(parsedCarousel.getElements().get(0).getBlockId(), is("card1"));
+ assertThat(parsedCarousel.getElements().get(0).getActions().size(), is(1));
+ }
}
diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java
index 12073ab6d..332ee593b 100644
--- a/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java
+++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java
@@ -100,6 +100,23 @@ public void testContextActions() {
), is(notNullValue()));
}
+ @Test
+ public void testAlert() {
+ assertThat(alert(a -> a.blockId("alert-1").level("warning").text(plainText("Watch out!"))), is(notNullValue()));
+ }
+
+ @Test
+ public void testCard() {
+ assertThat(card(c -> c.blockId("card-1").title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Title").build())), is(notNullValue()));
+ }
+
+ @Test
+ public void testCarousel() {
+ assertThat(carousel(c -> c.blockId("carousel-1").elements(Arrays.asList(
+ card(card -> card.title(com.slack.api.model.block.composition.MarkdownTextObject.builder().text("Card 1").build()))
+ ))), is(notNullValue()));
+ }
+
@Test
public void testImage() {
assertThat(Blocks.image(i -> i.blockId("block-id").imageUrl("https://www.example.com/")), is(notNullValue()));