@@ 23,6 23,10 @@ import com.atlassian.jira.component.Comp
import com.atlassian.jira.workflow.WorkflowManager
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.event.issue.*
+import com.atlassian.jira.event.issue.DelegatingJiraIssueEvent
+import com.atlassian.jira.event.issue.IssueEvent
+import com.atlassian.jira.event.issue.IssueEventBundle
+import com.atlassian.jira.event.type.EventType
import org.ofbiz.core.entity.GenericValue
@@ 123,14 127,66 @@ def messageWebhook(String webhookUrl, Ch
/** Classes implementing a toJSON() method, generating Hangout Chat message format (https://developers.google.com/hangouts/chat/reference/message-formats/).
*/
-interface ChatMessage {
- String toJSON();
+abstract class ChatMessage {
+ abstract String toJSON();
+ abstract String keyValue(key, value)
+ abstract String changeLog();
+ abstract String reformat(String text);
+ protected List<String> changeLogParts() {
+ if (event?.changeLog) {
+ // "changeitems: ${event.changeLog.getRelated('ChildChangeItem').getClass()}";
+ return event.changeLog.getRelated("ChildChangeItem").collect {
+ def ci = it as GenericValue;
+ def field = ci.getString("field");
+ //if (field == "status") return "";
+ def oldval = ci.getString("oldstring");
+ def newval = ci.getString("newstring");
+ if (field == "WorklogId") {
+ // worklog ID is weird. Only the 'old' value is set
+ keyValue(field, oldval);
+ } else if (field == "description") {
+ keyValue(field, reformat(newval)); // Don't print the old description, it's too confusing
+ } else if (field == "timespent" || field == "timeestimate" || field == "timeoriginalestimate") {
+ log "We're timeformatting ${newval}"
+ log "We're timeformatting ${oldval}"
+ keyValue(field, timeFormat(newval) + (oldval ? " (was: ${timeFormat(oldval)})" : ""));
+ } else {
+ keyValue(field, newval + (oldval ? " (was: ${oldval})" : ""));
+ }
+ }
+ }
+ }
+ /* Format seconds as days, hours and minutes. */
+ protected String formatSecs(int secs) {
+ PeriodFormatter formatter = new PeriodFormatterBuilder()
+ .appendDays()
+ .appendSuffix("d ")
+ .appendHours()
+ .appendSuffix("h ")
+ .appendMinutes()
+ .appendSuffix("m ")
+ .toFormatter();
+ // https://stackoverflow.com/questions/22420335/convert-joda-time-seconds-to-formatted-time-string
+ String formatted = formatter.print(Period.seconds(secs).normalizedStandard());
+ return formatted;
+ }
+
+ /* Given a string allegedly representing seconds, returns a nice representation in days/hours/minutes. */
+ protected timeFormat(String secsStr) {
+ //https://stackoverflow.com/questions/1713481/groovy-string-to-int
+ if (secsStr?.isInteger()) {
+ int secs = secsStr as Integer;
+ //log "Parsed ${secsStr} into ${secs} seconds, formatted to ${formatSecs(secs)}";
+ return formatSecs(secs);
+ } else return secsStr;
+ }
+
}
/**
* Create a GChat 'Simple Text' formatted message, converting from Jira wiki to GChat wiki where possible.
*/
-class SimpleChatMessage implements ChatMessage {
+class SimpleChatMessage extends ChatMessage {
Issue issue;
IssueEvent event;
@@ 161,14 217,7 @@ class SimpleChatMessage implements ChatM
} else { null }
}
- String textPara(key, value)
- {
- if (value != null) {
- """${value}"""
- } else { null }
- }
-
- String reformat(String text) {
+ public String reformat(String text) {
text?.
replaceAll(~"\\{quote\\}\\r?", '```')?.
replaceAll(~"\\{noformat\\}\\r?", '```')?.
@@ 176,74 225,13 @@ class SimpleChatMessage implements ChatM
replaceAll(~"\\{color[#:0-9a-f]*\\}", "")?.
replaceAll(~"h[1-6]\\. (.*)", '*$1*')?.
replaceAll(~'[\n\r]{4}', "\n")?.
- replaceAll(~'\\[([^|]+)\\|(http[^]]+)\\]', '<$2|$1>')?.
+ replaceAll(~'\\[([^]|]+?)\\|(http[^]]+?)\\]', '<$2|$1>')?.
replaceAll(~'\\{\\{(.*?)\\}\\}', '`$1`')?.
replaceAll(~'[A-Z]+-[0-9]+', "<${baseUrl}/browse/"+'$0|$0>');
}
- /* Format seconds as days, hours and minutes. */
- public String formatSecs(int secs) {
- PeriodFormatter formatter = new PeriodFormatterBuilder()
- .appendDays()
- .appendSuffix("d ")
- .appendHours()
- .appendSuffix("h ")
- .appendMinutes()
- .appendSuffix("m ")
- .toFormatter();
- // https://stackoverflow.com/questions/22420335/convert-joda-time-seconds-to-formatted-time-string
- String formatted = formatter.print(Period.seconds(secs).normalizedStandard());
- return formatted;
- }
-
- /* Given a string allegedly representing seconds, returns a nice representation in days/hours/minutes. */
- String timeFormat(String secsStr) {
- //https://stackoverflow.com/questions/1713481/groovy-string-to-int
- if (secsStr?.isInteger()) {
- int secs = secsStr as Integer;
- //log "Parsed ${secsStr} into ${secs} seconds, formatted to ${formatSecs(secs)}";
- return formatSecs(secs);
- } else return secsStr;
- }
-
- String changeLog() {
- if (event?.changeLog) {
- // "changeitems: ${event.changeLog.getRelated('ChildChangeItem').getClass()}";
- return event.changeLog.getRelated("ChildChangeItem").collect {
- def ci = it as GenericValue;
- def field = ci.getString("field");
- //if (field == "status") return "";
- def oldval = ci.getString("oldstring");
- def newval = ci.getString("newstring");
- if (field == "WorklogId") {
- // worklog ID is weird. Only the 'old' value is set
- keyValue(field, oldval);
- } else if (field == "description") {
- keyValue(field, reformat(newval)); // Don't print the old description, it's too confusing
- } else if (field == "timespent" || field == "timeestimate" || field == "timeoriginalestimate") {
- log "We're timeformatting ${newval}"
- log "We're timeformatting ${oldval}"
- keyValue(field, timeFormat(newval) + (oldval ? " (was: ${timeFormat(oldval)})" : ""));
- } else {
- keyValue(field, newval + (oldval ? " (was: ${oldval})" : ""));
- }
- }
- .join("\n")
- }
- }
-
- String comment() {
- if (event.comment) {
- def formattedComment = reformat(event.comment?.body);
- if (event?.changeLog) {
- return '`Comment`: ' + formattedContent;
- } else
- return formattedComment;
- }
- }
-
String header() {
- event.user?.displayName + " " + eventType.
+ "◆ " + event.user?.displayName + " " + eventType.
replaceAll('Commented', 'commented on').
replaceAll('Issue Commented', 'commented on').
replaceAll("Generic Event", "updated").
@@ 254,16 242,41 @@ class SimpleChatMessage implements ChatM
" " + (issue.assignee ? "${issue.assignee.displayName}'s" : "unassigned") +
" " + issue.issueType?.name +
" " + "<${baseUrl}/browse/${issue.key}|${issue.key} - ${issue.summary}> " + (issue.resolution ? "(${issue.status.name} / ${issue.resolution?.name})" : "") +
- ":\n"
+ " ◆ \n"
+ }
+
+ String body() {
+ // Only print the description if it's an 'Issue Created' event. In all other cases, the description
+ // will be printed (if changed) as part of the changelog
+ def Long eventTypeID = event.getEventTypeId();
+ if (eventTypeID == 1) {
+ return reformat(issue.description);
+ }
+ }
+
+ String changeLog() {
+ changeLogParts()?.join("\n");
+ }
+
+ String comment() {
+ if (event.comment) {
+ def formattedComment = reformat(event.comment?.body);
+ if (event?.changeLog) {
+ return '`Comment` ' + formattedComment;
+ } else
+ return formattedComment;
+ }
}
String toJSON()
{
def textField = header() + [
- changeLog()
+ body()
+ , changeLog()
, comment()
].findResults { it // keyValue() returns null for unset fields.
- }.join("\n") + "\n\n"; // The newlines are to visually separate bot comments, which otherwise blend together.
+ }.join("\n")
+ //+ "\n\n"; // The newlines are to visually separate bot comments, which otherwise blend together.
return '{ "text": "' + textField + '" }';
}
}
@@ 271,7 284,7 @@ class SimpleChatMessage implements ChatM
/** Create a Google CardChatMessage JSON representation of an event.
* Note that cards clip the horizontal space, so in practice the SimpleChatMessage representation works better.
*/
-class CardChatMessage implements ChatMessage {
+class CardChatMessage extends ChatMessage {
Issue issue;
IssueEvent event;
@@ 294,7 307,10 @@ class CardChatMessage implements ChatMes
return ComponentAccessor.eventTypeManager.getEventType(event.getEventTypeId()).getName();
}
-
+ public String reformat(String text) {
+ // Card formatting does not support enough HTML to emulate block text for {code} or {quote}.
+ text
+ }
String keyValue(key, value)
{
if (value != null) {
@@ 308,18 324,6 @@ class CardChatMessage implements ChatMes
} else { null }
}
- String textPara(key, value)
- {
- if (value != null) {
- """
- {
- "textParagraph": {
- "text": "${value}"
- }
- }"""
- } else { null }
- }
-
String changeLog() {
if (event?.changeLog) {
// "changeitems: ${event.changeLog.getRelated('ChildChangeItem').getClass()}";
@@ 385,9 389,44 @@ class CardChatMessage implements ChatMes
}
}
+/** Returns true if this event is the most interesting in a set of events fired simultaneously (e.g. updated + assigned).
+ * See Adaptavist docs on event bundling at https://scriptrunner.adaptavist.com/latest/jira/listeners.html
+ */
+def boolean isMostInterestingEvent() {
+ def getIncludedEvents = {
+ bundle.events.findResults { includedEvent ->
+ includedEvent instanceof DelegatingJiraIssueEvent ? includedEvent.asIssueEvent() : null
+ } as List<IssueEvent>
+ }
+
+ def eventWeight = { IssueEvent e ->
+ switch (e.eventTypeId) {
+ case 1: 10; break; // Created
+ case 2: 9; break; // Updated
+ case 3: 7; break; // Assigned
+ case 4..5: 10; break // Resolved, Closed
+ case 6: 6; break // Commented
+ case 7..12: 10; break // Reopened, Deleted, ...
+ case 13: 8; break; // Generic event
+ default: 10; break;
+ }
+ }
+
+ def sorted = getIncludedEvents().sort { a, b ->
+ log "Event a ${a.eventTypeId} has weight ${eventWeight(a)}";
+ log "Event b ${b.eventTypeId} has weight ${eventWeight(b)}";
+ eventWeight(a) < eventWeight(b) ? 1 : -1;
+ }
+
+ log "Event IDs: ${sorted.collect{ it.eventTypeId }.join(', ')}";
+ log "First event (of ${sorted.size}): ${sorted.first()?.eventTypeId}";
+
+ return event.is(sorted.first());
+}
+
def webhookUrl = getWebhookUrl(event.issue, WEBHOOK_CHAT_CUSTOMFIELD_ID);
-if (webhookUrl) {
- //CardChatMessage card = new CardChatMessage(event);
- ChatMessage msg = new SimpleChatMessage(event);
- messageWebhook(webhookUrl, msg);
+if (webhookUrl && isMostInterestingEvent()) {
+ //CardChatMessage msg = new CardChatMessage(event);
+ ChatMessage msg = new SimpleChatMessage(event);
+ messageWebhook(webhookUrl, msg);
}