From 6766122ab20b115062f11fbfb13c3bfea00fc00d Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 11:12:34 +0100 Subject: [PATCH 01/37] Redesign the usage of SlackClient with Builder --- pom.xml | 14 ++++- readme.md | 4 +- .../n1netails/slack/api/SlackClient.java | 14 ++++- .../n1netails/slack/api/SlackClientImpl.java | 51 +++++++++++++++++++ .../slack/internal/SlackClientImpl.java | 30 ----------- 5 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java delete mode 100644 src/main/java/com/n1netails/n1netails/slack/internal/SlackClientImpl.java diff --git a/pom.xml b/pom.xml index cd876e1..25663ed 100644 --- a/pom.xml +++ b/pom.xml @@ -16,8 +16,20 @@ scm:git:git://github.com/n1netails/n1netails-slack-client.git scm:git:ssh://git@github.com:n1netails/n1netails-slack-client.git + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + - + MIT License https://opensource.org/licenses/MIT diff --git a/readme.md b/readme.md index ded405a..827fc04 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ Here's how to use the client to send a message: ```java import com.n1netails.n1netails.slack.api.SlackClient; -import com.n1netails.n1netails.slack.internal.SlackClientImpl; +import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.model.SlackMessage; import com.n1netails.n1netails.slack.service.BotService; @@ -90,7 +90,7 @@ You can also send more complex messages using [Slack's Block Kit](https://api.sl ```java import com.n1netails.n1netails.slack.api.SlackClient; -import com.n1netails.n1netails.slack.internal.SlackClientImpl; +import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.model.SlackMessage; import com.n1netails.n1netails.slack.service.BotService; import com.slack.api.model.block.Blocks; diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java index 527e8da..d8c77ed 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java @@ -5,13 +5,25 @@ /** * Slick Client + * * @author shahid foy */ -public interface SlackClient { +public sealed interface SlackClient permits SlackClientImpl { /** * Send slack message + * * @param slackMessage slack message */ void sendMessage(SlackMessage slackMessage) throws SlackClientException; + + static Builder builder() { + return new SlackClientImpl.Builder(); + } + + sealed interface Builder permits SlackClientImpl.Builder { + Builder token(String token); + + SlackClient build() throws SlackClientException; + } } diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java new file mode 100644 index 0000000..8f9aae7 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java @@ -0,0 +1,51 @@ +package com.n1netails.n1netails.slack.api; + +import com.n1netails.n1netails.slack.exception.SlackClientException; +import com.n1netails.n1netails.slack.model.SlackMessage; +import com.n1netails.n1netails.slack.service.BotService; + +/** + * Slack Client Implementation + * + * @author shahid foy + */ +final class SlackClientImpl implements SlackClient { + + private final BotService botService; + + public SlackClientImpl(Builder builder) { + this.botService = new BotService(builder.token); + } + + /** + * Send slack notification + * + * @param slackMessage slack message + */ + @Override + public void sendMessage(SlackMessage slackMessage) throws SlackClientException { + try { + botService.send(slackMessage); + } catch (Exception e) { + throw new SlackClientException("Failed to send Slack message", e); + } + } + + public static final class Builder implements SlackClient.Builder { + private String token; + + @Override + public SlackClient.Builder token(String token) { + this.token = token; + return this; + } + + @Override + public SlackClient build() throws SlackClientException { + if (this.token == null || this.token.isBlank()) + throw new SlackClientException("token must be provided"); + + return new SlackClientImpl(this); + } + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/internal/SlackClientImpl.java b/src/main/java/com/n1netails/n1netails/slack/internal/SlackClientImpl.java deleted file mode 100644 index 3550acb..0000000 --- a/src/main/java/com/n1netails/n1netails/slack/internal/SlackClientImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.n1netails.n1netails.slack.internal; - -import com.n1netails.n1netails.slack.api.SlackClient; -import com.n1netails.n1netails.slack.exception.SlackClientException; -import com.n1netails.n1netails.slack.model.SlackMessage; -import com.n1netails.n1netails.slack.service.BotService; - -/** - * Slack Client Implementation - * @author shahid foy - */ -public class SlackClientImpl implements SlackClient { - - private final BotService botService; - - public SlackClientImpl(BotService botService) { this.botService = botService; } - - /** - * Send slack notification - * @param slackMessage slack message - */ - @Override - public void sendMessage(SlackMessage slackMessage) throws SlackClientException { - try { - botService.send(slackMessage); - } catch (Exception e) { - throw new SlackClientException("Failed to send Slack message", e); - } - } -} From 63937a54af1685639bbc2803ed25e7a4668a56c1 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 11:27:54 +0100 Subject: [PATCH 02/37] Update readme.md --- readme.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 827fc04..e112c44 100644 --- a/readme.md +++ b/readme.md @@ -53,7 +53,7 @@ Here's how to use the client to send a message: import com.n1netails.n1netails.slack.api.SlackClient; import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.model.SlackMessage; -import com.n1netails.n1netails.slack.service.BotService; +import com.n1netails.n1netails.slack.api.BotService; public class Example { public static void main(String[] args) { @@ -63,11 +63,8 @@ public class Example { // The channel you want to send the message to (e.g., "#general") String channel = "#prototype"; // or "#channel-name" - // Create the bot service - BotService botService = new BotService(token); - - // Create the Slack client - SlackClient slackClient = new SlackClientImpl(botService); + // Use builder to create a client + SlackClient client = SlackClient.builder().token(token).build(); // Create the message SlackMessage message = new SlackMessage(); @@ -92,7 +89,7 @@ You can also send more complex messages using [Slack's Block Kit](https://api.sl import com.n1netails.n1netails.slack.api.SlackClient; import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.model.SlackMessage; -import com.n1netails.n1netails.slack.service.BotService; +import com.n1netails.n1netails.slack.api.BotService; import com.slack.api.model.block.Blocks; import com.slack.api.model.block.composition.BlockCompositions; @@ -103,8 +100,7 @@ public class AdvancedExample { String token = "xoxb-your-bot-token"; String channel = "#prototype"; - BotService botService = new BotService(token); - SlackClient slackClient = new SlackClientImpl(botService); + SlackClient client = SlackClient.builder().token(token).build(); SlackMessage message = new SlackMessage(); message.setChannel(channel); From 6d80c6f3fd0f13eedb12ccda7c1e279115ab39df Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 11:28:48 +0100 Subject: [PATCH 03/37] Encapsulation: only SlackClient is available for creation --- .../n1netails/slack/{service => api}/BotService.java | 6 ++++-- .../java/com/n1netails/n1netails/slack/api/SlackClient.java | 1 + .../com/n1netails/n1netails/slack/api/SlackClientImpl.java | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) rename src/main/java/com/n1netails/n1netails/slack/{service => api}/BotService.java (94%) diff --git a/src/main/java/com/n1netails/n1netails/slack/service/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java similarity index 94% rename from src/main/java/com/n1netails/n1netails/slack/service/BotService.java rename to src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 4d3ae8f..6696c58 100644 --- a/src/main/java/com/n1netails/n1netails/slack/service/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -1,4 +1,4 @@ -package com.n1netails.n1netails.slack.service; +package com.n1netails.n1netails.slack.api; import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.model.SlackMessage; @@ -8,14 +8,16 @@ /** * Slack Bot Service + * * @author shahid foy */ -public class BotService { +class BotService { private final String token; /** * Bot Service Constructor + * * @param token slack bot token */ public BotService(String token) { diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java index d8c77ed..4cb0bd9 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClient.java @@ -1,5 +1,6 @@ package com.n1netails.n1netails.slack.api; +import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.model.SlackMessage; diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java index 8f9aae7..423d8b7 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java @@ -2,7 +2,6 @@ import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.model.SlackMessage; -import com.n1netails.n1netails.slack.service.BotService; /** * Slack Client Implementation From d5ed27b462c3b4d420c53ddd365df78f525a4b4f Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 11:46:17 +0100 Subject: [PATCH 04/37] Prevents recreating client every call --- .../java/com/n1netails/n1netails/slack/api/BotService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 6696c58..2d39cab 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -12,8 +12,7 @@ * @author shahid foy */ class BotService { - - private final String token; + private final MethodsClient methods; /** * Bot Service Constructor @@ -21,12 +20,11 @@ class BotService { * @param token slack bot token */ public BotService(String token) { - this.token = token; + this.methods = Slack.getInstance().methods(token); } public void send(SlackMessage slackMessage) throws SlackClientException { try { - MethodsClient methods = Slack.getInstance().methods(token); ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = ChatPostMessageRequest.builder() .channel(slackMessage.getChannel()) .text(slackMessage.getText()); From 53db1c853fea7198ea9dbc1341ed036240cde9e8 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 11:57:04 +0100 Subject: [PATCH 05/37] Error handling --- .../n1netails/slack/api/BotService.java | 33 +++++++++++++++---- .../n1netails/slack/api/SlackClientImpl.java | 6 +--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 2d39cab..8da2f79 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -4,7 +4,11 @@ import com.n1netails.n1netails.slack.model.SlackMessage; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; + +import java.io.IOException; /** * Slack Bot Service @@ -24,18 +28,35 @@ public BotService(String token) { } public void send(SlackMessage slackMessage) throws SlackClientException { + validateSlackMessage(slackMessage); try { - ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = ChatPostMessageRequest.builder() - .channel(slackMessage.getChannel()) - .text(slackMessage.getText()); + ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = + ChatPostMessageRequest.builder() + .channel(slackMessage.getChannel()) + .text(slackMessage.getText()); if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { requestBuilder.blocks(slackMessage.getBlocks()); } - methods.chatPostMessage(requestBuilder.build()); - } catch (Exception e) { - throw new SlackClientException("Failed to send Slack message", e); + ChatPostMessageResponse response = methods.chatPostMessage(requestBuilder.build()); + + if (!response.isOk()) { + throw new SlackClientException("Slack API error: " + response.getError()); + } + + } catch (IOException | SlackApiException e) { + throw new SlackClientException("Failed to send Slack message to channel: " + slackMessage.getChannel(), e); + } + } + + private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientException { + if (slackMessage == null) { + throw new SlackClientException("slackMessage cannot be null"); + } + + if (slackMessage.getChannel() == null || slackMessage.getChannel().isBlank()) { + throw new SlackClientException("Channel must be provided"); } } } diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java index 423d8b7..1e9bebd 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java @@ -23,11 +23,7 @@ public SlackClientImpl(Builder builder) { */ @Override public void sendMessage(SlackMessage slackMessage) throws SlackClientException { - try { - botService.send(slackMessage); - } catch (Exception e) { - throw new SlackClientException("Failed to send Slack message", e); - } + botService.send(slackMessage); } public static final class Builder implements SlackClient.Builder { From 1c4eed41ee9c56808be910ef8521bbf7ea7abe3e Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 13:23:01 +0100 Subject: [PATCH 06/37] Support pics and gifs --- .../n1netails/slack/api/BotService.java | 21 +++++-- .../n1netails/slack/model/SlackBlock.java | 9 +++ .../n1netails/slack/model/SlackMessage.java | 56 +++++++++++++++++-- .../slack/model/blocks/GifBlock.java | 26 +++++++++ .../slack/model/blocks/ImageBlock.java | 29 ++++++++++ 5 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/SlackBlock.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 8da2f79..e1d4aec 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -1,14 +1,19 @@ package com.n1netails.n1netails.slack.api; import com.n1netails.n1netails.slack.exception.SlackClientException; +import com.n1netails.n1netails.slack.model.SlackBlock; import com.n1netails.n1netails.slack.model.SlackMessage; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.model.block.LayoutBlock; +import lombok.Getter; +import lombok.Setter; import java.io.IOException; +import java.util.List; /** * Slack Bot Service @@ -34,9 +39,18 @@ public void send(SlackMessage slackMessage) throws SlackClientException { ChatPostMessageRequest.builder() .channel(slackMessage.getChannel()) .text(slackMessage.getText()); + List finalBlocks = null; + if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) { + finalBlocks = slackMessage.getRawBlocks(); - if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { - requestBuilder.blocks(slackMessage.getBlocks()); + } else if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { + finalBlocks = slackMessage.getBlocks().stream() + .map(SlackBlock::toLayoutBlock) + .toList(); + } + + if (finalBlocks != null && !finalBlocks.isEmpty()) { + requestBuilder.blocks(finalBlocks); } ChatPostMessageResponse response = methods.chatPostMessage(requestBuilder.build()); @@ -55,8 +69,5 @@ private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientE throw new SlackClientException("slackMessage cannot be null"); } - if (slackMessage.getChannel() == null || slackMessage.getChannel().isBlank()) { - throw new SlackClientException("Channel must be provided"); - } } } diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackBlock.java new file mode 100644 index 0000000..ced5e45 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackBlock.java @@ -0,0 +1,9 @@ +package com.n1netails.n1netails.slack.model; + +import com.slack.api.model.block.LayoutBlock; + +import java.util.List; + +public interface SlackBlock { + LayoutBlock toLayoutBlock(); +} diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 007f8f7..ecddfeb 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; +import java.util.ArrayList; import java.util.List; /** @@ -11,15 +12,58 @@ * @author shahid foy */ @Getter -@Setter public class SlackMessage { private String channel; private String text; - private List blocks; + private List blocks; + private List rawBlocks; - /** - * Slack Message Constructor - */ - public SlackMessage() {} + private SlackMessage(Builder builder) { + this.channel = builder.channel; + this.text = builder.text; + this.blocks = List.copyOf(builder.blocks); + this.rawBlocks = List.copyOf(builder.rawBlocks); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String channel; + private String text; + private final List blocks = new ArrayList<>(); + private final List rawBlocks = new ArrayList<>(); + + public Builder channel(String channel) { + this.channel = channel; + return this; + } + + public Builder text(String text) { + this.text = text; + return this; + } + + public Builder addBlock(SlackBlock block) { + this.blocks.add(block); + return this; + } + + public Builder addRawBlock(LayoutBlock block) { + this.rawBlocks.add(block); + return this; + } + + public SlackMessage build() { + if (channel == null || channel.isBlank()) { + throw new IllegalStateException("channel is required"); + } + if ((text == null || text.isBlank()) && blocks.isEmpty() && rawBlocks.isEmpty()) { + throw new IllegalStateException("Either text, blocks, or rawBlocks must be provided"); + } + return new SlackMessage(this); + } + } } diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java new file mode 100644 index 0000000..ba55585 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java @@ -0,0 +1,26 @@ +package com.n1netails.n1netails.slack.model.blocks; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.PlainTextObject; + +public class GifBlock implements SlackBlock { + private final String imageUrl; + private final String altText; + + public GifBlock(String imageUrl, String altText) { + this.imageUrl = imageUrl; + this.altText = altText; + } + + + @Override + public LayoutBlock toLayoutBlock() { + return + com.slack.api.model.block.ImageBlock.builder() + .altText(altText) + .imageUrl(imageUrl) + .build() + ; + } +} \ No newline at end of file diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java new file mode 100644 index 0000000..1a4141c --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java @@ -0,0 +1,29 @@ +package com.n1netails.n1netails.slack.model.blocks; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.slack.api.model.block.Blocks; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.BlockCompositions; + +import java.util.List; + +public class ImageBlock implements SlackBlock { + private final String imageUrl; + private final String altText; + + public ImageBlock(String imageUrl, String altText) { + this.imageUrl = imageUrl; + this.altText = altText; + } + + + @Override + public LayoutBlock toLayoutBlock() { + return + com.slack.api.model.block.ImageBlock.builder() + .imageUrl(imageUrl) + .altText(altText) + .build() + ; + } +} From 51c6ba2062ec64750d3e91efdd49ad86aafe9063 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 13:42:55 +0100 Subject: [PATCH 07/37] Support text --- .../slack/model/blocks/TextBlock.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java new file mode 100644 index 0000000..2109046 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java @@ -0,0 +1,20 @@ +package com.n1netails.n1netails.slack.model.blocks; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.PlainTextObject; + +public class TextBlock implements SlackBlock { + private final String text; + + public TextBlock(String text) { + this.text = text; + } + + @Override + public LayoutBlock toLayoutBlock() { + return com.slack.api.model.block.SectionBlock.builder() + .text(new PlainTextObject(text, false)) + .build(); + } +} From a271562f11df23f1484ed7ba162342edcf1f51e1 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 13:45:21 +0100 Subject: [PATCH 08/37] prevent usage of blocks and rawBlocks together --- .../n1netails/n1netails/slack/api/BotService.java | 5 +++-- .../n1netails/slack/model/SlackMessage.java | 14 +++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index e1d4aec..caa22e6 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -39,7 +39,7 @@ public void send(SlackMessage slackMessage) throws SlackClientException { ChatPostMessageRequest.builder() .channel(slackMessage.getChannel()) .text(slackMessage.getText()); - List finalBlocks = null; + List finalBlocks; if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) { finalBlocks = slackMessage.getRawBlocks(); @@ -47,7 +47,8 @@ public void send(SlackMessage slackMessage) throws SlackClientException { finalBlocks = slackMessage.getBlocks().stream() .map(SlackBlock::toLayoutBlock) .toList(); - } + } else + finalBlocks = null; if (finalBlocks != null && !finalBlocks.isEmpty()) { requestBuilder.blocks(finalBlocks); diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index ecddfeb..05d82a0 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -9,6 +9,7 @@ /** * Slack Message + * * @author shahid foy */ @Getter @@ -60,10 +61,21 @@ public SlackMessage build() { if (channel == null || channel.isBlank()) { throw new IllegalStateException("channel is required"); } - if ((text == null || text.isBlank()) && blocks.isEmpty() && rawBlocks.isEmpty()) { + + boolean hasTextOrBlocks = (text != null && !text.isBlank()) + || (!blocks.isEmpty()) + || (!rawBlocks.isEmpty()); + + if (!hasTextOrBlocks) { throw new IllegalStateException("Either text, blocks, or rawBlocks must be provided"); } + + if (!blocks.isEmpty() && !rawBlocks.isEmpty()) { + throw new IllegalStateException("Cannot mix SlackBlock and rawBlocks in the same message"); + } + return new SlackMessage(this); } + } } From f545153289e21af2ad5da24454f465bdfbafc416 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 13:45:39 +0100 Subject: [PATCH 09/37] update readme.md --- readme.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index e112c44..255d1c1 100644 --- a/readme.md +++ b/readme.md @@ -47,13 +47,15 @@ implementation 'com.n1netails:n1netails-slack-client:0.2.0' ``` ## Usage -Here's how to use the client to send a message: +Here's how to use the client to send a message, pics or gifs: ```java import com.n1netails.n1netails.slack.api.SlackClient; import com.n1netails.n1netails.slack.api.SlackClientImpl; import com.n1netails.n1netails.slack.model.SlackMessage; import com.n1netails.n1netails.slack.api.BotService; +import com.n1netails.n1netails.slack.model.blocks.GifBlock; +import com.n1netails.n1netails.slack.model.blocks.TextBlock; public class Example { public static void main(String[] args) { @@ -67,9 +69,13 @@ public class Example { SlackClient client = SlackClient.builder().token(token).build(); // Create the message - SlackMessage message = new SlackMessage(); - message.setChannel(channel); - message.setText("Hello from the N1ne Tails Slack Client!"); + SlackMessage message = SlackMessage.builder() + .channel("test") + .text("New content 🚀") //Remember if use ANY blocks this text becomes a fallback message for notifications + .addBlock(new TextBlock("New content 🚀")) + .addBlock(new ImageBlock("YOUR IMG LINK", "YOUR ALT TEXT")) + .addBlock(new GifBlock("YOUR GIF LINK", "YOUR ALT TEXT")) + .build(); try { // Send the message From 330f1271caffe57d04a14fd82f8e8036b10f27e5 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 20:37:09 +0100 Subject: [PATCH 10/37] Exceptions handling --- .../n1netails/n1netails/slack/api/BotService.java | 9 ++++++--- .../n1netails/slack/api/SlackClientImpl.java | 2 +- .../slack/exception/SlackApiExceptionWrapper.java | 14 ++++++++++++++ .../slack/exception/SlackTransportException.java | 7 +++++++ .../slack/exception/SlackValidationException.java | 7 +++++++ 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/exception/SlackValidationException.java diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index caa22e6..31b6d4e 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -1,5 +1,6 @@ package com.n1netails.n1netails.slack.api; +import com.n1netails.n1netails.slack.exception.SlackApiExceptionWrapper; import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.model.SlackBlock; import com.n1netails.n1netails.slack.model.SlackMessage; @@ -57,11 +58,13 @@ public void send(SlackMessage slackMessage) throws SlackClientException { ChatPostMessageResponse response = methods.chatPostMessage(requestBuilder.build()); if (!response.isOk()) { - throw new SlackClientException("Slack API error: " + response.getError()); + throw new SlackApiExceptionWrapper(response.getError()); } - } catch (IOException | SlackApiException e) { - throw new SlackClientException("Failed to send Slack message to channel: " + slackMessage.getChannel(), e); + } catch (IOException e) { + throw new SlackClientException("Network error while calling Slack API", e); + } catch (SlackApiException e) { + throw new SlackClientException("Slack SDK failure" + slackMessage.getChannel(), e); } } diff --git a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java index 1e9bebd..6b96f3c 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/SlackClientImpl.java @@ -38,7 +38,7 @@ public SlackClient.Builder token(String token) { @Override public SlackClient build() throws SlackClientException { if (this.token == null || this.token.isBlank()) - throw new SlackClientException("token must be provided"); + throw new SlackClientException("Token must be provided"); return new SlackClientImpl(this); } diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java new file mode 100644 index 0000000..249a358 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java @@ -0,0 +1,14 @@ +package com.n1netails.n1netails.slack.exception; + +public class SlackApiExceptionWrapper extends SlackClientException { + private final String slackError; + + public SlackApiExceptionWrapper(String slackError) { + super("Slack API error: " + slackError); + this.slackError = slackError; + } + + public String getSlackError() { + return slackError; + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java new file mode 100644 index 0000000..5baaae9 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java @@ -0,0 +1,7 @@ +package com.n1netails.n1netails.slack.exception; + +public class SlackTransportException extends SlackClientException { + public SlackTransportException(String message) { + super(message); + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackValidationException.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackValidationException.java new file mode 100644 index 0000000..671c5a8 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackValidationException.java @@ -0,0 +1,7 @@ +package com.n1netails.n1netails.slack.exception; + +public class SlackValidationException extends SlackClientException { + public SlackValidationException(String message) { + super(message); + } +} From 4bcc3d1b86f8a78c7c3d88524245c4441ba5af61 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 21:16:49 +0100 Subject: [PATCH 11/37] Graceful fallback for unsupported or invalid media --- .../slack/fallback/SlackFallbackHandler.java | 26 ++++++++++ .../n1netails/slack/model/SlackMessage.java | 50 +++++++++++++++---- .../slack/model/blocks/FallbackBlock.java | 20 ++++++++ .../slack/model/blocks/GifBlock.java | 11 ++-- .../slack/model/blocks/ImageBlock.java | 3 +- .../slack/model/blocks/TextBlock.java | 2 + .../slack/validation/SlackValidator.java | 7 +++ .../slack/validation/SlackValidators.java | 34 +++++++++++++ .../validation/impl/GifBlockValidator.java | 18 +++++++ .../validation/impl/ImageBlockValidator.java | 18 +++++++ .../validation/impl/TextBlockValidator.java | 15 ++++++ 11 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/blocks/FallbackBlock.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/SlackValidator.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/impl/GifBlockValidator.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/impl/ImageBlockValidator.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java diff --git a/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java b/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java new file mode 100644 index 0000000..da8d9bf --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java @@ -0,0 +1,26 @@ +package com.n1netails.n1netails.slack.fallback; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.n1netails.n1netails.slack.model.blocks.FallbackBlock; +import com.n1netails.n1netails.slack.model.blocks.GifBlock; +import com.n1netails.n1netails.slack.model.blocks.ImageBlock; +import com.n1netails.n1netails.slack.model.blocks.TextBlock; + +public class SlackFallbackHandler { + public static SlackBlock handle(SlackBlock block, Exception e) { + + if (block instanceof ImageBlock img) { + return new FallbackBlock("Image could not be displayed: " + img.getAltText()); + } + + if (block instanceof GifBlock gif) { + return new FallbackBlock("GIF could not be displayed: " + gif.getAltText()); + } + + if (block instanceof TextBlock txt) { + return new FallbackBlock("Text could not be displayed"); + } + + return new FallbackBlock("Unsupported content"); + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 05d82a0..05c16b5 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -1,5 +1,8 @@ package com.n1netails.n1netails.slack.model; +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.fallback.SlackFallbackHandler; +import com.n1netails.n1netails.slack.validation.SlackValidators; import com.slack.api.model.block.LayoutBlock; import lombok.Getter; import lombok.Setter; @@ -47,32 +50,59 @@ public Builder text(String text) { return this; } - public Builder addBlock(SlackBlock block) { + public Builder addBlock(SlackBlock block) throws SlackValidationException { + if (!rawBlocks.isEmpty()) { + throw new SlackValidationException( + "Cannot add SlackBlock when rawBlocks are already present" + ); + } this.blocks.add(block); return this; } - public Builder addRawBlock(LayoutBlock block) { + public Builder addRawBlock(LayoutBlock block) throws SlackValidationException { + if (!blocks.isEmpty()) { + throw new SlackValidationException( + "Cannot add rawBlock when SlackBlocks are already present" + ); + } this.rawBlocks.add(block); return this; } - public SlackMessage build() { + public SlackMessage build() throws SlackValidationException { if (channel == null || channel.isBlank()) { - throw new IllegalStateException("channel is required"); + throw new SlackValidationException("channel is required"); } - boolean hasTextOrBlocks = (text != null && !text.isBlank()) - || (!blocks.isEmpty()) - || (!rawBlocks.isEmpty()); + boolean hasContent = (text != null && !text.isBlank()) + || !blocks.isEmpty() + || !rawBlocks.isEmpty(); - if (!hasTextOrBlocks) { - throw new IllegalStateException("Either text, blocks, or rawBlocks must be provided"); + if (!hasContent) { + throw new SlackValidationException( + "Either text, blocks, or rawBlocks must be provided" + ); } if (!blocks.isEmpty() && !rawBlocks.isEmpty()) { - throw new IllegalStateException("Cannot mix SlackBlock and rawBlocks in the same message"); + throw new SlackValidationException( + "Cannot mix SlackBlock and rawBlocks in the same message" + ); } + List processedBlocks = new ArrayList<>(); + + for (SlackBlock block : blocks) { + try { + SlackValidators.validate(block); + processedBlocks.add(block); + } catch (Exception e) { + processedBlocks.add(SlackFallbackHandler.handle(block, e)); + } + } + + this.blocks.clear(); + this.blocks.addAll(processedBlocks); return new SlackMessage(this); } diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/FallbackBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/FallbackBlock.java new file mode 100644 index 0000000..a483ba1 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/FallbackBlock.java @@ -0,0 +1,20 @@ +package com.n1netails.n1netails.slack.model.blocks; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.PlainTextObject; + +public class FallbackBlock implements SlackBlock { + + private final String text; + + public FallbackBlock(String text) { + this.text = text; + } + @Override + public LayoutBlock toLayoutBlock() { + return com.slack.api.model.block.SectionBlock.builder() + .text(new PlainTextObject(text, false)) + .build(); + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java index ba55585..15f7398 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/GifBlock.java @@ -2,14 +2,15 @@ import com.n1netails.n1netails.slack.model.SlackBlock; import com.slack.api.model.block.LayoutBlock; -import com.slack.api.model.block.composition.PlainTextObject; +import lombok.Getter; +@Getter public class GifBlock implements SlackBlock { - private final String imageUrl; + private final String gifUrl; private final String altText; - public GifBlock(String imageUrl, String altText) { - this.imageUrl = imageUrl; + public GifBlock(String gifUrl, String altText) { + this.gifUrl = gifUrl; this.altText = altText; } @@ -19,7 +20,7 @@ public LayoutBlock toLayoutBlock() { return com.slack.api.model.block.ImageBlock.builder() .altText(altText) - .imageUrl(imageUrl) + .imageUrl(gifUrl) .build() ; } diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java index 1a4141c..2c9cff1 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/ImageBlock.java @@ -4,9 +4,10 @@ import com.slack.api.model.block.Blocks; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.BlockCompositions; +import lombok.Getter; -import java.util.List; +@Getter public class ImageBlock implements SlackBlock { private final String imageUrl; private final String altText; diff --git a/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java index 2109046..95f3b94 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/blocks/TextBlock.java @@ -3,7 +3,9 @@ import com.n1netails.n1netails.slack.model.SlackBlock; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.PlainTextObject; +import lombok.Getter; +@Getter public class TextBlock implements SlackBlock { private final String text; diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidator.java new file mode 100644 index 0000000..d5b9091 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidator.java @@ -0,0 +1,7 @@ +package com.n1netails.n1netails.slack.validation; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; + +public interface SlackValidator { + void validate(T target) throws SlackValidationException; +} diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java b/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java new file mode 100644 index 0000000..d52fe30 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java @@ -0,0 +1,34 @@ +package com.n1netails.n1netails.slack.validation; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.blocks.GifBlock; +import com.n1netails.n1netails.slack.model.blocks.ImageBlock; +import com.n1netails.n1netails.slack.model.blocks.TextBlock; +import com.n1netails.n1netails.slack.validation.impl.GifBlockValidator; +import com.n1netails.n1netails.slack.validation.impl.ImageBlockValidator; +import com.n1netails.n1netails.slack.validation.impl.TextBlockValidator; + +import java.util.HashMap; +import java.util.Map; + +public class SlackValidators { + private static final Map, SlackValidator> VALIDATORS = new HashMap<>(); + + static { + VALIDATORS.put(ImageBlock.class, new ImageBlockValidator()); + VALIDATORS.put(GifBlock.class, new GifBlockValidator()); + VALIDATORS.put(TextBlock.class, new TextBlockValidator()); + } + + @SuppressWarnings("unchecked") + public static void validate(T target) throws SlackValidationException { + if (target == null) return; + + SlackValidator validator = + (SlackValidator) VALIDATORS.get(target.getClass()); + + if (validator != null) { + validator.validate(target); + } + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/GifBlockValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/GifBlockValidator.java new file mode 100644 index 0000000..18e9f5b --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/GifBlockValidator.java @@ -0,0 +1,18 @@ +package com.n1netails.n1netails.slack.validation.impl; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.blocks.GifBlock; +import com.n1netails.n1netails.slack.validation.SlackValidator; + +public class GifBlockValidator implements SlackValidator { + @Override + public void validate(GifBlock target) throws SlackValidationException { + if (target.getGifUrl() == null || target.getGifUrl().isBlank()) { + throw new SlackValidationException("Gif URL cannot be empty"); + } + + if (target.getAltText() == null || target.getAltText().isBlank()) { + throw new SlackValidationException("altText is required for accessibility"); + } + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/ImageBlockValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/ImageBlockValidator.java new file mode 100644 index 0000000..2dacdd3 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/ImageBlockValidator.java @@ -0,0 +1,18 @@ +package com.n1netails.n1netails.slack.validation.impl; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.blocks.ImageBlock; +import com.n1netails.n1netails.slack.validation.SlackValidator; + +public class ImageBlockValidator implements SlackValidator { + @Override + public void validate(ImageBlock target) throws SlackValidationException { + if (target.getImageUrl() == null || target.getImageUrl().isBlank()) { + throw new SlackValidationException("Image URL cannot be empty"); + } + + if (target.getAltText() == null || target.getAltText().isBlank()) { + throw new SlackValidationException("altText is required for accessibility"); + } + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java new file mode 100644 index 0000000..4c03936 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java @@ -0,0 +1,15 @@ +package com.n1netails.n1netails.slack.validation.impl; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.blocks.TextBlock; +import com.n1netails.n1netails.slack.validation.SlackValidator; + +public class TextBlockValidator implements SlackValidator { + @Override + public void validate(TextBlock target) throws SlackValidationException { + if (target.getText() == null || target.getText().isBlank()) { + throw new SlackValidationException("Text cannot be empty"); + } + + } +} From 58d1df4fd5cd4325e9d193569e2da68aee1689fa Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 21:23:02 +0100 Subject: [PATCH 12/37] change to RuntimeException --- .../n1netails/slack/exception/SlackClientException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackClientException.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackClientException.java index 56a103b..3906188 100644 --- a/src/main/java/com/n1netails/n1netails/slack/exception/SlackClientException.java +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackClientException.java @@ -4,7 +4,7 @@ * Slack Client Exception * @author shahid foy */ -public class SlackClientException extends Exception { +public class SlackClientException extends RuntimeException { /** * Slack Client Exception Constructor From fdb99f33141d81871dfcb4c776362d36ef2509f0 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 21:29:23 +0100 Subject: [PATCH 13/37] Correct Exception handling --- .../com/n1netails/n1netails/slack/api/BotService.java | 9 ++++++--- .../slack/exception/SlackApiExceptionWrapper.java | 6 ------ .../slack/exception/SlackTransportException.java | 4 ++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 31b6d4e..693644b 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -2,6 +2,8 @@ import com.n1netails.n1netails.slack.exception.SlackApiExceptionWrapper; import com.n1netails.n1netails.slack.exception.SlackClientException; +import com.n1netails.n1netails.slack.exception.SlackTransportException; +import com.n1netails.n1netails.slack.exception.SlackValidationException; import com.n1netails.n1netails.slack.model.SlackBlock; import com.n1netails.n1netails.slack.model.SlackMessage; import com.slack.api.Slack; @@ -40,6 +42,7 @@ public void send(SlackMessage slackMessage) throws SlackClientException { ChatPostMessageRequest.builder() .channel(slackMessage.getChannel()) .text(slackMessage.getText()); + List finalBlocks; if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) { finalBlocks = slackMessage.getRawBlocks(); @@ -62,15 +65,15 @@ public void send(SlackMessage slackMessage) throws SlackClientException { } } catch (IOException e) { - throw new SlackClientException("Network error while calling Slack API", e); + throw new SlackTransportException("Network error while calling Slack API", e); } catch (SlackApiException e) { - throw new SlackClientException("Slack SDK failure" + slackMessage.getChannel(), e); + throw new SlackTransportException("Slack SDK failure" + slackMessage.getChannel(), e); } } private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientException { if (slackMessage == null) { - throw new SlackClientException("slackMessage cannot be null"); + throw new SlackValidationException("slackMessage cannot be null"); } } diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java index 249a358..84dffdc 100644 --- a/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackApiExceptionWrapper.java @@ -1,14 +1,8 @@ package com.n1netails.n1netails.slack.exception; public class SlackApiExceptionWrapper extends SlackClientException { - private final String slackError; - public SlackApiExceptionWrapper(String slackError) { super("Slack API error: " + slackError); - this.slackError = slackError; - } - public String getSlackError() { - return slackError; } } diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java index 5baaae9..642f6e2 100644 --- a/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackTransportException.java @@ -4,4 +4,8 @@ public class SlackTransportException extends SlackClientException { public SlackTransportException(String message) { super(message); } + + public SlackTransportException(String message, Throwable cause) { + super(message, cause); + } } From db85abc62a9e3b92a80bc89fa43c7856711538c8 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Tue, 17 Feb 2026 21:29:43 +0100 Subject: [PATCH 14/37] bug in text validation --- .../com/n1netails/n1netails/slack/model/SlackMessage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 05c16b5..b183406 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -76,8 +76,8 @@ public SlackMessage build() throws SlackValidationException { } boolean hasContent = (text != null && !text.isBlank()) - || !blocks.isEmpty() - || !rawBlocks.isEmpty(); + && (!blocks.isEmpty() + || !rawBlocks.isEmpty()); if (!hasContent) { throw new SlackValidationException( From fde56f595837a58d593f08ff0cdeac5915723676 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Wed, 18 Feb 2026 13:35:40 +0100 Subject: [PATCH 15/37] refactoring BotService --- .../n1netails/slack/api/BotService.java | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 693644b..7cad471 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -38,43 +38,53 @@ public BotService(String token) { public void send(SlackMessage slackMessage) throws SlackClientException { validateSlackMessage(slackMessage); try { - ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = - ChatPostMessageRequest.builder() - .channel(slackMessage.getChannel()) - .text(slackMessage.getText()); + ChatPostMessageRequest request = buildRequest(slackMessage); + ChatPostMessageResponse response = executeRequest(request); + handleSlackResponse(response); + } catch (IOException e) { + throw new SlackTransportException("Network error while calling Slack API", e); + } catch (SlackApiException e) { + throw new SlackTransportException("Slack SDK failure" + slackMessage.getChannel(), e); + } + } - List finalBlocks; - if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) { - finalBlocks = slackMessage.getRawBlocks(); + private void handleSlackResponse(ChatPostMessageResponse response) throws SlackApiExceptionWrapper { + if (!response.isOk()) { + throw new SlackApiExceptionWrapper(response.getError()); + } + } - } else if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { - finalBlocks = slackMessage.getBlocks().stream() - .map(SlackBlock::toLayoutBlock) - .toList(); - } else - finalBlocks = null; + private ChatPostMessageResponse executeRequest(ChatPostMessageRequest request) throws SlackApiException, IOException { + return methods.chatPostMessage(request); + } - if (finalBlocks != null && !finalBlocks.isEmpty()) { - requestBuilder.blocks(finalBlocks); - } + private ChatPostMessageRequest buildRequest(SlackMessage slackMessage) { + ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = + ChatPostMessageRequest.builder() + .channel(slackMessage.getChannel()) + .text(slackMessage.getText()); - ChatPostMessageResponse response = methods.chatPostMessage(requestBuilder.build()); + List blocks = resolveBlocks(slackMessage); + if ((blocks != null && !blocks.isEmpty())) + requestBuilder.blocks(blocks); + return requestBuilder.build(); + } - if (!response.isOk()) { - throw new SlackApiExceptionWrapper(response.getError()); - } + private List resolveBlocks(SlackMessage slackMessage) { + if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) + return slackMessage.getRawBlocks(); - } catch (IOException e) { - throw new SlackTransportException("Network error while calling Slack API", e); - } catch (SlackApiException e) { - throw new SlackTransportException("Slack SDK failure" + slackMessage.getChannel(), e); + if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { + return slackMessage.getBlocks().stream() + .map(SlackBlock::toLayoutBlock) + .toList(); } + return null; } private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientException { if (slackMessage == null) { throw new SlackValidationException("slackMessage cannot be null"); } - } } From 0a7b3ca973fbca14eb7ac9789c4e63b002b8c7f0 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Wed, 18 Feb 2026 13:43:52 +0100 Subject: [PATCH 16/37] Builder for SlackMessage refactoring --- .../n1netails/slack/model/SlackMessage.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index b183406..2192eb5 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -30,6 +30,13 @@ private SlackMessage(Builder builder) { this.rawBlocks = List.copyOf(builder.rawBlocks); } + private SlackMessage(String channel, String text, List blocks, List rawBlocks) { + this.channel = channel; + this.text = text; + this.blocks = blocks; + this.rawBlocks = rawBlocks; + } + public static Builder builder() { return new Builder(); } @@ -76,8 +83,9 @@ public SlackMessage build() throws SlackValidationException { } boolean hasContent = (text != null && !text.isBlank()) - && (!blocks.isEmpty() - || !rawBlocks.isEmpty()); + || !blocks.isEmpty() + || !rawBlocks.isEmpty(); + if (!hasContent) { throw new SlackValidationException( @@ -90,8 +98,16 @@ public SlackMessage build() throws SlackValidationException { "Cannot mix SlackBlock and rawBlocks in the same message" ); } - List processedBlocks = new ArrayList<>(); + List processedBlocks = processBlocks(blocks); + + this.blocks.clear(); + this.blocks.addAll(processedBlocks); + return new SlackMessage(channel, text, processedBlocks, List.copyOf(rawBlocks)); + } + + private List processBlocks(List blocks) { + List processedBlocks = new ArrayList<>(); for (SlackBlock block : blocks) { try { SlackValidators.validate(block); @@ -100,11 +116,7 @@ public SlackMessage build() throws SlackValidationException { processedBlocks.add(SlackFallbackHandler.handle(block, e)); } } - - this.blocks.clear(); - this.blocks.addAll(processedBlocks); - - return new SlackMessage(this); + return List.copyOf(processedBlocks); } } From d52a4838f82fdf9f0c9e2b672501cf6a95e51543 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Wed, 18 Feb 2026 13:47:45 +0100 Subject: [PATCH 17/37] deleted double functionality --- .../com/n1netails/n1netails/slack/model/SlackMessage.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 2192eb5..4d48418 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -100,10 +100,7 @@ public SlackMessage build() throws SlackValidationException { } List processedBlocks = processBlocks(blocks); - this.blocks.clear(); - this.blocks.addAll(processedBlocks); - - return new SlackMessage(channel, text, processedBlocks, List.copyOf(rawBlocks)); + return new SlackMessage(channel, text, List.copyOf(processedBlocks), List.copyOf(rawBlocks)); } private List processBlocks(List blocks) { @@ -116,7 +113,7 @@ private List processBlocks(List blocks) { processedBlocks.add(SlackFallbackHandler.handle(block, e)); } } - return List.copyOf(processedBlocks); + return processedBlocks; } } From d45dd7df3369e09ef24cd4d6477d962a2194546f Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Sun, 22 Feb 2026 10:27:58 +0100 Subject: [PATCH 18/37] remove validation behaviour from builder --- .../n1netails/slack/model/SlackMessage.java | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 4d48418..2b09543 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -30,12 +30,6 @@ private SlackMessage(Builder builder) { this.rawBlocks = List.copyOf(builder.rawBlocks); } - private SlackMessage(String channel, String text, List blocks, List rawBlocks) { - this.channel = channel; - this.text = text; - this.blocks = blocks; - this.rawBlocks = rawBlocks; - } public static Builder builder() { return new Builder(); @@ -98,22 +92,8 @@ public SlackMessage build() throws SlackValidationException { "Cannot mix SlackBlock and rawBlocks in the same message" ); } - List processedBlocks = processBlocks(blocks); - - return new SlackMessage(channel, text, List.copyOf(processedBlocks), List.copyOf(rawBlocks)); - } - private List processBlocks(List blocks) { - List processedBlocks = new ArrayList<>(); - for (SlackBlock block : blocks) { - try { - SlackValidators.validate(block); - processedBlocks.add(block); - } catch (Exception e) { - processedBlocks.add(SlackFallbackHandler.handle(block, e)); - } - } - return processedBlocks; + return new SlackMessage(this); } } From c33962271c116ec084cb0769a552f369f3838209 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Sun, 22 Feb 2026 10:28:44 +0100 Subject: [PATCH 19/37] place validation behaviour into Service --- .../n1netails/slack/api/BotService.java | 56 +++++++++++++++---- .../validation/impl/TextBlockValidator.java | 1 - 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 7cad471..71fddf0 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -4,8 +4,10 @@ import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.exception.SlackTransportException; import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.fallback.SlackFallbackHandler; import com.n1netails.n1netails.slack.model.SlackBlock; import com.n1netails.n1netails.slack.model.SlackMessage; +import com.n1netails.n1netails.slack.validation.SlackValidators; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; @@ -16,6 +18,7 @@ import lombok.Setter; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -40,7 +43,9 @@ public void send(SlackMessage slackMessage) throws SlackClientException { try { ChatPostMessageRequest request = buildRequest(slackMessage); ChatPostMessageResponse response = executeRequest(request); - handleSlackResponse(response); + if (!response.isOk()) { + fallbackToText(slackMessage, response); + } } catch (IOException e) { throw new SlackTransportException("Network error while calling Slack API", e); } catch (SlackApiException e) { @@ -48,12 +53,27 @@ public void send(SlackMessage slackMessage) throws SlackClientException { } } - private void handleSlackResponse(ChatPostMessageResponse response) throws SlackApiExceptionWrapper { - if (!response.isOk()) { - throw new SlackApiExceptionWrapper(response.getError()); + private void fallbackToText(SlackMessage message, ChatPostMessageResponse originalResponse) { + try { + ChatPostMessageRequest fallbackRequest = + ChatPostMessageRequest.builder() + .channel(message.getChannel()) + .text(message.getText()) + .build(); + + ChatPostMessageResponse fallbackResponse = + executeRequest(fallbackRequest); + + if (!fallbackResponse.isOk()) { + throw new SlackApiExceptionWrapper(originalResponse.getError()); + } + + } catch (Exception e) { + throw new SlackApiExceptionWrapper(originalResponse.getError()); } } + private ChatPostMessageResponse executeRequest(ChatPostMessageRequest request) throws SlackApiException, IOException { return methods.chatPostMessage(request); } @@ -70,16 +90,28 @@ private ChatPostMessageRequest buildRequest(SlackMessage slackMessage) { return requestBuilder.build(); } - private List resolveBlocks(SlackMessage slackMessage) { - if (slackMessage.getRawBlocks() != null && !slackMessage.getRawBlocks().isEmpty()) - return slackMessage.getRawBlocks(); + private List resolveBlocks(SlackMessage message) { + if (message.getRawBlocks() != null && !message.getRawBlocks().isEmpty()) { + return message.getRawBlocks(); + } - if (slackMessage.getBlocks() != null && !slackMessage.getBlocks().isEmpty()) { - return slackMessage.getBlocks().stream() - .map(SlackBlock::toLayoutBlock) - .toList(); + if (message.getBlocks() == null || message.getBlocks().isEmpty()) { + return null; } - return null; + + List result = new ArrayList<>(); + + for (SlackBlock block : message.getBlocks()) { + try { + SlackValidators.validate(block); + result.add(block.toLayoutBlock()); + } catch (Exception e) { + SlackBlock fallback = SlackFallbackHandler.handle(block, e); + result.add(fallback.toLayoutBlock()); + } + } + + return result; } private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientException { diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java index 4c03936..3161b5d 100644 --- a/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/TextBlockValidator.java @@ -10,6 +10,5 @@ public void validate(TextBlock target) throws SlackValidationException { if (target.getText() == null || target.getText().isBlank()) { throw new SlackValidationException("Text cannot be empty"); } - } } From fbd74b0d82878f6beaa3625d8cdc7ad57fdce855 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Sun, 22 Feb 2026 14:52:51 +0100 Subject: [PATCH 20/37] restructure to keep it simple --- .../n1netails/slack/api/BotService.java | 76 +++++-------------- .../slack/exception/SlackErrorMapper.java | 65 ++++++++++++++++ .../slack/fallback/SlackFallbackHandler.java | 26 ------- .../n1netails/slack/model/SlackMessage.java | 3 - .../slack/processing/SlackBlockProcessor.java | 63 +++++++++++++++ ...idators.java => BasicSlackValidators.java} | 25 +++--- 6 files changed, 155 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/exception/SlackErrorMapper.java delete mode 100644 src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/processing/SlackBlockProcessor.java rename src/main/java/com/n1netails/n1netails/slack/validation/{SlackValidators.java => BasicSlackValidators.java} (50%) diff --git a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java index 71fddf0..00616ad 100644 --- a/src/main/java/com/n1netails/n1netails/slack/api/BotService.java +++ b/src/main/java/com/n1netails/n1netails/slack/api/BotService.java @@ -1,24 +1,19 @@ package com.n1netails.n1netails.slack.api; -import com.n1netails.n1netails.slack.exception.SlackApiExceptionWrapper; +import com.n1netails.n1netails.slack.exception.SlackErrorMapper; import com.n1netails.n1netails.slack.exception.SlackClientException; import com.n1netails.n1netails.slack.exception.SlackTransportException; import com.n1netails.n1netails.slack.exception.SlackValidationException; -import com.n1netails.n1netails.slack.fallback.SlackFallbackHandler; -import com.n1netails.n1netails.slack.model.SlackBlock; import com.n1netails.n1netails.slack.model.SlackMessage; -import com.n1netails.n1netails.slack.validation.SlackValidators; +import com.n1netails.n1netails.slack.processing.SlackBlockProcessor; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.response.chat.ChatPostMessageResponse; import com.slack.api.model.block.LayoutBlock; -import lombok.Getter; -import lombok.Setter; import java.io.IOException; -import java.util.ArrayList; import java.util.List; /** @@ -28,6 +23,7 @@ */ class BotService { private final MethodsClient methods; + private final SlackBlockProcessor blockProcessor; /** * Bot Service Constructor @@ -36,87 +32,51 @@ class BotService { */ public BotService(String token) { this.methods = Slack.getInstance().methods(token); + this.blockProcessor = new SlackBlockProcessor(); + } public void send(SlackMessage slackMessage) throws SlackClientException { - validateSlackMessage(slackMessage); + validateBasic(slackMessage); try { ChatPostMessageRequest request = buildRequest(slackMessage); - ChatPostMessageResponse response = executeRequest(request); + ChatPostMessageResponse response = methods.chatPostMessage(request); if (!response.isOk()) { - fallbackToText(slackMessage, response); + throw SlackErrorMapper.map(response); } } catch (IOException e) { throw new SlackTransportException("Network error while calling Slack API", e); } catch (SlackApiException e) { - throw new SlackTransportException("Slack SDK failure" + slackMessage.getChannel(), e); + throw new SlackTransportException("Slack SDK failure for channel: " + slackMessage.getChannel(), e); } } - private void fallbackToText(SlackMessage message, ChatPostMessageResponse originalResponse) { - try { - ChatPostMessageRequest fallbackRequest = - ChatPostMessageRequest.builder() - .channel(message.getChannel()) - .text(message.getText()) - .build(); - ChatPostMessageResponse fallbackResponse = - executeRequest(fallbackRequest); + private void validateBasic(SlackMessage message) throws SlackValidationException { + if (message == null) { + throw new SlackValidationException("SlackMessage cannot be null"); + } - if (!fallbackResponse.isOk()) { - throw new SlackApiExceptionWrapper(originalResponse.getError()); - } + if ((message.getText() == null || message.getText().isBlank()) && + (message.getBlocks() == null || message.getBlocks().isEmpty()) && + (message.getRawBlocks() == null || message.getRawBlocks().isEmpty())) { - } catch (Exception e) { - throw new SlackApiExceptionWrapper(originalResponse.getError()); + throw new SlackValidationException("Message must contain text or blocks"); } } - private ChatPostMessageResponse executeRequest(ChatPostMessageRequest request) throws SlackApiException, IOException { - return methods.chatPostMessage(request); - } - private ChatPostMessageRequest buildRequest(SlackMessage slackMessage) { ChatPostMessageRequest.ChatPostMessageRequestBuilder requestBuilder = ChatPostMessageRequest.builder() .channel(slackMessage.getChannel()) .text(slackMessage.getText()); - List blocks = resolveBlocks(slackMessage); + List blocks = blockProcessor.process(slackMessage); if ((blocks != null && !blocks.isEmpty())) requestBuilder.blocks(blocks); return requestBuilder.build(); } - private List resolveBlocks(SlackMessage message) { - if (message.getRawBlocks() != null && !message.getRawBlocks().isEmpty()) { - return message.getRawBlocks(); - } - - if (message.getBlocks() == null || message.getBlocks().isEmpty()) { - return null; - } - List result = new ArrayList<>(); - - for (SlackBlock block : message.getBlocks()) { - try { - SlackValidators.validate(block); - result.add(block.toLayoutBlock()); - } catch (Exception e) { - SlackBlock fallback = SlackFallbackHandler.handle(block, e); - result.add(fallback.toLayoutBlock()); - } - } - - return result; - } - - private void validateSlackMessage(SlackMessage slackMessage) throws SlackClientException { - if (slackMessage == null) { - throw new SlackValidationException("slackMessage cannot be null"); - } - } } diff --git a/src/main/java/com/n1netails/n1netails/slack/exception/SlackErrorMapper.java b/src/main/java/com/n1netails/n1netails/slack/exception/SlackErrorMapper.java new file mode 100644 index 0000000..52f1de5 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/exception/SlackErrorMapper.java @@ -0,0 +1,65 @@ +package com.n1netails.n1netails.slack.exception; + +import com.slack.api.methods.response.chat.ChatPostMessageResponse; + +import java.util.ArrayList; +import java.util.List; + +public class SlackErrorMapper { + + public static SlackValidationException map(ChatPostMessageResponse response) { + if (response.getErrors() == null || response.getErrors().isEmpty()) { + return new SlackValidationException(response.getError()); + } + + List formattedErrors = new ArrayList<>(); + + for (String error : response.getErrors()) { + formattedErrors.add(format(error)); + } + + return new SlackValidationException( + "Slack API validation failed:\n - " + String.join("\n - ", formattedErrors) + ); + } + + private static String format(String rawError) { + try { + String message = rawError; + int pointerStart = rawError.indexOf("[json-pointer:"); + + if (pointerStart > -1) { + message = rawError.substring(0, pointerStart).trim(); + + String pointer = rawError.substring(pointerStart) + .replace("[json-pointer:", "") + .replace("]", ""); + + return mapPointer(pointer, message); + } + + return message; + + } catch (Exception e) { + return rawError; // fallback + } + } + + private static String mapPointer(String pointer, String message) { + String[] parts = pointer.split("/"); + + if (parts.length >= 4 && "blocks".equals(parts[1])) { + int index = Integer.parseInt(parts[2]); + String field = parts[3]; + + return String.format( + "Block[%d].%s: %s", + index, + field, + message + ); + } + + return pointer + ": " + message; + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java b/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java deleted file mode 100644 index da8d9bf..0000000 --- a/src/main/java/com/n1netails/n1netails/slack/fallback/SlackFallbackHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.n1netails.n1netails.slack.fallback; - -import com.n1netails.n1netails.slack.model.SlackBlock; -import com.n1netails.n1netails.slack.model.blocks.FallbackBlock; -import com.n1netails.n1netails.slack.model.blocks.GifBlock; -import com.n1netails.n1netails.slack.model.blocks.ImageBlock; -import com.n1netails.n1netails.slack.model.blocks.TextBlock; - -public class SlackFallbackHandler { - public static SlackBlock handle(SlackBlock block, Exception e) { - - if (block instanceof ImageBlock img) { - return new FallbackBlock("Image could not be displayed: " + img.getAltText()); - } - - if (block instanceof GifBlock gif) { - return new FallbackBlock("GIF could not be displayed: " + gif.getAltText()); - } - - if (block instanceof TextBlock txt) { - return new FallbackBlock("Text could not be displayed"); - } - - return new FallbackBlock("Unsupported content"); - } -} diff --git a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java index 2b09543..825e81c 100644 --- a/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java +++ b/src/main/java/com/n1netails/n1netails/slack/model/SlackMessage.java @@ -1,11 +1,8 @@ package com.n1netails.n1netails.slack.model; import com.n1netails.n1netails.slack.exception.SlackValidationException; -import com.n1netails.n1netails.slack.fallback.SlackFallbackHandler; -import com.n1netails.n1netails.slack.validation.SlackValidators; import com.slack.api.model.block.LayoutBlock; import lombok.Getter; -import lombok.Setter; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/n1netails/n1netails/slack/processing/SlackBlockProcessor.java b/src/main/java/com/n1netails/n1netails/slack/processing/SlackBlockProcessor.java new file mode 100644 index 0000000..723f511 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/processing/SlackBlockProcessor.java @@ -0,0 +1,63 @@ +package com.n1netails.n1netails.slack.processing; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.n1netails.n1netails.slack.model.SlackMessage; +import com.n1netails.n1netails.slack.validation.BasicSlackValidators; +import com.slack.api.model.block.LayoutBlock; + +import java.util.ArrayList; +import java.util.List; + +public class SlackBlockProcessor { + + private final BasicSlackValidators basicSlackValidators; + + public SlackBlockProcessor() { + this.basicSlackValidators = new BasicSlackValidators(); + } + + public List process(SlackMessage message) { + if (message.getRawBlocks() != null && !message.getRawBlocks().isEmpty()) { + return message.getRawBlocks(); + } + + if (message.getBlocks() == null || message.getBlocks().isEmpty()) { + return null; + } + + List result = new ArrayList<>(); + List errors = new ArrayList<>(); + + int index = 0; + + for (SlackBlock block : message.getBlocks()) { + try { + basicSlackValidators.validate(block); + result.add(block.toLayoutBlock()); + } catch (SlackValidationException e) { + errors.add(formatError(index, block, e)); + } + index++; + } + + if (!errors.isEmpty()) { + throw new SlackValidationException(buildErrorMessage(errors)); + } + + return result; + } + + private String formatError(int index, SlackBlock block, Exception e) { + return String.format( + "Block[%d] (%s): %s", + index, + block.getClass().getSimpleName(), + e.getMessage() + ); + } + + private String buildErrorMessage(List errors) { + return "Slack message validation failed:\n - " + String.join("\n - ", errors); + } +} \ No newline at end of file diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java b/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java similarity index 50% rename from src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java rename to src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java index d52fe30..c0c2c81 100644 --- a/src/main/java/com/n1netails/n1netails/slack/validation/SlackValidators.java +++ b/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java @@ -11,24 +11,17 @@ import java.util.HashMap; import java.util.Map; -public class SlackValidators { - private static final Map, SlackValidator> VALIDATORS = new HashMap<>(); +public class BasicSlackValidators { + private final Map, SlackValidator> validators = new HashMap<>(); - static { - VALIDATORS.put(ImageBlock.class, new ImageBlockValidator()); - VALIDATORS.put(GifBlock.class, new GifBlockValidator()); - VALIDATORS.put(TextBlock.class, new TextBlockValidator()); + public BasicSlackValidators() { + validators.put(ImageBlock.class, new ImageBlockValidator()); + validators.put(GifBlock.class, new GifBlockValidator()); + validators.put(TextBlock.class, new TextBlockValidator()); } - - @SuppressWarnings("unchecked") - public static void validate(T target) throws SlackValidationException { + public void validate(T target) { if (target == null) return; - - SlackValidator validator = - (SlackValidator) VALIDATORS.get(target.getClass()); - - if (validator != null) { - validator.validate(target); - } + SlackValidator validator = (SlackValidator) validators.get(target.getClass()); + if (validator != null) validator.validate(target); } } From e8eea5ade97f369eec350e60898eca450dac41f8 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Mon, 23 Feb 2026 12:28:32 +0100 Subject: [PATCH 21/37] new block --- .../slack/model/block/ActionsBlock.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/block/ActionsBlock.java diff --git a/src/main/java/com/n1netails/n1netails/slack/model/block/ActionsBlock.java b/src/main/java/com/n1netails/n1netails/slack/model/block/ActionsBlock.java new file mode 100644 index 0000000..d6778eb --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/block/ActionsBlock.java @@ -0,0 +1,30 @@ +package com.n1netails.n1netails.slack.model.block; + +import com.n1netails.n1netails.slack.model.SlackBlock; +import com.n1netails.n1netails.slack.model.SlackElement; +import com.slack.api.model.block.LayoutBlock; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class ActionsBlock implements SlackBlock { + + private final List elements; + + public ActionsBlock(List elements) { + this.elements = elements; + } + + @Override + public LayoutBlock toLayoutBlock() { + return com.slack.api.model.block.ActionsBlock.builder() + .elements( + elements.stream() + .map(SlackElement::toBlockElement) + .toList() + ) + .build(); + } +} From 95c010001b850de2f52440b8c51d41cd557c77c5 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Mon, 23 Feb 2026 12:29:00 +0100 Subject: [PATCH 22/37] first element for the ActionsBlock --- .../slack/model/actions_element/Button.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/n1netails/n1netails/slack/model/actions_element/Button.java diff --git a/src/main/java/com/n1netails/n1netails/slack/model/actions_element/Button.java b/src/main/java/com/n1netails/n1netails/slack/model/actions_element/Button.java new file mode 100644 index 0000000..92fedf1 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/model/actions_element/Button.java @@ -0,0 +1,32 @@ +package com.n1netails.n1netails.slack.model.actions_element; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.SlackElement; +import com.slack.api.model.block.composition.PlainTextObject; +import com.slack.api.model.block.element.BlockElement; +import com.slack.api.model.block.element.ButtonElement; +import lombok.Getter; + +@Getter +public class Button implements SlackElement { + private final String text; + private final String actionId; + private final String url; + + public Button(String text, String actionId, String url) { + this.text = text; + this.actionId = actionId; + this.url = url; + } + + @Override + public BlockElement toBlockElement() { + return com.slack.api.model.block.element.ButtonElement.builder() + .text(com.slack.api.model.block.composition.PlainTextObject.builder() + .text(text) + .build()) + .actionId(actionId) + .url(url) + .build(); + } +} From d91ef66179b3bd9293ca4b3d83957d22d80461a7 Mon Sep 17 00:00:00 2001 From: ArturSlimac Date: Mon, 23 Feb 2026 12:29:41 +0100 Subject: [PATCH 23/37] validators --- .../validation/BasicSlackValidators.java | 22 ++++++++++----- .../impl/block/ActionsBlockValidator.java | 27 +++++++++++++++++++ .../impl/element/ButtonElementValidator.java | 21 +++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/impl/block/ActionsBlockValidator.java create mode 100644 src/main/java/com/n1netails/n1netails/slack/validation/impl/element/ButtonElementValidator.java diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java b/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java index c0c2c81..b01d5da 100644 --- a/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java +++ b/src/main/java/com/n1netails/n1netails/slack/validation/BasicSlackValidators.java @@ -1,12 +1,13 @@ package com.n1netails.n1netails.slack.validation; -import com.n1netails.n1netails.slack.exception.SlackValidationException; -import com.n1netails.n1netails.slack.model.blocks.GifBlock; -import com.n1netails.n1netails.slack.model.blocks.ImageBlock; -import com.n1netails.n1netails.slack.model.blocks.TextBlock; -import com.n1netails.n1netails.slack.validation.impl.GifBlockValidator; -import com.n1netails.n1netails.slack.validation.impl.ImageBlockValidator; -import com.n1netails.n1netails.slack.validation.impl.TextBlockValidator; +import com.n1netails.n1netails.slack.model.block.ActionsBlock; +import com.n1netails.n1netails.slack.model.block.GifBlock; +import com.n1netails.n1netails.slack.model.block.ImageBlock; +import com.n1netails.n1netails.slack.model.block.TextBlock; +import com.n1netails.n1netails.slack.validation.impl.block.ActionsBlockValidator; +import com.n1netails.n1netails.slack.validation.impl.block.GifBlockValidator; +import com.n1netails.n1netails.slack.validation.impl.block.ImageBlockValidator; +import com.n1netails.n1netails.slack.validation.impl.block.TextBlockValidator; import java.util.HashMap; import java.util.Map; @@ -14,11 +15,18 @@ public class BasicSlackValidators { private final Map, SlackValidator> validators = new HashMap<>(); + public BasicSlackValidators() { + init(); + } + + private void init() { validators.put(ImageBlock.class, new ImageBlockValidator()); validators.put(GifBlock.class, new GifBlockValidator()); validators.put(TextBlock.class, new TextBlockValidator()); + validators.put(ActionsBlock.class, new ActionsBlockValidator(this)); } + public void validate(T target) { if (target == null) return; SlackValidator validator = (SlackValidator) validators.get(target.getClass()); diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/block/ActionsBlockValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/block/ActionsBlockValidator.java new file mode 100644 index 0000000..ed5a8d6 --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/block/ActionsBlockValidator.java @@ -0,0 +1,27 @@ +package com.n1netails.n1netails.slack.validation.impl.block; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.SlackElement; +import com.n1netails.n1netails.slack.model.block.ActionsBlock; +import com.n1netails.n1netails.slack.validation.BasicSlackValidators; +import com.n1netails.n1netails.slack.validation.SlackValidator; + +public class ActionsBlockValidator implements SlackValidator { + private final BasicSlackValidators validators; + + public ActionsBlockValidator(BasicSlackValidators validators) { + this.validators = validators; + } + + @Override + public void validate(ActionsBlock target) throws SlackValidationException { + if (target.getElements() == null || target.getElements().isEmpty()) { + throw new SlackValidationException("ActionsBlock must contain at least one element"); + } + + + for (SlackElement element : target.getElements()) { + validators.validate(element); + } + } +} diff --git a/src/main/java/com/n1netails/n1netails/slack/validation/impl/element/ButtonElementValidator.java b/src/main/java/com/n1netails/n1netails/slack/validation/impl/element/ButtonElementValidator.java new file mode 100644 index 0000000..c641d5e --- /dev/null +++ b/src/main/java/com/n1netails/n1netails/slack/validation/impl/element/ButtonElementValidator.java @@ -0,0 +1,21 @@ +package com.n1netails.n1netails.slack.validation.impl.element; + +import com.n1netails.n1netails.slack.exception.SlackValidationException; +import com.n1netails.n1netails.slack.model.actions_element.Button; +import com.n1netails.n1netails.slack.validation.SlackValidator; + +public class ButtonElementValidator implements SlackValidator