diff --git a/README.md b/README.md index 70b90930..3d4346d1 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,21 @@ slackSend( ) ``` +#### Update Messages + +You can update the content of a previously sent message using the pipeline step. +The step returns an object which you can use to retrieve the timestamp and channelId +NOTE: The slack API requires the channel ID for `chat.update` calls. + +Example: + +```groovy +def slackResponse = slackSend(channel: "updating-stuff", message: "Here is the primary message") +slackSend(channel: slackResponse.channelId, message: "Update message now", timestamp: slackResponse.ts) +``` + +This feature requires [botUser](#bot-user-mode) mode. + #### Unfurling Links You can allow link unfurling if you send the message as text. This only works in a text message, as attachments cannot be unfurled. diff --git a/src/main/java/jenkins/plugins/slack/SlackRequest.java b/src/main/java/jenkins/plugins/slack/SlackRequest.java index 65502048..11e20542 100644 --- a/src/main/java/jenkins/plugins/slack/SlackRequest.java +++ b/src/main/java/jenkins/plugins/slack/SlackRequest.java @@ -6,10 +6,11 @@ public class SlackRequest { private String message; private String color; + private String timestamp; private JSONArray attachments; private JSONArray blocks; - private SlackRequest(String message, String color, JSONArray attachments, JSONArray blocks) { + private SlackRequest(String message, String color, JSONArray attachments, JSONArray blocks, String timestamp) { if (blocks != null && color != null) { throw new IllegalArgumentException("Color is not supported when blocks are set"); } @@ -18,6 +19,7 @@ private SlackRequest(String message, String color, JSONArray attachments, JSONAr this.color = color; this.attachments = attachments; this.blocks = blocks; + this.timestamp = timestamp; } public static SlackRequestBuilder builder() { @@ -28,6 +30,10 @@ public String getMessage() { return message; } + public String getTimestamp() { + return timestamp; + } + public String getColor() { return color; } @@ -47,23 +53,25 @@ public boolean equals(Object o) { SlackRequest that = (SlackRequest) o; return Objects.equals(message, that.message) && Objects.equals(color, that.color) && + Objects.equals(timestamp, that.timestamp) && Objects.equals(attachments, that.attachments) && Objects.equals(blocks, that.blocks); } @Override public String toString() { - return String.format("SlackRequest{message='%s', color='%s', attachments=%s, blocks=%s}", message, color, attachments, blocks); + return String.format("SlackRequest{message='%s', color='%s', attachments=%s, blocks=%s, timestamp='%s'}", message, color, attachments, blocks, timestamp); } @Override public int hashCode() { - return Objects.hash(message, color, attachments, blocks); + return Objects.hash(message, color, attachments, blocks, timestamp); } public static class SlackRequestBuilder { private String message; private String color; + private String timestamp; private JSONArray attachments; private JSONArray blocks; @@ -80,6 +88,11 @@ public SlackRequestBuilder withColor(String color) { return this; } + public SlackRequestBuilder withTimestamp(String timestamp) { + this.timestamp = timestamp; + return this; + } + public SlackRequestBuilder withAttachments(JSONArray attachments) { this.attachments = attachments; return this; @@ -91,7 +104,7 @@ public SlackRequestBuilder withBlocks(JSONArray blocks) { } public SlackRequest build() { - return new SlackRequest(message, color, attachments, blocks); + return new SlackRequest(message, color, attachments, blocks, timestamp); } } diff --git a/src/main/java/jenkins/plugins/slack/SlackService.java b/src/main/java/jenkins/plugins/slack/SlackService.java index 51fa627b..f95c6b00 100755 --- a/src/main/java/jenkins/plugins/slack/SlackService.java +++ b/src/main/java/jenkins/plugins/slack/SlackService.java @@ -9,7 +9,11 @@ public interface SlackService { boolean publish(String message, String color); + boolean publish(String message, String color, String timestamp); + boolean publish(String message, JSONArray attachments, String color); + boolean publish(String message, JSONArray attachments, String color, String timestamp); + String getResponseString(); } diff --git a/src/main/java/jenkins/plugins/slack/StandardSlackService.java b/src/main/java/jenkins/plugins/slack/StandardSlackService.java index 5917180f..8ee17a8a 100755 --- a/src/main/java/jenkins/plugins/slack/StandardSlackService.java +++ b/src/main/java/jenkins/plugins/slack/StandardSlackService.java @@ -46,6 +46,7 @@ public class StandardSlackService implements SlackService { private String populatedToken; private boolean notifyCommitters; private SlackUserIdResolver userIdResolver; + private String timestamp; /** @@ -111,6 +112,7 @@ public StandardSlackService(StandardSlackServiceBuilder standardSlackServiceBuil this.populatedToken = standardSlackServiceBuilder.populatedToken; this.notifyCommitters = standardSlackServiceBuilder.notifyCommitters; this.userIdResolver = standardSlackServiceBuilder.userIdResolver; + this.timestamp = standardSlackServiceBuilder.timestamp; } public static StandardSlackServiceBuilder builder() { @@ -145,6 +147,7 @@ public boolean publish(SlackRequest slackRequest) { boolean result = true; String message = slackRequest.getMessage(); + String timestamp = slackRequest.getTimestamp(); JSONArray attachments = slackRequest.getAttachments(); JSONArray blocks = slackRequest.getBlocks(); String color = slackRequest.getColor(); @@ -190,6 +193,13 @@ public boolean publish(SlackRequest slackRequest) { json.put("unfurl_links", "true"); json.put("unfurl_media", "true"); + String apiEndpoint = "chat.postMessage"; + + if (StringUtils.isNotEmpty(timestamp)) { + json.put("ts", timestamp); + apiEndpoint = "chat.update"; + } + if (baseUrl != null) { correctMisconfigurationOfBaseUrl(); } @@ -203,7 +213,7 @@ public boolean publish(SlackRequest slackRequest) { post = new HttpPost(url); } else { - url = "https://slack.com/api/chat.postMessage"; + url = "https://slack.com/api/" + apiEndpoint; post = new HttpPost(url); post.setHeader("Authorization", "Bearer " + populatedToken); @@ -300,6 +310,43 @@ public boolean publish(String message, JSONArray attachments, String color) { ); } + @Override + public boolean publish(String message, String color, String timestamp) { + //prepare attachments first + JSONObject field = new JSONObject(); + field.put("short", false); + field.put("value", message); + + JSONArray fields = new JSONArray(); + fields.add(field); + + JSONObject attachment = new JSONObject(); + attachment.put("fallback", message); + attachment.put("color", color); + attachment.put("fields", fields); + JSONArray mrkdwn = new JSONArray(); + mrkdwn.add("pretext"); + mrkdwn.add("text"); + mrkdwn.add("fields"); + attachment.put("mrkdwn_in", mrkdwn); + JSONArray attachments = new JSONArray(); + attachments.add(attachment); + + return publish(null, attachments, color, timestamp); + } + + @Override + public boolean publish(String message, JSONArray attachments, String color, String timestamp) { + return publish( + SlackRequest.builder() + .withMessage(message) + .withTimestamp(timestamp) + .withAttachments(attachments) + .withColor(color) + .build() + ); + } + private String getTokenToUse(String authTokenCredentialId, String token) { if (!StringUtils.isEmpty(authTokenCredentialId)) { diff --git a/src/main/java/jenkins/plugins/slack/StandardSlackServiceBuilder.java b/src/main/java/jenkins/plugins/slack/StandardSlackServiceBuilder.java index 85f2b93e..6a42269b 100644 --- a/src/main/java/jenkins/plugins/slack/StandardSlackServiceBuilder.java +++ b/src/main/java/jenkins/plugins/slack/StandardSlackServiceBuilder.java @@ -16,6 +16,7 @@ public class StandardSlackServiceBuilder { String populatedToken; boolean notifyCommitters; SlackUserIdResolver userIdResolver; + String timestamp; public StandardSlackServiceBuilder() { } @@ -75,6 +76,11 @@ public StandardSlackServiceBuilder withSlackUserIdResolver(SlackUserIdResolver u return this; } + public StandardSlackServiceBuilder withTimestamp(String timestamp) { + this.timestamp = timestamp; + return this; + } + public StandardSlackService build() { return new StandardSlackService(this); } } diff --git a/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java b/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java index ca613cd9..97332b8b 100644 --- a/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java +++ b/src/main/java/jenkins/plugins/slack/workflow/SlackSendStep.java @@ -47,6 +47,7 @@ public class SlackSendStep extends Step { private static final Logger logger = Logger.getLogger(SlackSendStep.class.getName()); private String message; + private String timestamp; private String color; private String token; private String tokenCredentialId; @@ -68,6 +69,10 @@ public String getMessage() { return message; } + public String getTimestamp() { + return timestamp; + } + public String getColor() { return color; } @@ -166,6 +171,11 @@ public void setMessage(String message) { this.message = Util.fixEmpty(message); } + @DataBoundSetter + public void setTimestamp(String timestamp) { + this.timestamp = Util.fixEmpty(timestamp); + } + public boolean getReplyBroadcast() { return replyBroadcast; } @@ -283,7 +293,7 @@ protected SlackResponse run() throws Exception { listener.getLogger().println(Messages.slackSendStepValues( defaultIfEmpty(baseUrl), defaultIfEmpty(teamDomain), channel, defaultIfEmpty(color), botUser, - defaultIfEmpty(tokenCredentialId), notifyCommitters, defaultIfEmpty(iconEmoji), defaultIfEmpty(username)) + defaultIfEmpty(tokenCredentialId), notifyCommitters, defaultIfEmpty(iconEmoji), defaultIfEmpty(username), defaultIfEmpty(step.timestamp)) ); final String populatedToken; try { @@ -311,7 +321,11 @@ protected SlackResponse run() throws Exception { final boolean publishSuccess; if (sendAsText) { - publishSuccess = slackService.publish(step.message, new JSONArray(), color); + if (step.timestamp != null) { + publishSuccess = slackService.publish(step.message, new JSONArray(), color, step.timestamp); + } else { + publishSuccess = slackService.publish(step.message, new JSONArray(), color); + } } else if (step.attachments != null) { JSONArray jsonArray = getAttachmentsAsJSONArray(); for (Object object : jsonArray) { @@ -327,6 +341,7 @@ protected SlackResponse run() throws Exception { .withMessage(step.message) .withAttachments(jsonArray) .withColor(color) + .withTimestamp(step.timestamp) .build() ); } else if (step.blocks != null) { @@ -336,10 +351,15 @@ protected SlackResponse run() throws Exception { SlackRequest.builder() .withMessage(step.message) .withBlocks(jsonArray) + .withTimestamp(step.timestamp) .build() ); } else if (step.message != null) { - publishSuccess = slackService.publish(step.message, color); + if (step.timestamp != null) { + publishSuccess = slackService.publish(step.message, color, step.timestamp); + } else { + publishSuccess = slackService.publish(step.message, color); + } } else { listener.error(Messages .notificationFailedWithException(new IllegalArgumentException("No message, attachments or blocks provided"))); diff --git a/src/main/resources/jenkins/plugins/slack/Messages.properties b/src/main/resources/jenkins/plugins/slack/Messages.properties index d6b724d2..78ba5852 100644 --- a/src/main/resources/jenkins/plugins/slack/Messages.properties +++ b/src/main/resources/jenkins/plugins/slack/Messages.properties @@ -7,7 +7,7 @@ slackUserIdsFromCommittersDisplayName=Resolve Slack UserIds from Changeset Autho # Messages to display in the build logs notificationFailed=Slack notification failed. See Jenkins logs for details. notificationFailedWithException=Slack notification failed with exception: {0} -slackSendStepValues=Slack Send Pipeline step running, values are - baseUrl: {0}, teamDomain: {1}, channel: {2}, color: {3}, botUser: {4}, tokenCredentialId: {5}, notifyCommitters: {6}, iconEmoji: {7}, username: {8} +slackSendStepValues=Slack Send Pipeline step running, values are - baseUrl: {0}, teamDomain: {1}, channel: {2}, color: {3}, botUser: {4}, tokenCredentialId: {5}, notifyCommitters: {6}, iconEmoji: {7}, username: {8}, timestamp: {9} slackSendStepValuesEmptyMessage= failedToParseSlackResponse=Could not parse response from slack, potentially because of invalid configuration (botUser: true and baseUrl set), response: {0} diff --git a/src/main/resources/jenkins/plugins/slack/workflow/SlackSendStep/help-timestamp.html b/src/main/resources/jenkins/plugins/slack/workflow/SlackSendStep/help-timestamp.html new file mode 100644 index 00000000..04ededf8 --- /dev/null +++ b/src/main/resources/jenkins/plugins/slack/workflow/SlackSendStep/help-timestamp.html @@ -0,0 +1,3 @@ +
+ Allows updating an existing message instead of posting a new one. +
diff --git a/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java b/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java index 160fed02..3c9716b2 100644 --- a/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java +++ b/src/test/java/jenkins/plugins/slack/SlackNotifierTest.java @@ -72,11 +72,21 @@ public boolean publish(String message, String color) { return response; } + @Override + public boolean publish(String message, String color, String timestamp) { + return response; + } + @Override public boolean publish(String message, JSONArray attachments, String color) { return response; } + @Override + public boolean publish(String message, JSONArray attachments, String color, String timestamp) { + return response; + } + public void setResponse(boolean response) { this.response = response; } diff --git a/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java b/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java index c079f572..49acf08a 100755 --- a/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java +++ b/src/test/java/jenkins/plugins/slack/StandardSlackServiceTest.java @@ -206,6 +206,22 @@ public void sendAsBotUserInThreadReturnsTrue() { assertTrue(service.publish("message")); } + @Test + public void sendAsBotUserWithUpdate() { + StandardSlackServiceStub service = new StandardSlackServiceStub( + StandardSlackService.builder() + .withBaseUrl("") + .withTeamDomain("domain") + .withBotUser(true) + .withRoomId("#room1:1528317530") + .withPopulatedToken("token") + .withTimestamp("132124")); + CloseableHttpClientStub httpClientStub = new CloseableHttpClientStub(); + httpClientStub.setHttpStatus(HttpStatus.SC_OK); + service.setHttpClient(httpClientStub); + assertTrue(service.publish("message")); + } + @Test public void populatedTokenIsUsed() { final String populatedToken = "secret-text"; diff --git a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepIntegrationTest.java b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepIntegrationTest.java index cb8b2e2e..694da198 100644 --- a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepIntegrationTest.java +++ b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepIntegrationTest.java @@ -35,10 +35,10 @@ public void configRoundTrip() throws Exception { public void test_global_config_override() throws Exception { WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "workflow"); //just define message - job.setDefinition(new CpsFlowDefinition("slackSend(message: 'message', baseUrl: 'baseUrl', teamDomain: 'teamDomain', token: 'token', tokenCredentialId: 'tokenCredentialId', channel: '#channel', color: 'good', iconEmoji: ':+1:', username: 'username');", true)); + job.setDefinition(new CpsFlowDefinition("slackSend(message: 'message', baseUrl: 'baseUrl', teamDomain: 'teamDomain', token: 'token', tokenCredentialId: 'tokenCredentialId', channel: '#channel', color: 'good', iconEmoji: ':+1:', username: 'username', timestamp: '124124.12412');", true)); WorkflowRun run = jenkinsRule.assertBuildStatusSuccess(job.scheduleBuild2(0).get()); //everything should come from step configuration - jenkinsRule.assertLogContains(Messages.slackSendStepValues("baseUrl/", "teamDomain", "#channel", "good", false, "tokenCredentialId", false, ":+1:", "username"), run); + jenkinsRule.assertLogContains(Messages.slackSendStepValues("baseUrl/", "teamDomain", "#channel", "good", false, "tokenCredentialId", false, ":+1:", "username", "124124.12412"), run); } @Test diff --git a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java index c4f9cbe2..63f868ad 100644 --- a/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java +++ b/src/test/java/jenkins/plugins/slack/workflow/SlackSendStepTest.java @@ -472,6 +472,42 @@ public void testUsername() throws Exception { verify(slackServiceMock, times(1)).publish("message", ""); } + @Test + public void testTimestamp() throws Exception { + SlackSendStep step = new SlackSendStep(); + step.setMessage("message"); + step.setUsername("username"); + step.setTimestamp("1241242.124124"); + + SlackSendStep.SlackSendStepExecution stepExecution = spy(new SlackSendStep.SlackSendStepExecution(step, stepContextMock)); + + when(Jenkins.get()).thenReturn(jenkins); + + PowerMockito.when(CredentialsObtainer.getTokenToUse(eq("globalTokenCredentialId"), any(Item.class), any())).thenReturn("token"); + + when(stepContextMock.get(Project.class)).thenReturn(project); + + when(slackDescMock.getBaseUrl()).thenReturn("globalBaseUrl"); + when(slackDescMock.getTeamDomain()).thenReturn("globalTeamDomain"); + when(slackDescMock.getTokenCredentialId()).thenReturn("globalTokenCredentialId"); + when(slackDescMock.isBotUser()).thenReturn(false); + when(slackDescMock.getRoom()).thenReturn("globalChannel"); + when(slackDescMock.getIconEmoji()).thenReturn(":+1:"); + + NoSlackUserIdResolver noSlackUserIdResolver = new NoSlackUserIdResolver(); + when(slackDescMock.getSlackUserIdResolver()).thenReturn(noSlackUserIdResolver); + + when(taskListenerMock.getLogger()).thenReturn(printStreamMock); + doNothing().when(printStreamMock).println(); + + when(stepExecution.getSlackService(eq(run), anyString(), anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), any(), any(), any(), anyBoolean(), any(SlackUserIdResolver.class))).thenReturn(slackServiceMock); + + stepExecution.run(); + verify(stepExecution, times(1)).getSlackService(run, "globalBaseUrl", "globalTeamDomain", + false, "globalChannel", false, false, ":+1:", "username","token", false, noSlackUserIdResolver); + verify(slackServiceMock, times(1)).publish("message", "", "1241242.124124"); + } + @Test public void testNonNullEmptyColor() throws Exception { SlackSendStep step = new SlackSendStep();