Skip to content

Commit

Permalink
Fix #2 Add support for Slack message formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
juhasipo committed Apr 3, 2016
1 parent 2487a37 commit 0957650
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
1.2
---

* Add support for Slack message formatting in message

1.1
---

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Configuration
|Display name|Sender name (e.g. Go CD)|Yes|
|Title|Message title (e.g. Deployed to production)|Yes|
|Message|Actual message|No|
|Message: Slack message formatting|Enable Slack message formatting for the message|No|
|Icon or Emoji|Icon URL or Slack emoji (e.g. `https://example.org/icon.png` or `:tada:`)|No|
|Color|Pre-defined colors|Yes|
|Custom color|Custom color as a six hex digit code without `#` (e.g. `ff0000` for red). Only when custom color is selected as the color.|No|
Expand All @@ -35,6 +36,10 @@ The plugin currently only supports adding the environment variables as is.
Normal Bash string manipulation is not supported. Referencing parameters works the
same way it does in Go (e.g. `#{paramName}`).

It is possible to format the message using [Slack's message formatting options](https://get.slack.help/hc/en-us/articles/202288908-How-can-I-add-formatting-to-my-messages-).
Formatting can be enabled for the message via the _Slack message formatting_ checkbox under
the message input. Display name and title fields don't support message formatting.

License
-------

Expand Down
13 changes: 13 additions & 0 deletions resources/views/task.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@
<span class="form_error" ng-show="GOINPUTNAME[Message].$error.server">
{{ GOINPUTNAME[Message].$error.server }}
</span>
<div class="checkbox_row">
<input id="markdownInText" type="checkbox" ng-model="cbMarkdownInText"
ng-init="cbMarkdownInText = MarkdownInText"
ng-change="MarkdownInText = cbMarkdownInText"
ng-true-value="true" ng-false-value="false">
<input type="hidden" value="{{MarkdownInText}}" ng-model="MarkdownInText">
<label for="markdownInText">
Slack message formatting (See <a href="https://get.slack.help/hc/en-us/articles/202288908-How-can-I-add-formatting-to-my-messages-" target="_blank">Slack help</a>)
</label>
</div>
<span class="form_error" ng-show="GOINPUTNAME[MarkdownInText].$error.server">
{{ GOINPUTNAME[MarkdownInText].$error.server }}
</span>
</div>
<div class="form_item_block">
<label for="IconOrEmoji">Icon or Emoji</label>
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/vincit/go/task/slack/SlackTaskPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ protected GoPluginApiResponse handleTaskExecution(GoPluginApiRequest request) {
messageFormatter.format(config.getTitle()),
messageFormatter.format(config.getMessage()),
config.getIconOrEmoji(),
config.getColor()
config.getColor(),
config.getMarkdownIns()
);

TaskSlackDestination destination = new TaskSlackDestination(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ConfigProvider {
public static final String DISPLAY_NAME = "DisplayName";
public static final String COLOR = "Color";
public static final String COLOR_TYPE = "ColorType";
public static final String MARKDOWN_IN_TEXT = "MarkdownInText";

private static Map<String, HashMap<String, Object>> fieldConfig = new HashMap<>();

Expand All @@ -34,6 +35,7 @@ public class ConfigProvider {
fieldConfig.put(DISPLAY_NAME, createField("", Secure.NO, Required.NO));
fieldConfig.put(COLOR_TYPE, createField(ColorType.NONE.getDisplayValue(), Secure.NO, Required.YES));
fieldConfig.put(COLOR, createField("", Secure.NO, Required.NO));
fieldConfig.put(MARKDOWN_IN_TEXT, createField("", Secure.NO, Required.NO));
}

public static Map<String, ?> getFieldConfig() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.vincit.go.task.slack.executor;

import com.vincit.go.task.slack.model.ChannelType;
import com.vincit.go.task.slack.model.MarkdownField;
import in.ashwanthkumar.slack.webhook.Slack;
import in.ashwanthkumar.slack.webhook.SlackAttachment;

Expand All @@ -26,6 +27,10 @@ public void sendMessage(TaskSlackMessage message) throws IOException {
.color(message.getColor())
.title(message.getTitle());

for (MarkdownField field : message.getMarkdownIns()) {
attachment.addMarkdownIn(field.getApiValue());
}

updateChannel(destination);

slack.displayName(message.getDisplayName());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.vincit.go.task.slack.executor;

import com.vincit.go.task.slack.model.MarkdownField;

import java.util.Set;

public class TaskSlackMessage {

private final String title;
private final String message;
private final String iconOrEmoji;
private final String color;
private final String displayName;
private final Set<MarkdownField> markdownIns;

public TaskSlackMessage(String displayName, String title, String message, String iconOrEmoji, String color) {
public TaskSlackMessage(String displayName, String title, String message, String iconOrEmoji, String color, Set<MarkdownField> markdownIns) {
this.title = title;
this.message = message;
this.iconOrEmoji = iconOrEmoji;
this.color = color;
this.displayName = displayName;
this.markdownIns = markdownIns;
}

public String getTitle() {
Expand All @@ -35,4 +41,8 @@ public String getColor() {
public String getDisplayName() {
return displayName;
}

public Set<MarkdownField> getMarkdownIns() {
return markdownIns;
}
}
20 changes: 19 additions & 1 deletion src/main/java/com/vincit/go/task/slack/model/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.vincit.go.task.slack.config.ConfigProvider;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class Config {

Expand All @@ -26,11 +28,13 @@ public class Config {
private Property colorType;
@SerializedName("Color")
private Property color;
@SerializedName("MarkdownInText")
private Property markdownInText;

public Config() {
}

public Config(Property webhookUrl, Property channel, Property channelType, Property displayName, Property title, Property message, Property iconOrEmoji, Property colorType, Property color) {
public Config(Property webhookUrl, Property channel, Property channelType, Property displayName, Property title, Property message, Property iconOrEmoji, Property colorType, Property color, Property markdownInText) {
this.message = message;
this.title = title;
this.iconOrEmoji = iconOrEmoji;
Expand All @@ -40,6 +44,7 @@ public Config(Property webhookUrl, Property channel, Property channelType, Prope
this.displayName = displayName;
this.colorType = colorType;
this.color = color;
this.markdownInText = markdownInText;
}

private String getValueOr(Property property, String value) {
Expand Down Expand Up @@ -144,4 +149,17 @@ public Map<String, String> validate() {
}


public Set<MarkdownField> getMarkdownIns() {
Set<MarkdownField> markdownIns = new HashSet<>();

addFieldIfSet(markdownInText, MarkdownField.TEXT, markdownIns);

return markdownIns;
}

private void addFieldIfSet(Property property, MarkdownField field, Set<MarkdownField> ins) {
if (property != null && property.isPresent() && Boolean.valueOf(property.getValueOr("false"))) {
ins.add(field);
}
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/vincit/go/task/slack/model/MarkdownField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.vincit.go.task.slack.model;

public enum MarkdownField {

PRETEXT("pretext"),
TEXT("text"),
FIELDS("fields");

private final String apiValue;

MarkdownField(String apiValue) {
this.apiValue = apiValue;
}

public String getApiValue() {
return apiValue;
}
}
19 changes: 12 additions & 7 deletions src/test/java/com/vincit/go/task/slack/SlackTaskPluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse;
import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;
import com.vincit.go.task.slack.config.ConfigProvider;
import com.vincit.go.task.slack.executor.*;
import com.vincit.go.task.slack.executor.SlackExecutor;
import com.vincit.go.task.slack.executor.SlackExecutorFactory;
import com.vincit.go.task.slack.executor.TaskSlackDestination;
import com.vincit.go.task.slack.executor.TaskSlackMessage;
import com.vincit.go.task.slack.model.*;
import com.vincit.go.task.slack.utils.FileReader;
import com.vincit.go.task.slack.utils.JsonUtil;
Expand All @@ -20,9 +23,7 @@
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyMap;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;

public class SlackTaskPluginTest {

Expand Down Expand Up @@ -62,7 +63,8 @@ public void testHandleTaskExecution() throws Exception {
prop("webhook"), prop("channel"), prop("CHANNEL"), prop("display-name"), prop("title"), prop("message"),
prop("icon"),
prop("CUSTOM"),
prop("00ff00")
prop("00ff00"),
prop(null)
),
new Context(new HashMap<String, String>())
)
Expand Down Expand Up @@ -105,7 +107,8 @@ public void testHandleValidation() throws Exception {
requiredProp("webhook"), prop("channel"), requiredProp("Channel"), prop("display-name"), prop("title"), prop("message"),
prop("icon"),
requiredProp("Custom"),
prop("00ff00")
prop("00ff00"),
prop(null)
)
);
when(jsonUtil.responseAsJson(eq(200), any(Object.class))).thenReturn(new DefaultGoPluginApiResponse(200));
Expand Down Expand Up @@ -135,7 +138,8 @@ public void testHandleValidationRequiredErrors() throws Exception {
requiredProp(""), prop("channel"), requiredProp(""), prop("display-name"), prop("title"), prop("message"),
prop("icon"),
requiredProp(""),
prop("")
prop(""),
prop(null)
)
);
when(jsonUtil.responseAsJson(eq(200), any())).thenReturn(new DefaultGoPluginApiResponse(200));
Expand Down Expand Up @@ -185,6 +189,7 @@ public void testConfig() throws Exception {
expected.put("Title", field());
expected.put("ColorType", requiredFieldWithDefaultValue("None"));
expected.put("ChannelType", requiredFieldWithDefaultValue("Channel"));
expected.put("MarkdownInText", field());

assertThat(config, is((HashMap)expected));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
package com.vincit.go.task.slack.executor;

import com.vincit.go.task.slack.model.ChannelType;
import com.vincit.go.task.slack.model.MarkdownField;
import in.ashwanthkumar.slack.webhook.Slack;
import in.ashwanthkumar.slack.webhook.SlackAttachment;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static org.mockito.Matchers.any;
import static com.vincit.go.task.slack.utils.HashSets.asSet;
import static com.vincit.go.task.slack.utils.HashSets.valuesAsSet;
import static com.vincit.go.task.slack.utils.ReflectionUtil.getFieldValue;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class SlackExecutorTest {

@Test
public void testSendMessageToChannel() throws IOException {
public void testSendMessageToChannel() throws Exception {
Slack slack = mock(Slack.class);
TaskSlackDestination destination = new TaskSlackDestination(
"https://example.org/",
Expand All @@ -24,18 +33,27 @@ public void testSendMessageToChannel() throws IOException {

SlackExecutor executor = new SlackExecutor(slack, destination);
TaskSlackMessage message = new TaskSlackMessage(
"displayName", "title", "message", "iconOrEmoji", "good"
"displayName", "title", "message", "iconOrEmoji", "good",
new HashSet<>(Arrays.asList(MarkdownField.TEXT, MarkdownField.PRETEXT))
);
executor.sendMessage(message);

ArgumentCaptor<SlackAttachment> attachmentArgumentCaptor =
ArgumentCaptor.forClass(SlackAttachment.class);

verify(slack).sendToChannel("channel-name");
verify(slack).push(any(SlackAttachment.class));
verify(slack).push(attachmentArgumentCaptor.capture());
verify(slack).displayName("displayName");
verify(slack).icon("iconOrEmoji");

SlackAttachment attachment = attachmentArgumentCaptor.getValue();
Set<String> values = asSet(getFieldValue(attachment, "markdown", List.class));
assertThat(values, is(valuesAsSet("pretext", "text")));

}

@Test
public void testSendMessageToUser() throws IOException {
public void testSendMessageToUser() throws Exception {
Slack slack = mock(Slack.class);
TaskSlackDestination destination = new TaskSlackDestination(
"https://example.org/",
Expand All @@ -45,14 +63,23 @@ public void testSendMessageToUser() throws IOException {

SlackExecutor executor = new SlackExecutor(slack, destination);
TaskSlackMessage message = new TaskSlackMessage(
"displayName", "title", "message", "iconOrEmoji", "good"
"displayName", "title", "message", "iconOrEmoji", "good",
new HashSet<>(Arrays.asList(MarkdownField.TEXT, MarkdownField.PRETEXT))
);
executor.sendMessage(message);

ArgumentCaptor<SlackAttachment> attachmentArgumentCaptor =
ArgumentCaptor.forClass(SlackAttachment.class);

verify(slack).sendToUser("channel-name");
verify(slack).push(any(SlackAttachment.class));
verify(slack).push(attachmentArgumentCaptor.capture());
verify(slack).displayName("displayName");
verify(slack).icon("iconOrEmoji");

SlackAttachment attachment = attachmentArgumentCaptor.getValue();
Set<String> values = asSet(getFieldValue(attachment, "markdown", List.class));
assertThat(values, is(valuesAsSet("pretext", "text")));
}


}
16 changes: 14 additions & 2 deletions src/test/java/com/vincit/go/task/slack/model/ConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsCollectionContaining.hasItem;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue;

Expand Down Expand Up @@ -149,12 +150,23 @@ public void parseTargetConfig() throws Exception {

}

@Test
public void testMarkdownIns_Text() {
Config config = configWithTextMarkdownIn();
assertThat(config.getMarkdownIns(), hasItem(MarkdownField.TEXT));
assertThat(config.getMarkdownIns().size(), is(1));
}

private Config configWithColor(String colorType, String color) {
return new Config(prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(colorType), prop(color));
return new Config(prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(colorType), prop(color), prop(null));
}

private Config configWithChannel(String channelType, String channel) {
return new Config(prop(null), prop(channel), prop(channelType), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null));
return new Config(prop(null), prop(channel), prop(channelType), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null));
}

private Config configWithTextMarkdownIn() {
return new Config(prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop(null), prop("true"));
}

private Property prop(String value) {
Expand Down
Loading

0 comments on commit 0957650

Please sign in to comment.