Skip to content

Commit

Permalink
Merge branch 'master' into MM-564
Browse files Browse the repository at this point in the history
  • Loading branch information
raghavaggarwal2308 authored Sep 23, 2024
2 parents e21ce63 + ac41da4 commit 26626e3
Show file tree
Hide file tree
Showing 17 changed files with 365 additions and 330 deletions.
4 changes: 2 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"bundle_path": "webapp/dist/main.js"
},
"settings_schema": {
"header": "Please refer to the '/jira' command [**documentation**](https://docs.mattermost.com/integrate/jira-interoperability.html#setup) to further configure the Jira plugin.",
"footer": "Please refer to the '/jira' command [**documentation**](https://docs.mattermost.com/integrate/jira-interoperability.html#setup) to further configure the Jira plugin. Specifically, ['/jira instance [un-]install'](https://docs.mattermost.com/integrate/jira-interoperability.html#setup) and ['/jira webhook'](https://docs.mattermost.com/integrate/jira-interoperability.html#configure-webhooks-in-jira).",
"header": "Please refer to the '/jira' command [**documentation**](https://mattermost.com/pl/integrate/jira-admin-setup) to further configure the Jira plugin.",
"footer": "Please refer to the '/jira' command [**documentation**](https://mattermost.com/pl/integrate/jira-admin-setup) to further configure the Jira plugin. Specifically, ['/jira instance [un-]install'](https://mattermost.com/pl/integrate/jira-admin-setup) and ['/jira webhook'](https://mattermost.com/pl/integrate/configure-webhooks-in-jira).",
"settings": [
{
"key": "EnableJiraUI",
Expand Down
4 changes: 2 additions & 2 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func createInstanceCommand(optInstance bool) *model.AutocompleteData {
uninstall := model.NewAutocompleteData(
"uninstall", "[server|cloud-oauth] [URL]", "Disconnect Mattermost from a Jira instance")
uninstall.AddStaticListArgument("Jira type: server, cloud or cloud-oauth", true, jiraTypes)
uninstall.AddDynamicListArgument("Jira instance", makeAutocompleteRoute(routeAutocompleteInstalledInstance), true)
uninstall.AddDynamicListArgument("Jira instance", makeAutocompleteRoute(routeAutocompleteInstalledInstanceWithAlias), true)
uninstall.RoleID = model.SystemAdminRoleId

list := model.NewAutocompleteData(
Expand Down Expand Up @@ -229,7 +229,7 @@ func withParamIssueKey(cmd *model.AutocompleteData) {
func createConnectCommand() *model.AutocompleteData {
connect := model.NewAutocompleteData(
"connect", "", "Connect your Mattermost account to your Jira account")
connect.AddDynamicListArgument("Jira URL", makeAutocompleteRoute(routeAutocompleteConnect), false)
connect.AddDynamicListArgument("Jira URL", makeAutocompleteRoute(routeAutocompleteInstalledInstanceWithAlias), false)
return connect
}

Expand Down
5 changes: 4 additions & 1 deletion server/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,11 +418,14 @@ func (p *Plugin) httpAutocompleteInstalledInstanceWithAlias(w http.ResponseWrite

for _, instanceID := range info.Instances.IDs() {
item := instances.getAlias(instanceID)
helpText := string(instanceID)
if item == "" {
item = string(instanceID)
helpText = ""
}
out = append(out, model.AutocompleteListItem{
Item: item,
Item: item,
HelpText: helpText,
})
}
return respondJSON(w, out)
Expand Down
39 changes: 29 additions & 10 deletions server/subscribe.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import (
const (
JiraSubscriptionsKey = "jirasub"

FilterIncludeAny = "include_any"
FilterIncludeAll = "include_all"
FilterExcludeAny = "exclude_any"
FilterEmpty = "empty"
FilterIncludeAny = "include_any"
FilterIncludeAll = "include_all"
FilterExcludeAny = "exclude_any"
FilterEmpty = "empty"
FilterIncludeOrEmpty = "include_or_empty"

MaxSubscriptionNameLength = 100
)
Expand Down Expand Up @@ -192,7 +193,8 @@ func isValidFieldInclusion(field FieldFilter, value StringSet, inclusion string)
if (inclusion == FilterIncludeAny && !containsAny) ||
(inclusion == FilterIncludeAll && !containsAll) ||
(inclusion == FilterExcludeAny && containsAny) ||
(inclusion == FilterEmpty && value.Len() > 0) {
(inclusion == FilterEmpty && value.Len() > 0) ||
(inclusion == FilterIncludeOrEmpty && !containsAny && value.Len() > 0) {
return false
}

Expand All @@ -206,10 +208,14 @@ func (p *Plugin) getChannelsSubscribed(wh *webhook, instanceID types.ID) ([]Chan
}

var channelSubscriptions []ChannelSubscription
subscriptionMap := make(map[string]bool)
subIds := subs.Channel.ByID
for _, sub := range subIds {
if p.matchesSubsciptionFilters(wh, sub.Filters) {
channelSubscriptions = append(channelSubscriptions, sub)
if !subscriptionMap[sub.ChannelID] {
subscriptionMap[sub.ChannelID] = true
channelSubscriptions = append(channelSubscriptions, sub)
}
}
}

Expand Down Expand Up @@ -237,6 +243,10 @@ func (p *Plugin) getSubscriptionsForChannel(instanceID types.ID, channelID strin
channelSubscriptions = append(channelSubscriptions, subs.Channel.ByID[channelSubscriptionID])
}

sort.Slice(channelSubscriptions, func(i, j int) bool {
return channelSubscriptions[i].Name < channelSubscriptions[j].Name
})

return channelSubscriptions, nil
}

Expand Down Expand Up @@ -501,13 +511,22 @@ func (p *Plugin) listChannelSubscriptions(instanceID types.ID, teamID string) (s
}
rows = append(rows, fmt.Sprintf("\t* (%d) %s", len(subsIDs), instanceID))

channelSubscriptions := []ChannelSubscription{}
for _, subID := range subsIDs {
sub := subs.Channel.ByID[subID]
subName := "(No Name)"
if sub.Name != "" {
subName = sub.Name
if sub.Name == "" {
sub.Name = "(No Name)"
}
rows = append(rows, fmt.Sprintf("\t\t* %s - %s", sub.Filters.Projects.Elems()[0], subName))

channelSubscriptions = append(channelSubscriptions, sub)
}

sort.Slice(channelSubscriptions, func(i, j int) bool {
return channelSubscriptions[i].Name < channelSubscriptions[j].Name
})

for _, channelSubscription := range channelSubscriptions {
rows = append(rows, fmt.Sprintf("\t\t* %s - %s", channelSubscription.Filters.Projects.Elems()[0], channelSubscription.Name))
}
}
}
Expand Down
98 changes: 94 additions & 4 deletions server/webhook_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -245,7 +247,7 @@ func parseWebhookCommentCreated(jwh *JiraWebhook) (Webhook, error) {
JiraWebhook: jwh,
eventTypes: NewStringSet(eventCreatedComment),
headline: fmt.Sprintf("%s **commented** on %s", commentAuthor, jwh.mdKeySummaryLink()),
text: truncate(quoteIssueComment(jwh.Comment.Body), 3000),
text: truncate(quoteIssueComment(preProcessText(jwh.Comment.Body)), 3000),
}

appendCommentNotifications(wh, "**mentioned** you in a new comment on")
Expand All @@ -270,7 +272,7 @@ func appendCommentNotifications(wh *webhook, verb string) {
}

// don't mention the author of the comment
if u == jwh.User.Name || u == jwh.User.AccountID {
if u == jwh.User.Name || u == jwh.User.AccountID || u == jwh.Comment.Author.AccountID {
continue
}

Expand Down Expand Up @@ -316,6 +318,94 @@ func quoteIssueComment(comment string) string {
return "> " + strings.ReplaceAll(comment, "\n", "\n> ")
}

// preProcessText processes the given string to apply various formatting transformations.
// The purpose of the function is to convert the formatting provided by JIRA into the corresponding formatting supported by Mattermost.
// This includes converting asterisks to bold, hyphens to strikethrough, JIRA-style headings to Markdown headings,
// JIRA code blocks to inline code, numbered lists to Markdown lists, colored text to plain text, and JIRA links to Markdown links.
// For more reference, please visit https://github.com/mattermost/mattermost-plugin-jira/issues/1096
func preProcessText(jiraMarkdownString string) string {
asteriskRegex := regexp.MustCompile(`\*(\w+)\*`)
hyphenRegex := regexp.MustCompile(`-(\w+)-`)
headingRegex := regexp.MustCompile(`(?m)^(h[1-6]\.)\s+`)
langSpecificCodeBlockRegex := regexp.MustCompile(`\{code:[^}]+\}(.+?)\{code\}`)
numberedListRegex := regexp.MustCompile(`^#\s+`)
colouredTextRegex := regexp.MustCompile(`\{color:[^}]+\}(.*?)\{color\}`)
linkRegex := regexp.MustCompile(`\[(.*?)\|([^|\]]+)(?:\|([^|\]]+))?\]`)
quoteRegex := regexp.MustCompile(`\{quote\}(.*?)\{quote\}`)
codeBlockRegex := regexp.MustCompile(`\{\{(.+?)\}\}`)

// the below code converts lines starting with "#" into a numbered list. It increments the counter if consecutive lines are numbered,
// otherwise resets it to 1. The "#" is replaced with the corresponding number and period. Non-numbered lines are added unchanged.
var counter int
var lastLineWasNumberedList bool
var result []string
lines := strings.Split(jiraMarkdownString, "\n")
for _, line := range lines {
if numberedListRegex.MatchString(line) {
if !lastLineWasNumberedList {
counter = 1
} else {
counter++
}
line = strconv.Itoa(counter) + ". " + strings.TrimPrefix(line, "# ")
lastLineWasNumberedList = true
} else {
lastLineWasNumberedList = false
}
result = append(result, line)
}
processedString := strings.Join(result, "\n")

// the below code converts links in the format "[text|url]" or "[text|url|optional]" to Markdown links. If the text is empty,
// the URL is used for both the text and link. If the optional part is present, it's ignored. Unrecognized patterns remain unchanged.
processedString = linkRegex.ReplaceAllStringFunc(processedString, func(link string) string {
parts := linkRegex.FindStringSubmatch(link)
if len(parts) == 4 {
if parts[1] == "" {
return "[" + parts[2] + "](" + parts[2] + ")"
}
if parts[3] != "" {
return "[" + parts[1] + "](" + parts[2] + ")"
}
return "[" + parts[1] + "](" + parts[2] + ")"
}
return link
})

processedString = asteriskRegex.ReplaceAllStringFunc(processedString, func(word string) string {
return "**" + strings.Trim(word, "*") + "**"
})

processedString = hyphenRegex.ReplaceAllStringFunc(processedString, func(word string) string {
return "~~" + strings.Trim(word, "-") + "~~"
})

processedString = headingRegex.ReplaceAllStringFunc(processedString, func(heading string) string {
level := heading[1]
hashes := strings.Repeat("#", int(level-'0'))
return hashes + " "
})

processedString = langSpecificCodeBlockRegex.ReplaceAllStringFunc(processedString, func(codeBlock string) string {
codeContent := codeBlock[strings.Index(codeBlock, "}")+1 : strings.LastIndex(codeBlock, "{code}")]
return "`" + codeContent + "`"
})

processedString = codeBlockRegex.ReplaceAllStringFunc(processedString, func(match string) string {
curlyContent := codeBlockRegex.FindStringSubmatch(match)[1]
return "`" + curlyContent + "`"
})

processedString = colouredTextRegex.ReplaceAllString(processedString, "$1")

processedString = quoteRegex.ReplaceAllStringFunc(processedString, func(quote string) string {
quotedText := quote[strings.Index(quote, "}")+1 : strings.LastIndex(quote, "{quote}")]
return "> " + quotedText
})

return processedString
}

func parseWebhookCommentDeleted(jwh *JiraWebhook) (Webhook, error) {
if jwh.Issue.ID == "" {
return nil, ErrWebhookIgnored
Expand Down Expand Up @@ -348,7 +438,7 @@ func parseWebhookCommentUpdated(jwh *JiraWebhook) (Webhook, error) {
JiraWebhook: jwh,
eventTypes: NewStringSet(eventUpdatedComment),
headline: fmt.Sprintf("%s **edited comment** in %s", mdUser(&jwh.Comment.UpdateAuthor), jwh.mdKeySummaryLink()),
text: truncate(quoteIssueComment(jwh.Comment.Body), 3000),
text: truncate(quoteIssueComment(preProcessText(jwh.Comment.Body)), 3000),
}

return wh, nil
Expand Down Expand Up @@ -414,7 +504,7 @@ func parseWebhookUpdatedDescription(jwh *JiraWebhook, from, to string) *webhook
fromFmttd := "\n**From:** " + truncate(from, 500)
toFmttd := "\n**To:** " + truncate(to, 500)
wh.fieldInfo = webhookField{descriptionField, descriptionField, fromFmttd, toFmttd}
wh.text = jwh.mdIssueDescription()
wh.text = preProcessText(jwh.mdIssueDescription())
return wh
}

Expand Down
77 changes: 77 additions & 0 deletions server/webhook_parser_misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,80 @@ func TestWebhookQuotedComment(t *testing.T) {
assert.True(t, strings.HasPrefix(w.text, ">"))
}
}

func TestPreProcessText(t *testing.T) {
tests := map[string]struct {
input string
expectedOutput string
}{
"BOLD formatting": {
input: "*BOLD*",
expectedOutput: "**BOLD**",
},
"STRIKETHROUGH formatting": {
input: "-STRIKETHROUGH-",
expectedOutput: "~~STRIKETHROUGH~~",
},
"Colored text formatting": {
input: "{color:#ff5630}RED{color} {color:#4c9aff}BLUE{color} {color:#36b37e}GREEN{color}",
expectedOutput: "RED BLUE GREEN",
},
"Numbered list with mixed content formatting": {
input: `# NUMBERED LIST ROW 1
# NUMBERED LIST ROW 2
non-numbered list text
# NUMBERED LIST ROW 1`,
expectedOutput: `1. NUMBERED LIST ROW 1
2. NUMBERED LIST ROW 2
non-numbered list text
1. NUMBERED LIST ROW 1`,
},
"Code block formatting": {
input: "{code:go}fruit := \"APPLE\"{code}",
expectedOutput: "`fruit := \"APPLE\"`",
},
"Bullet list formatting": {
input: `* BULLET LIST ROW 1
* BULLET LIST ROW 2`,
expectedOutput: `* BULLET LIST ROW 1
* BULLET LIST ROW 2`,
},
"Heading formatting": {
input: `h1. HEADING 1
h2. HEADING 2
h3. HEADING 3
h4. HEADING 4
h5. HEADING 5
h6. HEADING 6`,
expectedOutput: `# HEADING 1
## HEADING 2
### HEADING 3
#### HEADING 4
##### HEADING 5
###### HEADING 6`,
},
"Link formatting with text": {
input: "[www.googlesd.com|http://www.googlesd.com]",
expectedOutput: "[www.googlesd.com](http://www.googlesd.com)",
},
"Link formatting with smart-link": {
input: "[http://www.google.com|http://www.google.com|smart-link]",
expectedOutput: "[http://www.google.com](http://www.google.com)",
},
"Link formatting with title": {
input: "[google|http://www.google.com]",
expectedOutput: "[google](http://www.google.com)",
},
"Quote formatting": {
input: "{quote}This is a quote{quote}",
expectedOutput: "> This is a quote",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
actualOutput := preProcessText(tc.input)
assert.Equal(t, tc.expectedOutput, actualOutput)
})
}
}
1 change: 1 addition & 0 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build
node_modules
.npminstall
junit.xml
Loading

0 comments on commit 26626e3

Please sign in to comment.