Merge pull request #13794 from thingsboard/fix/slack
Fix Slack files upload support
This commit is contained in:
		
						commit
						85ae3ed778
					
				@ -15,20 +15,25 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
package org.thingsboard.server.service.notification.provider;
 | 
					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.Cache;
 | 
				
			||||||
import com.github.benmanes.caffeine.cache.Caffeine;
 | 
					import com.github.benmanes.caffeine.cache.Caffeine;
 | 
				
			||||||
import com.slack.api.Slack;
 | 
					import com.slack.api.Slack;
 | 
				
			||||||
import com.slack.api.methods.MethodsClient;
 | 
					import com.slack.api.methods.MethodsClient;
 | 
				
			||||||
import com.slack.api.methods.SlackApiRequest;
 | 
					import com.slack.api.methods.SlackApiRequest;
 | 
				
			||||||
import com.slack.api.methods.SlackApiTextResponse;
 | 
					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.chat.ChatPostMessageRequest;
 | 
				
			||||||
import com.slack.api.methods.request.conversations.ConversationsListRequest;
 | 
					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.request.users.UsersListRequest;
 | 
				
			||||||
import com.slack.api.methods.response.conversations.ConversationsListResponse;
 | 
					import com.slack.api.methods.response.conversations.ConversationsListResponse;
 | 
				
			||||||
import com.slack.api.methods.response.users.UsersListResponse;
 | 
					import com.slack.api.methods.response.users.UsersListResponse;
 | 
				
			||||||
import com.slack.api.model.ConversationType;
 | 
					import com.slack.api.model.ConversationType;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import org.springframework.stereotype.Service;
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.thingsboard.common.util.JacksonUtil;
 | 
				
			||||||
import org.thingsboard.rule.engine.api.notification.SlackService;
 | 
					import org.thingsboard.rule.engine.api.notification.SlackService;
 | 
				
			||||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
					import org.thingsboard.server.common.data.id.TenantId;
 | 
				
			||||||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
 | 
					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.settings.SlackNotificationDeliveryMethodConfig;
 | 
				
			||||||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
 | 
					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.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.common.data.util.ThrowingBiFunction;
 | 
				
			||||||
import org.thingsboard.server.dao.notification.NotificationSettingsService;
 | 
					import org.thingsboard.server.dao.notification.NotificationSettingsService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -58,12 +65,41 @@ public class DefaultSlackService implements SlackService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public void sendMessage(TenantId tenantId, String token, String conversationId, String message) {
 | 
					    public void sendMessage(TenantId tenantId, String token, String conversationId, String message) {
 | 
				
			||||||
 | 
					        sendMessage(tenantId, token, conversationId, message, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void sendMessage(TenantId tenantId, String token, String conversationId, String message, List<SlackFile> 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()
 | 
					            ChatPostMessageRequest request = ChatPostMessageRequest.builder()
 | 
				
			||||||
                    .channel(conversationId)
 | 
					                    .channel(conversationId)
 | 
				
			||||||
                    .text(message)
 | 
					                    .text(message)
 | 
				
			||||||
                    .build();
 | 
					                    .build();
 | 
				
			||||||
            sendRequest(token, request, MethodsClient::chatPostMessage);
 | 
					            sendRequest(token, request, MethodsClient::chatPostMessage);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType) {
 | 
					    public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType) {
 | 
				
			||||||
@ -128,22 +164,52 @@ public class DefaultSlackService implements SlackService {
 | 
				
			|||||||
        R response;
 | 
					        R response;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            response = method.apply(client, request);
 | 
					            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) {
 | 
					        } catch (Exception e) {
 | 
				
			||||||
            throw new RuntimeException(e.getMessage(), e);
 | 
					            throw new RuntimeException(e.getMessage(), e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!response.isOk()) {
 | 
					        checkResponse(response);
 | 
				
			||||||
 | 
					        return response;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void checkResponse(SlackApiTextResponse response) {
 | 
				
			||||||
 | 
					        if (response.isOk()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        String error = response.getError();
 | 
					        String error = response.getError();
 | 
				
			||||||
            if (error == null) {
 | 
					        if (error != null) {
 | 
				
			||||||
                error = "unknown error";
 | 
					            switch (error) {
 | 
				
			||||||
            } else if (error.contains("missing_scope")) {
 | 
					                case "missing_scope" -> {
 | 
				
			||||||
                    String neededScope = response.getNeeded();
 | 
					                    String neededScope = response.getNeeded();
 | 
				
			||||||
                    error = "bot token scope '" + neededScope + "' is needed";
 | 
					                    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);
 | 
					        throw new RuntimeException("Slack API error: " + error);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return response;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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.id.TenantId;
 | 
				
			||||||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
 | 
					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.SlackConversationType;
 | 
				
			||||||
 | 
					import org.thingsboard.server.common.data.notification.targets.slack.SlackFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.List;
 | 
					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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void sendMessage(TenantId tenantId, String token, String conversationId, String message, List<SlackFile> files);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType);
 | 
					    List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    String getToken(TenantId tenantId);
 | 
					    String getToken(TenantId tenantId);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user