Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mm-507] Add support for DM subscriptions based on "watching" an issue in Jira (#507) #792

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
397c5fd
Add support for DM subscriptions based on "watching" an issue in Jira…
jupriano Aug 4, 2021
626719d
Jira's autolink should support issue links that contain a comment lin…
jupriano Aug 19, 2021
d046db9
Jira's autolink should support issue links that contain a comment lin…
jupriano Sep 2, 2021
2325e8e
Jira's autolink should support issue links that contain a comment lin…
jupriano Sep 2, 2021
899017f
Update server/webhook.go
jupriano Oct 28, 2021
8b6b28b
Fixing Request Changes (#507)
Oct 28, 2021
e17c0cf
Fixing Request Changes (#507)
Oct 28, 2021
74e9b69
Revert "Fixing Request Changes (#507)"
Oct 28, 2021
51f0fb2
Revert "Fixing Request Changes (#507)"
Oct 28, 2021
ece396d
Fixing Request Changes (#507)
Oct 28, 2021
dd55c1d
Merge branch 'mattermost:master' into MM-507
jupriano Nov 15, 2021
e3e5502
[GH-507] Change GetConnection Struct (#507)
Nov 16, 2021
29e4ed3
[mm-507] Fixing Unit Test and CI lint
Nov 25, 2021
0b02444
[mm-507] Fixing Unit Test and CI lint
Nov 25, 2021
f980d21
[mm-507] Fixing Unit Test and CI lint
Nov 25, 2021
9cc2e4d
[mm-507] Fixing Unit Test and CI lint
Nov 25, 2021
028ae19
[mm-507] Remove no notification block issue watcher (#507)
Nov 25, 2021
23d01fc
[mm-507] Loload instance error instance not found (#507)
Nov 25, 2021
e1e669d
Update server/user_test.go
jupriano Dec 1, 2021
702c079
Update server/plugin.go
jupriano Dec 1, 2021
24eaa07
Update server/command_test.go
jupriano Dec 1, 2021
73373d1
Update server/command_test.go
jupriano Dec 1, 2021
29a812d
Update server/user_test.go
jupriano Dec 1, 2021
9d72ffe
[MM-507] Add Error Log for error (#MM-507)
Dec 1, 2021
ae2b7f2
[MM-507] Fixing Failed Test
Dec 6, 2021
65f0e66
[MM-507] Fixing Failed Test
Dec 6, 2021
681eff5
[MM-507] Fixing Failed Test
Dec 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type SearchService interface {
type IssueService interface {
GetIssue(key string, options *jira.GetQueryOptions) (*jira.Issue, error)
CreateIssue(issue *jira.Issue) (*jira.Issue, error)
GetWatchers(issueKey string) (*[]jira.User, error)

AddAttachment(api plugin.API, issueKey, fileID string, maxSize utils.ByteSize) (mattermostName, jiraName, mime string, err error)
AddComment(issueKey string, comment *jira.Comment) (*jira.Comment, error)
Expand Down Expand Up @@ -199,6 +200,15 @@ func (client JiraClient) GetIssue(key string, options *jira.GetQueryOptions) (*j
return issue, nil
}

// GetWatchers returns an array of Jira users watching for a given issue.
func (client JiraClient) GetWatchers(issueKey string) (*[]jira.User, error) {
watchers, resp, err := client.Jira.Issue.GetWatchers(issueKey)
if err != nil {
return nil, userFriendlyJiraError(resp, err)
}
return watchers, nil
}

// GetTransitions returns transitions for an issue with issueKey.
func (client JiraClient) GetTransitions(issueKey string) ([]jira.Transition, error) {
transitions, resp, err := client.Jira.Issue.GetTransitions(issueKey)
Expand Down
2 changes: 2 additions & 0 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@ func executeSettings(p *Plugin, c *plugin.Context, header *model.CommandArgs, ar
return p.responsef(header, "Current settings:\n%s", conn.Settings.String())
case "notifications":
return p.settingsNotifications(header, instance.GetID(), user.MattermostUserID, conn, args)
case "watching":
return p.settingsWatching(header, instance.GetID(), user.MattermostUserID, conn, args)
default:
return p.responsef(header, "Unknown setting.")
}
Expand Down
4 changes: 2 additions & 2 deletions server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ func TestPlugin_ExecuteCommand_Settings(t *testing.T) {
"no params, with notifications": {
commandArgs: &model.CommandArgs{Command: "/jira settings", UserId: mockUserIDWithNotifications},
numInstances: 1,
expectedMsg: "Current settings:\n\tNotifications: on",
expectedMsg: "Current settings:\n- Notifications: on\n\t- Watching: on",
},
"no params, without notifications": {
commandArgs: &model.CommandArgs{Command: "/jira settings", UserId: mockUserIDWithoutNotifications},
numInstances: 1,
expectedMsg: "Current settings:\n\tNotifications: off",
expectedMsg: "Current settings:\n- Notifications: off\n\t- Watching: off",
},
"unknown setting": {
commandArgs: &model.CommandArgs{Command: "/jira settings" + " test", UserId: mockUserIDWithoutNotifications},
Expand Down
9 changes: 8 additions & 1 deletion server/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,17 @@ func (store *store) LoadInstance(instanceID types.ID) (Instance, error) {
if instanceID == "" {
return nil, errors.Wrap(kvstore.ErrNotFound, "no instance specified")
}
instance, err := store.LoadInstanceFullKey(hashkey(prefixInstance, instanceID.String()))
hashedKey := hashkey(prefixInstance, instanceID.String())
instance, err := store.LoadInstanceFullKey(hashedKey)
if err != nil {
if errors.Is(errors.Wrap(kvstore.ErrNotFound, hashedKey), err) {
return nil, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vicky-demansol can we log the error and in below case too.

}
return nil, errors.Wrap(err, instanceID.String())
}
if instance == nil {
return nil, nil
}
return instance, nil
}

Expand Down
64 changes: 64 additions & 0 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/mattermost/mattermost-plugin-jira/server/tracker"
"github.com/mattermost/mattermost-plugin-jira/server/utils"
"github.com/mattermost/mattermost-plugin-jira/server/utils/telemetry"
"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
)

const (
Expand Down Expand Up @@ -425,3 +426,66 @@ func (p *Plugin) CheckSiteURL() error {
}
return nil
}

func (p *Plugin) GetWebhookURL(jiraURL string, teamID, channelID string) (subURL, legacyURL string, err error) {
cf := p.getConfig()

instanceID, err := p.ResolveWebhookInstanceURL(jiraURL)
if err != nil {
p.API.LogError("error while getting instance ID", err)
return "", "", err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding error log will help

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done i was add the logging

}

team, appErr := p.API.GetTeam(teamID)
if appErr != nil {
return "", "", appErr
}

channel, appErr := p.API.GetChannel(channelID)
if appErr != nil {
return "", "", appErr
}

v := url.Values{}
secret, _ := url.QueryUnescape(cf.Secret)
v.Add("secret", secret)
subURL = p.GetPluginURL() + instancePath(routeAPISubscribeWebhook, instanceID) + "?" + v.Encode()

// For the legacy URL, add team and channel. Secret is already in the map.
v.Add("team", team.Name)
v.Add("channel", channel.Name)
legacyURL = p.GetPluginURL() + instancePath(routeIncomingWebhook, instanceID) + "?" + v.Encode()

return subURL, legacyURL, nil
}
func newWebhook(jwh *JiraWebhook, eventType string, format string, args ...interface{}) *webhook {
return &webhook{
JiraWebhook: jwh,
eventTypes: NewStringSet(eventType),
headline: jwh.mdUser() + " " + fmt.Sprintf(format, args...) + " " + jwh.mdKeySummaryLink(),
}
}

func (p *Plugin) getConnection(instance Instance, notification webhookUserNotification) (con *Connection, err error) {
var mattermostUserID types.ID

// prefer accountId to username when looking up UserIds
if notification.jiraAccountID != "" {
mattermostUserID, err = p.userStore.LoadMattermostUserID(instance.GetID(), notification.jiraAccountID)
} else {
mattermostUserID, err = p.userStore.LoadMattermostUserID(instance.GetID(), notification.jiraUsername)
}
if err != nil {
return
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think error was declare as err in function return and it will return if it has error value

}

// Check if the user has permissions.
con, err = p.userStore.LoadConnection(instance.GetID(), mattermostUserID)
if err != nil {
p.API.LogError("error while load connection", err)
// Not connected to Jira, so can't check permissions
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems logging and returning error can help to understand better the situation

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done i was add the logging

}

return
}
39 changes: 37 additions & 2 deletions server/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
const (
settingOn = "on"
settingOff = "off"

errStoreNewSettings = "Could not store new settings. Please contact your system administrator. error: %v"
errConnectToJira = "Your username is not connected to Jira. Please type `/jira connect`. %v"
)

func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, mattermostUserID types.ID, connection *Connection, args []string) *model.CommandResponse {
Expand All @@ -34,13 +37,13 @@ func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, ma
connection.Settings.Notifications = value
if err := p.userStore.StoreConnection(instanceID, mattermostUserID, connection); err != nil {
p.errorf("settingsNotifications, err: %v", err)
p.responsef(header, "Could not store new settings. Please contact your system administrator. error: %v", err)
p.responsef(header, errStoreNewSettings, err)
}

// send back the actual value
updatedConnection, err := p.userStore.LoadConnection(instanceID, mattermostUserID)
if err != nil {
return p.responsef(header, "Your username is not connected to Jira. Please type `jira connect`. %v", err)
return p.responsef(header, errConnectToJira, err)
}
notifications := settingOff
if updatedConnection.Settings.Notifications {
Expand All @@ -49,3 +52,35 @@ func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, ma

return p.responsef(header, "Settings updated. Notifications %s.", notifications)
}

func (p *Plugin) settingsWatching(header *model.CommandArgs, instanceID, mattermostUserID types.ID, connection *Connection, args []string) *model.CommandResponse {
const helpText = "`/jira settings watching [value]`\\n* Invalid value. Accepted values are: `on` or `off`."

if len(args) != 2 {
return p.responsef(header, helpText)
}

wathing := false
wathingOnOff := settingOff

switch args[1] {
case settingOn:
wathing = true
wathingOnOff = settingOn
case settingOff:
wathing = false
default:
return p.responsef(header, helpText)
}

if connection.Settings == nil {
connection.Settings = &ConnectionSettings{}
}
connection.Settings.Watching = &wathing
if err := p.userStore.StoreConnection(instanceID, mattermostUserID, connection); err != nil {
p.errorf("settingsWatching, err: %v", err)
return p.responsef(header, errStoreNewSettings, err)
}

return p.responsef(header, "Settings updated. Watching %s.", wathingOnOff)
}
25 changes: 20 additions & 5 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,30 @@ func (c *Connection) JiraAccountID() types.ID {
}

type ConnectionSettings struct {
Notifications bool `json:"notifications"`
Notifications bool `json:"notifications"`
Watching *bool `json:"watching"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to update display logic of settings to include this new setting. It needs to included added here:

func (s *ConnectionSettings) String() string {
notifications := "off"
if s != nil && s.Notifications {
notifications = "on"
}
return fmt.Sprintf("\tNotifications: %s", notifications)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mickmister I've pushed a new commit for this change, could you please check it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR, can we also add separate settings for assignee and mention notifications, as explained here #705 (comment)? The Reporter notifications will not be included here however.

}

func (s *ConnectionSettings) String() string {
notifications := "off"
if s != nil && s.Notifications {
notifications = "on"
notifications, watching := settingOff, settingOff
if s != nil {
if s.Notifications {
notifications = "on"
}
if s.ShouldReceiveWatcherNotifications() {
watching = "on"
}
}
return fmt.Sprintf("- Notifications: %s\n\t- Watching: %s", notifications, watching)
}

func (s *ConnectionSettings) ShouldReceiveWatcherNotifications() bool {
if s.Watching != nil {
return *s.Watching
}
return fmt.Sprintf("\tNotifications: %s", notifications)

// Check old setting for backwards compatibility
return s.Notifications
}

func NewUser(mattermostUserID types.ID) *User {
Expand Down
4 changes: 2 additions & 2 deletions server/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ func TestUserSettings_String(t *testing.T) {
}{
"notifications on": {
settings: ConnectionSettings{Notifications: false},
expectedOutput: "\tNotifications: off",
expectedOutput: "- Notifications: off\n\t- Watching: off",
},
"notifications off": {
settings: ConnectionSettings{Notifications: true},
expectedOutput: "\tNotifications: on",
expectedOutput: "- Notifications: on\n\t- Watching: on",
},
}
for name, tt := range tests {
Expand Down
76 changes: 47 additions & 29 deletions server/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package main
import (
"fmt"
"net/http"
"net/url"

"github.com/pkg/errors"

Expand All @@ -26,6 +25,7 @@ type Webhook interface {
Events() StringSet
PostToChannel(p *Plugin, instanceID types.ID, channelID, fromUserID, subscriptionName string) (*model.Post, int, error)
PostNotifications(p *Plugin, instanceID types.ID) ([]*model.Post, int, error)
CheckIssueWatchers(p *Plugin, instanceID types.ID)
}

type webhookField struct {
Expand Down Expand Up @@ -161,41 +161,59 @@ func (wh *webhook) PostNotifications(p *Plugin, instanceID types.ID) ([]*model.P
return posts, http.StatusOK, nil
}

func newWebhook(jwh *JiraWebhook, eventType string, format string, args ...interface{}) *webhook {
return &webhook{
JiraWebhook: jwh,
eventTypes: NewStringSet(eventType),
headline: jwh.mdUser() + " " + fmt.Sprintf(format, args...) + " " + jwh.mdKeySummaryLink(),
func (wh *webhook) CheckIssueWatchers(p *Plugin, instanceID types.ID) {
instance, err := p.instanceStore.LoadInstance(instanceID)
if err != nil && instance == nil {
// This isn't an internal server error. There's just no instance installed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't an internal server error. There's just no instance installed.

How do you know this is true? It looks like there are several kinds of errors that can happen in LoadInstance. Can we make it so the LoadInstance function returns a nil error, and nil for instance, so we know whether it was a real error if there is no instance?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vicky-demansol This request hasn't been addressed yet the comment was marked as resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mickmister @vicky-demansol is doing just that. But if it returns nil, do we log it here

return
}
}

func (p *Plugin) GetWebhookURL(jiraURL string, teamID, channelID string) (subURL, legacyURL string, err error) {
cf := p.getConfig()

instanceID, err := p.ResolveWebhookInstanceURL(jiraURL)
c, err := p.getConnection(instance, webhookUserNotification{
jiraAccountID: wh.JiraWebhook.User.AccountID,
jiraUsername: wh.JiraWebhook.User.Name,
})
if err != nil {
return "", "", err
return
}

team, appErr := p.API.GetTeam(teamID)
if appErr != nil {
return "", "", appErr
client, err := instance.GetClient(c)
if err != nil {
return
}

channel, appErr := p.API.GetChannel(channelID)
if appErr != nil {
return "", "", appErr
watcherUsers, err := client.GetWatchers(wh.Issue.ID)
if err != nil {
return
}

v := url.Values{}
secret, _ := url.QueryUnescape(cf.Secret)
v.Add("secret", secret)
subURL = p.GetPluginURL() + instancePath(routeAPISubscribeWebhook, instanceID) + "?" + v.Encode()
for _, watcherUser := range *watcherUsers {
postType := ""
message := ""
var shouldNotReceiveNotification bool
for _, notification := range wh.notifications {
if notification.jiraAccountID == watcherUser.AccountID || (notification.jiraUsername != "" && notification.jiraUsername == watcherUser.Name) {
shouldNotReceiveNotification = true
break
}
postType = notification.postType
message = notification.message
jupriano marked this conversation as resolved.
Show resolved Hide resolved
}
if shouldNotReceiveNotification {
continue
}
whUserNotification := webhookUserNotification{
jiraUsername: watcherUser.Name,
jiraAccountID: watcherUser.AccountID,
message: message,
postType: postType,
commentSelf: wh.JiraWebhook.Comment.Self,
}

// For the legacy URL, add team and channel. Secret is already in the map.
v.Add("team", team.Name)
v.Add("channel", channel.Name)
legacyURL = p.GetPluginURL() + instancePath(routeIncomingWebhook, instanceID) + "?" + v.Encode()
c, err = p.getConnection(instance, whUserNotification)

return subURL, legacyURL, nil
// if setting watching value is false don't put into webhookUserNotification
if err != nil || c.Settings == nil || !c.Settings.ShouldReceiveWatcherNotifications() {
continue
}

wh.notifications = append(wh.notifications, whUserNotification)
}
}
2 changes: 2 additions & 0 deletions server/webhook_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func (ww webhookWorker) process(msg *webhookMessage) (err error) {
return err
}

wh.CheckIssueWatchers(ww.p, msg.InstanceID)

if _, _, err = wh.PostNotifications(ww.p, msg.InstanceID); err != nil {
ww.p.errorf("WebhookWorker id: %d, error posting notifications, err: %v", ww.id, err)
}
Expand Down