Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update slack upload flow v2 #292

Merged
merged 15 commits into from
Sep 25, 2024
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
package guru.qa.allure.notifications.clients.slack;

import com.google.gson.Gson;
import guru.qa.allure.notifications.clients.Notifier;
import guru.qa.allure.notifications.clients.slack.model.ImageBlock;
import guru.qa.allure.notifications.clients.slack.model.SectionBlock;
import guru.qa.allure.notifications.clients.slack.model.TextObject;
import guru.qa.allure.notifications.config.slack.Slack;
import guru.qa.allure.notifications.exceptions.MessagingException;
import guru.qa.allure.notifications.template.MessageTemplate;
import guru.qa.allure.notifications.template.data.MessageData;
import kong.unirest.ContentType;
import kong.unirest.Unirest;
import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

@Slf4j
public class SlackClient implements Notifier {
private final Slack slack;

Expand All @@ -18,28 +40,152 @@ public SlackClient(Slack slack) {
}

@Override
public void sendText(MessageData messageData) throws MessagingException {
String body = String.format("channel=%s&text=%s",
slack.getChat(), new MessageTemplate(messageData).createMessageFromTemplate(slack.getTemplatePath()));

Unirest.post("https://slack.com/api/chat.postMessage")
.header("Authorization", "Bearer " + slack.getToken())
.header("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
.body(body)
.asString()
.getBody();
public void sendText(MessageData messageData) {
try (CloseableHttpClient client = HttpClients.createDefault()) {
postMessage(client, messageData);
} catch (IOException e) {
log.error("Failed to post message to Slack", e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap into MessagingException and re-throw it

}
}

@Override
public void sendPhoto(MessageData messageData, byte[] chartImage) throws MessagingException {
Unirest.post("https://slack.com/api/files.upload")
.header("Authorization", "Bearer " + slack.getToken())
.field("file", new ByteArrayInputStream(chartImage), ContentType.IMAGE_PNG, "chart.png")
.field("channels", slack.getChat())
.field("filename", " ")
.field("initial_comment", new MessageTemplate(messageData).createMessageFromTemplate(
slack.getTemplatePath()))
.asString()
.getBody();
@SneakyThrows
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it needed?

public void sendPhoto(MessageData messageData, byte[] chartImage) {
try (CloseableHttpClient client = HttpClients.createDefault()) {
String title = "chart.png";
JSONObject uploadResponse = getUploadURLExternal(client, title, chartImage);

String fileId = uploadResponse.getString("file_id");
String uploadUrl = uploadResponse.getString("upload_url");

MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
.addBinaryBody("file", new ByteArrayInputStream(chartImage), org.apache.http.entity.ContentType.DEFAULT_BINARY, "chart");

if (!uploadFile(uploadUrl, multipartEntityBuilder, client)) {
log.error("Failed to upload file to Slack");
}

//todo wait until file processing to complete
Thread.sleep(2000);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there way to check is file processing completed instead of hardcoded wait?


JSONObject completeResponse = completeUploadExternal(client, fileId, title);

String filePermalink = completeResponse.getJSONArray("files").getJSONObject(0).getString("permalink");

postMessage(client, messageData, filePermalink);

} catch (IOException e) {
log.error("Failed to post message with file to Slack", e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap into MessagingException and re-throw it

}
}

private void postMessage(CloseableHttpClient client, MessageData messageData) throws UnsupportedEncodingException {
postMessage(client, messageData, "");
}

private void postMessage(CloseableHttpClient client, MessageData messageData, String fileUrl) throws UnsupportedEncodingException {
HttpUriRequest request;
if (fileUrl.isEmpty()) {
request = RequestBuilder
.post("https://slack.com/api/chat.postMessage")
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken())
.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
.setEntity(new StringEntity(getTextData(messageData)))
.build();
} else {
request = RequestBuilder
.post("https://slack.com/api/chat.postMessage")
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken())
.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
.addParameter("channel", slack.getChat())
.addParameter("blocks", getBlocksForPostMessage(messageData, fileUrl))
.build();
}

try (CloseableHttpResponse responseBody = client.execute(request)) {
if (responseBody.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
log.info(EntityUtils.toString(responseBody.getEntity()));
}
} catch (IOException | UnsupportedOperationException e) {
log.error("Error post message", e);
}
}

private boolean uploadFile(String uploadUrl, MultipartEntityBuilder multipartEntityBuilder, CloseableHttpClient client) throws IOException {
HttpUriRequest request = RequestBuilder
.post(uploadUrl)
.setEntity(multipartEntityBuilder.build())
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken())
.build();

try (CloseableHttpResponse responseBody = client.execute(request)) {
if (responseBody.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
return false;
}
}
return true;
}

private String getTextData(MessageData messageData) {
return String.format("channel=%s&text=%s", slack.getChat(), proceedMessageData(messageData));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use UrlEncodedFormEntity instead

}

private String getBlocksForPostMessage(MessageData messageData, String imageUrl) {
SectionBlock sectionBlock = SectionBlock.builder().text(TextObject.builder().text(proceedMessageData(messageData)).build()).build();
ImageBlock imageBlock = ImageBlock.builder().imageUrl(imageUrl).altText("chart").build();
return new Gson().toJson(Arrays.asList(sectionBlock, imageBlock));
}

private String proceedMessageData(MessageData messageData) {
String text = "";
try {
text = new MessageTemplate(messageData).createMessageFromTemplate(slack.getTemplatePath());
} catch (MessagingException e) {
log.error("Could not create message from template", e);
}
return text;
}

private JSONObject completeUploadExternal(CloseableHttpClient client, String fileId, String title) {
JSONObject completeUploadResponse = new JSONObject();
JSONArray array = new JSONArray();
JSONObject node = new JSONObject();
node.put("id", fileId);
node.put("title", title);
array.put(node);

HttpUriRequest request = RequestBuilder
.post("https://slack.com/api/files.completeUploadExternal")
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken())
.addParameter("files", array.toString())
.build();
try (CloseableHttpResponse responseBody = client.execute(request)) {
if (responseBody.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
completeUploadResponse = new JSONObject(EntityUtils.toString(responseBody.getEntity()));
}
} catch (IOException | UnsupportedOperationException e) {
log.error("Error complete upload file", e);
}
return completeUploadResponse;
}

private JSONObject getUploadURLExternal(CloseableHttpClient client, String fileName, byte[] chartImage) {
JSONObject uploadResponse = new JSONObject();
HttpUriRequest request = RequestBuilder
.get("https://slack.com/api/files.getUploadURLExternal")
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + slack.getToken())
.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this header really needed for GET request?

.addParameter("filename", fileName)
.addParameter("length", String.valueOf(chartImage.length))
.build();
try (CloseableHttpResponse responseBody = client.execute(request)) {
if (responseBody.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
uploadResponse = new JSONObject(EntityUtils.toString(responseBody.getEntity()));
}
} catch (IOException | UnsupportedOperationException e) {
log.error("Error getting upload URL", e);
}
return uploadResponse;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package guru.qa.allure.notifications.clients.slack.model;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageBlock implements LayoutBlock {
public static final String TYPE = "image";
private final String type = TYPE;
private String fallback;

@SerializedName("image_url")
private String imageUrl;

@SerializedName("alt_text")
private String altText;
private String title;
private String blockId;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package guru.qa.allure.notifications.clients.slack.model;

public interface LayoutBlock {
String getType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package guru.qa.allure.notifications.clients.slack.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SectionBlock implements LayoutBlock {
public static final String TYPE = "section";
private final String type = TYPE;
private TextObject text;
private List<TextObject> fields;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package guru.qa.allure.notifications.clients.slack.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TextObject {
public static final String TYPE = "mrkdwn";
private final String type = TYPE;
private String text;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure to rebase the branch

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not fixed, please rebase your branch

File renamed without changes.