From f2ad1ef79650e409ef5482ac1c99066afb65d639 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 30 Jul 2025 12:23:06 +0300 Subject: [PATCH] Fix Slack files upload support --- .../provider/DefaultSlackService.java | 98 ++++++++++++++++--- .../notification/targets/slack/SlackFile.java | 29 ++++++ .../engine/api/notification/SlackService.java | 3 + 3 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java diff --git a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java index 85f0642c55..ec835d152f 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java @@ -15,20 +15,25 @@ */ package org.thingsboard.server.service.notification.provider; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiRequest; import com.slack.api.methods.SlackApiTextResponse; +import com.slack.api.methods.SlackFilesUploadV2Exception; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.request.conversations.ConversationsListRequest; +import com.slack.api.methods.request.conversations.ConversationsOpenRequest; +import com.slack.api.methods.request.files.FilesUploadV2Request; import com.slack.api.methods.request.users.UsersListRequest; import com.slack.api.methods.response.conversations.ConversationsListResponse; import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.model.ConversationType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -36,6 +41,8 @@ import org.thingsboard.server.common.data.notification.settings.NotificationSett import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.notification.targets.slack.SlackFile; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.dao.notification.NotificationSettingsService; @@ -58,11 +65,40 @@ public class DefaultSlackService implements SlackService { @Override public void sendMessage(TenantId tenantId, String token, String conversationId, String message) { - ChatPostMessageRequest request = ChatPostMessageRequest.builder() - .channel(conversationId) - .text(message) - .build(); - sendRequest(token, request, MethodsClient::chatPostMessage); + sendMessage(tenantId, token, conversationId, message, null); + } + + @Override + public void sendMessage(TenantId tenantId, String token, String conversationId, String message, List files) { + if (CollectionsUtil.isNotEmpty(files)) { + if (conversationId.startsWith("U")) { // direct message + /* + * files.uploadV2 requires an existing channel ID, while chat.postMessage auto‑opens DMs + * */ + conversationId = sendRequest(token, ConversationsOpenRequest.builder() + .users(List.of(conversationId)) + .build(), MethodsClient::conversationsOpen).getChannel().getId(); + } + + FilesUploadV2Request request = FilesUploadV2Request.builder() + .initialComment(message) + .channel(conversationId) + .uploadFiles(files.stream() + .map(file -> FilesUploadV2Request.UploadFile.builder() + .filename(file.getName()) + .title(file.getName()) + .fileData(file.getData()) + .build()) + .toList()) + .build(); + sendRequest(token, request, MethodsClient::filesUploadV2); + } else { + ChatPostMessageRequest request = ChatPostMessageRequest.builder() + .channel(conversationId) + .text(message) + .build(); + sendRequest(token, request, MethodsClient::chatPostMessage); + } } @Override @@ -128,22 +164,52 @@ public class DefaultSlackService implements SlackService { R response; try { response = method.apply(client, request); + } catch (SlackFilesUploadV2Exception e) { + if (e.getGetURLResponses() != null) { + e.getGetURLResponses().forEach(this::checkResponse); + } + if (e.getCompleteResponse() != null) { + checkResponse(e.getCompleteResponse()); + } + if (e.getFileInfoResponses() != null) { + e.getFileInfoResponses().forEach(this::checkResponse); + } + throw new RuntimeException("Failed to upload Slack file: " + e.toString(), e); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } - if (!response.isOk()) { - String error = response.getError(); - if (error == null) { - error = "unknown error"; - } else if (error.contains("missing_scope")) { - String neededScope = response.getNeeded(); - error = "bot token scope '" + neededScope + "' is needed"; - } - throw new RuntimeException("Slack API error: " + error); - } - + checkResponse(response); return response; } + + private void checkResponse(SlackApiTextResponse response) { + if (response.isOk()) { + return; + } + + String error = response.getError(); + if (error != null) { + switch (error) { + case "missing_scope" -> { + String neededScope = response.getNeeded(); + error = "bot token scope '" + neededScope + "' is needed"; + } + case "not_in_channel" -> { + error = "app needs to be added to the channel"; + } + default -> { + error = null; + } + } + } + if (error == null) { + ObjectNode responseJson = (ObjectNode) JacksonUtil.valueToTree(response); + responseJson.remove("httpResponseHeaders"); + error = responseJson.toString(); + } + throw new RuntimeException("Slack API error: " + error); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java new file mode 100644 index 0000000000..1be73221c8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.targets.slack; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SlackFile { + + private final String name; + private final String type; // one of https://api.slack.com/types/file#file_types + private final byte[] data; + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java index e68310fce9..1ac800f488 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api.notification; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.notification.targets.slack.SlackFile; import java.util.List; @@ -25,6 +26,8 @@ public interface SlackService { void sendMessage(TenantId tenantId, String token, String conversationId, String message); + void sendMessage(TenantId tenantId, String token, String conversationId, String message, List files); + List listConversations(TenantId tenantId, String token, SlackConversationType conversationType); String getToken(TenantId tenantId);