diff --git a/pom.xml b/pom.xml
index ea6ddda5..99eb624b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,10 @@
io.jenkins.plugins
commons-lang3-api
+
+ io.jenkins.plugins
+ caffeine-api
+
org.jenkins-ci.plugins
structs
diff --git a/src/main/java/jenkins/plugins/slack/HttpClient.java b/src/main/java/jenkins/plugins/slack/HttpClient.java
index 2dece14a..74787ce2 100644
--- a/src/main/java/jenkins/plugins/slack/HttpClient.java
+++ b/src/main/java/jenkins/plugins/slack/HttpClient.java
@@ -1,7 +1,7 @@
package jenkins.plugins.slack;
import hudson.ProxyConfiguration;
-import jenkins.plugins.slack.NoProxyHostCheckerRoutePlanner;
+import hudson.util.Secret;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
@@ -20,7 +20,7 @@
@Restricted(NoExternalUse.class)
public class HttpClient {
- public static CloseableHttpClient getCloseableHttpClient(ProxyConfiguration proxy) {
+ public static HttpClientBuilder getCloseableHttpClientBuilder(ProxyConfiguration proxy) {
int timeoutInSeconds = 60;
RequestConfig config = RequestConfig.custom()
@@ -29,9 +29,9 @@ public static CloseableHttpClient getCloseableHttpClient(ProxyConfiguration prox
.setSocketTimeout(timeoutInSeconds * 1000).build();
final HttpClientBuilder clientBuilder = HttpClients
- .custom()
- .useSystemProperties()
- .setDefaultRequestConfig(config);
+ .custom()
+ .useSystemProperties()
+ .setDefaultRequestConfig(config);
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
@@ -41,14 +41,20 @@ public static CloseableHttpClient getCloseableHttpClient(ProxyConfiguration prox
clientBuilder.setRoutePlanner(routePlanner);
String username = proxy.getUserName();
- String password = proxy.getPassword();
+ Secret secretPassword = proxy.getSecretPassword();
+ String password = Secret.toString(secretPassword);
// Consider it to be passed if username specified. Sufficient?
- if (username != null && !"".equals(username.trim())) {
+ if (username != null && !username.trim().isEmpty()) {
credentialsProvider.setCredentials(new AuthScope(proxyHost),
createCredentials(username, password));
}
}
- return clientBuilder.build();
+ return clientBuilder;
+
+ }
+
+ public static CloseableHttpClient getCloseableHttpClient(ProxyConfiguration proxy) {
+ return getCloseableHttpClientBuilder(proxy).build();
}
private static Credentials createCredentials(String userName, String password) {
diff --git a/src/main/java/jenkins/plugins/slack/SlackNotifier.java b/src/main/java/jenkins/plugins/slack/SlackNotifier.java
index 84776ebe..c0265046 100755
--- a/src/main/java/jenkins/plugins/slack/SlackNotifier.java
+++ b/src/main/java/jenkins/plugins/slack/SlackNotifier.java
@@ -27,6 +27,7 @@
import java.util.logging.Logger;
import java.util.stream.Stream;
import jenkins.model.Jenkins;
+import jenkins.plugins.slack.cache.SlackChannelIdCache;
import jenkins.plugins.slack.config.GlobalCredentialMigrator;
import jenkins.plugins.slack.logging.BuildAwareLogger;
import jenkins.plugins.slack.logging.BuildKey;
@@ -870,6 +871,16 @@ public String getDisplayName() {
return PLUGIN_DISPLAY_NAME;
}
+ @POST
+ public FormValidation doClearCache() {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+
+ logger.info("Clearing channel ID cache");
+ SlackChannelIdCache.clearCache();
+
+ return FormValidation.ok("Cache cleared");
+ }
+
@POST
public FormValidation doTestConnectionGlobal(
@QueryParameter("baseUrl") final String baseUrl,
diff --git a/src/main/java/jenkins/plugins/slack/StandardSlackService.java b/src/main/java/jenkins/plugins/slack/StandardSlackService.java
index f5229ad8..232197b8 100755
--- a/src/main/java/jenkins/plugins/slack/StandardSlackService.java
+++ b/src/main/java/jenkins/plugins/slack/StandardSlackService.java
@@ -1,6 +1,7 @@
package jenkins.plugins.slack;
import com.google.common.annotations.VisibleForTesting;
+import hudson.AbortException;
import hudson.FilePath;
import hudson.ProxyConfiguration;
import hudson.Util;
@@ -20,6 +21,7 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jenkins.model.Jenkins;
+import jenkins.plugins.slack.cache.SlackChannelIdCache;
import jenkins.plugins.slack.pipeline.SlackFileRequest;
import jenkins.plugins.slack.pipeline.SlackUploadFileRunner;
import jenkins.plugins.slack.user.SlackUserIdResolver;
@@ -256,8 +258,17 @@ public boolean upload(FilePath workspace, String artifactIncludes, TaskListener
boolean result = true;
if(workspace!=null) {
for(String roomId : roomIds) {
+ String channelId;
+ try {
+ channelId = SlackChannelIdCache.getChannelId(populatedToken, roomId);
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (AbortException e) {
+ return false;
+ }
+
SlackFileRequest slackFileRequest = new SlackFileRequest(
- workspace, populatedToken, roomId, null, artifactIncludes);
+ workspace, populatedToken, channelId, null, artifactIncludes, null);
try {
workspace.getChannel().callAsync(new SlackUploadFileRunner(log, Jenkins.get().proxy, slackFileRequest)).get();
} catch (IllegalStateException | InterruptedException e) {
diff --git a/src/main/java/jenkins/plugins/slack/cache/SlackChannelIdCache.java b/src/main/java/jenkins/plugins/slack/cache/SlackChannelIdCache.java
new file mode 100644
index 00000000..a1606a61
--- /dev/null
+++ b/src/main/java/jenkins/plugins/slack/cache/SlackChannelIdCache.java
@@ -0,0 +1,159 @@
+package jenkins.plugins.slack.cache;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import hudson.AbortException;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+import jenkins.plugins.slack.HttpClient;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public class SlackChannelIdCache {
+
+ private static final String UPLOAD_FAILED_TEMPLATE = "Failed to retrieve channel names. Response: ";
+ private static final Logger logger = Logger.getLogger(SlackChannelIdCache.class.getName());
+
+ // cache that includes all channel names and IDs for each workspace used
+ private static final LoadingCache> CHANNEL_METADATA_CACHE = Caffeine.newBuilder()
+ .maximumSize(100)
+ .refreshAfterWrite(Duration.ofHours(24))
+ .build(SlackChannelIdCache::populateCache);
+ private static final int MAX_RETRIES = 10;
+
+ private static Map populateCache(String token) {
+ HttpClientBuilder closeableHttpClientBuilder = HttpClient.getCloseableHttpClientBuilder(Jenkins.get().getProxy())
+ .setRetryHandler((exception, executionCount, context) -> executionCount <= MAX_RETRIES)
+ .setServiceUnavailableRetryStrategy(new ServiceUnavailableRetryStrategy() {
+
+ long retryInterval;
+
+ @Override
+ public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
+ boolean shouldRetry = executionCount <= MAX_RETRIES &&
+ response.getStatusLine().getStatusCode() == HttpStatus.SC_TOO_MANY_REQUESTS;
+ if (shouldRetry) {
+ Header firstHeader = response.getFirstHeader("Retry-After");
+ if (firstHeader != null) {
+ retryInterval = Long.parseLong(firstHeader.getValue()) * 1000L;
+ logger.info(String.format("Rate limited by Slack, retrying in %dms", retryInterval));
+ }
+ }
+ return shouldRetry;
+ }
+
+ @Override
+ public long getRetryInterval() {
+ return retryInterval;
+ }
+ });
+ try (CloseableHttpClient client = closeableHttpClientBuilder.build()) {
+ return convertChannelNameToId(client, token, new HashMap<>(), null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String getChannelId(String botUserToken, String channelName) throws ExecutionException, InterruptedException, AbortException {
+ Map channelNameToIdMap = CHANNEL_METADATA_CACHE.get(botUserToken);
+ String channelId = channelNameToIdMap.get(channelName);
+
+ // most likely is that a new channel has been created since the last cache refresh
+ // or a typo in the channel name, a bit risky in larger workspaces but shouldn't happen too often
+ if (channelId == null) {
+ try {
+ CompletableFuture