Skip to content

Commit

Permalink
Use file.upload v2 in slackUploadFile (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored May 7, 2024
1 parent f3b17c6 commit 41060f5
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 52 deletions.
213 changes: 162 additions & 51 deletions src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,29 @@
import jenkins.plugins.slack.HttpClient;
import jenkins.security.MasterToSlaveCallable;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.ResponseHandler;
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.ContentType;
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.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;

public class SlackUploadFileRunner extends MasterToSlaveCallable<Boolean, Throwable> implements Serializable {

private static final long serialVersionUID = 1L;
private static final String API_URL = "https://slack.com/api/files.upload";
private static final String GET_UPLOAD_URL_API = "https://slack.com/api/files.getUploadURLExternal";
private static final Logger logger = Logger.getLogger(SlackUploadFileRunner.class.getName());
private static final String UPLOAD_FAILED_TEMPLATE = "Slack upload may have failed. Response: ";

private final FilePath filePath;
private String fileToUploadPath;
private final String fileToUploadPath;

private final String channels;

Expand All @@ -55,9 +59,9 @@ public SlackUploadFileRunner(TaskListener listener, ProxyConfiguration proxy, Sl

@Override
public Boolean call() throws Throwable {
logger.info(filePath + "");
logger.info(fileToUploadPath);
listener.getLogger().println(String.format("Using dirname=%s and includeMask=%s", filePath.getRemote(), fileToUploadPath));
logger.fine(filePath + "");
logger.fine(fileToUploadPath);
listener.getLogger().printf("Using dirname=%s and includeMask=%s%n", filePath.getRemote(), fileToUploadPath);

final List<File> files = new ArrayList<>();
new DirScanner.Glob(fileToUploadPath, null).scan(new File(filePath.getRemote()), new FileVisitor() {
Expand All @@ -79,59 +83,166 @@ public void visit(File file, String relativePath) {
}

private boolean doIt(List<File> files) {
CloseableHttpClient client = HttpClient.getCloseableHttpClient(proxy);
String threadTs = null;
String theChannels = channels;

//thread_ts is passed once with roomId: Ex: roomId:threadTs
String[] splitThread = channels.split(":", 2);
if (splitThread.length == 2) {
theChannels = splitThread[0];
threadTs = splitThread[1];
}
for (File file:files) {
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()
String threadTs = null;
String theChannels = channels;

//thread_ts is passed once with roomId: Ex: roomId:threadTs
String[] splitThread = channels.split(":", 2);
if (splitThread.length == 2) {
theChannels = splitThread[0];
threadTs = splitThread[1];
}

List<String> fileIds = new ArrayList<>();
try (CloseableHttpClient client = HttpClient.getCloseableHttpClient(proxy)) {
for (File file : files) {
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
.addTextBody("channels", theChannels, ContentType.DEFAULT_TEXT)
.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName());

if (initialComment != null) {
multipartEntityBuilder = multipartEntityBuilder
.addTextBody("initial_comment", initialComment, ContentType.DEFAULT_TEXT);
JSONObject getUploadUrlResult = getUploadUrlExternal(file, client);
if (getUploadUrlResult == null) {
return false;
}

if (threadTs != null) {
multipartEntityBuilder = multipartEntityBuilder
.addTextBody("thread_ts", threadTs, ContentType.DEFAULT_TEXT);
}
String uploadUrl = getUploadUrlResult.getString("upload_url");

HttpUriRequest request = RequestBuilder
.post(API_URL)
.setEntity(multipartEntityBuilder.build())
.addHeader("Authorization", "Bearer " + token)
.build();
ResponseHandler<JSONObject> responseHandler = response -> {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? new org.json.JSONObject(EntityUtils.toString(entity)) : null;
} else {
logger.log(Level.WARNING, UPLOAD_FAILED_TEMPLATE + status);
return null;
}
};
try {
org.json.JSONObject responseBody = client.execute(request, responseHandler);
if (responseBody != null && !responseBody.getBoolean("ok")) {
listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + responseBody.toString());
return false;
}
} catch (IOException e) {
String msg = "Exception uploading files '" + file + "' to Slack ";
logger.log(Level.WARNING, msg, e);
listener.getLogger().println(msg + e.getMessage());
if (!uploadFile(uploadUrl, multipartEntityBuilder, client)) {
listener.getLogger().println("Failed to upload file to Slack");
return false;
}
String fileId = getUploadUrlResult.getString("file_id");
fileIds.add(fileId);
}
String channelId = convertChannelNameToId(theChannels, client);
if (!completeUploadExternal(channelId, threadTs, fileIds, client)) {
listener.getLogger().println("Failed to complete uploading file to Slack");
return false;
}

} catch (IOException e) {
String msg = "Exception uploading to Slack ";
logger.log(Level.WARNING, msg, e);
listener.getLogger().println(msg + e.getMessage());
}
return true;
}

private boolean completeUploadExternal(String channelId, String threadTs, List<String> fileIds, CloseableHttpClient client) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("channel_id", channelId);
if (initialComment != null) {
jsonObject.put("initial_comment", initialComment);
}
if (threadTs != null) {
jsonObject.put("thread_ts", threadTs);
}

jsonObject.put("files", convertListToJsonArray(fileIds));
HttpUriRequest completeRequest = RequestBuilder
.post("https://slack.com/api/files.completeUploadExternal")
.setEntity(new StringEntity(jsonObject.toString(), ContentType.APPLICATION_JSON))
.addHeader("Authorization", "Bearer " + token)
.build();

JSONObject completeRequestResponse = client.execute(completeRequest, getStandardResponseHandler());

if (completeRequestResponse != null && !completeRequestResponse.getBoolean("ok")) {
listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + completeRequestResponse);
return false;
}

return true;
}

private static JSONArray convertListToJsonArray(List<String> fileIds) {
JSONArray jsonArray = new JSONArray();
fileIds.stream()
.map(fileId -> new JSONObject().put("id", fileId))
.forEach(jsonArray::put);
return jsonArray;
}

private static ResponseHandler<JSONObject> getStandardResponseHandler() {
return response -> {
int status = response.getStatusLine().getStatusCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? new JSONObject(EntityUtils.toString(entity)) : null;
} else {
logger.log(Level.WARNING, UPLOAD_FAILED_TEMPLATE + status);
return null;
}
};
}

private String convertChannelNameToId(String channels, CloseableHttpClient client) throws IOException {
return convertChannelNameToId(channels, client, null);
}

private String convertChannelNameToId(String channelName, CloseableHttpClient client, String cursor) throws IOException {
RequestBuilder requestBuilder = RequestBuilder.get("https://slack.com/api/conversations.list")
.addHeader("Authorization", "Bearer " + token)
.addParameter("exclude_archived", "true")
.addParameter("types", "public_channel,private_channel");

if (cursor != null) {
requestBuilder.addParameter("cursor", cursor);
}
ResponseHandler<JSONObject> standardResponseHandler = getStandardResponseHandler();
JSONObject result = client.execute(requestBuilder.build(), standardResponseHandler);

if (result == null || !result.getBoolean("ok")) {
return null;
}

JSONArray channelsArray = result.getJSONArray("channels");
for (int i = 0; i < channelsArray.length(); i++) {
JSONObject channel = channelsArray.getJSONObject(i);
if (channel.getString("name").equals(channelName)) {
return channel.getString("id");
}
}

cursor = result.getJSONObject("response_metadata").getString("next_cursor");
if (cursor != null && !cursor.isEmpty()) {
return convertChannelNameToId(channelName, client, cursor);
}

listener.getLogger().println("Couldn't find channel id for channel name " + channelName);

return null;
}

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

try (CloseableHttpResponse responseBody = client.execute(request)) {
if (responseBody.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + EntityUtils.toString(responseBody.getEntity()));
return false;
}
}
return true;
}

private JSONObject getUploadUrlExternal(File file, CloseableHttpClient client) throws IOException {
HttpUriRequest getUploadApiRequest = RequestBuilder.get(GET_UPLOAD_URL_API)
.addParameter("filename", file.getName())
.addParameter("length", String.valueOf(file.length()))
.addHeader("Authorization", "Bearer " + token)
.build();
JSONObject getUploadRequestResponse = client.execute(getUploadApiRequest, getStandardResponseHandler());
if (getUploadRequestResponse != null && !getUploadRequestResponse.getBoolean("ok")) {
listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + getUploadRequestResponse);
return null;
} else if (getUploadRequestResponse == null) {
listener.getLogger().println(UPLOAD_FAILED_TEMPLATE);
return null;
}
return getUploadRequestResponse;

Check warning on line 246 in src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 96-246 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<div>
Allows overriding the Slack Plugin channel specified in the global configuration.
Multiple channels may be provided as a comma separated string.
<br>
<code>slackUploadFile channel: "#channel-name", filePath: "file.txt"</code>
</div>

0 comments on commit 41060f5

Please sign in to comment.