diff --git a/server/command.go b/server/command.go index 0b5d8f94c..dc5799d93 100644 --- a/server/command.go +++ b/server/command.go @@ -251,12 +251,30 @@ func createSettingsCommand(optInstance bool) *model.AutocompleteData { "list", "", "View your current settings") settings.AddCommand(list) + setting := []model.AutocompleteListItem{ + {HelpText: "Turn on notification on", Item: "on"}, + {HelpText: "Turn on notification off", Item: "off"}, + } + notifications := model.NewAutocompleteData( - "notifications", "[on|off]", "Update your user notifications settings") - notifications.AddStaticListArgument("value", true, []model.AutocompleteListItem{ - {HelpText: "Turn notifications on", Item: "on"}, - {HelpText: "Turn notifications off", Item: "off"}, - }) + "notifications", "[assinee|mention|reporter]", "manage notifications") + + assigneeNotifications := model.NewAutocompleteData( + subCommandAssignee, "", "manage assignee notifications") + assigneeNotifications.AddStaticListArgument("value", true, setting) + + mentionNotifications := model.NewAutocompleteData( + subCommandMention, "", "manage mention notifications") + mentionNotifications.AddStaticListArgument("value", true, setting) + + reporterNotifications := model.NewAutocompleteData( + subCommandReporter, "", "manage reporter notifications") + reporterNotifications.AddStaticListArgument("value", true, setting) + + notifications.AddCommand(assigneeNotifications) + notifications.AddCommand(mentionNotifications) + notifications.AddCommand(reporterNotifications) + withFlagInstance(notifications, optInstance, routeAutocompleteInstalledInstanceWithAlias) settings.AddCommand(notifications) @@ -367,10 +385,12 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, commandArgs *model.CommandArg if err != nil { return p.responsef(commandArgs, err.Error()), nil } + args := strings.Fields(commandArgs.Command) if len(args) == 0 || args[0] != "/jira" { return p.help(commandArgs), nil } + return jiraCommandHandler.Handle(p, c, commandArgs, args[1:]...), nil } @@ -583,7 +603,10 @@ func executeSettings(p *Plugin, c *plugin.Context, header *model.CommandArgs, ar switch args[0] { case "list": - return p.responsef(header, "Current settings:\n%s", conn.Settings.String()) + if conn.Settings != nil { + return p.responsef(header, "Current settings:\n%s", conn.Settings.String()) + } + return p.responsef(header, "Please connect to jira account `/jira connect`") case "notifications": return p.settingsNotifications(header, instance.GetID(), user.MattermostUserID, conn, args) default: diff --git a/server/command_test.go b/server/command_test.go index 01a1c307d..e7c5728c8 100644 --- a/server/command_test.go +++ b/server/command_test.go @@ -24,12 +24,13 @@ import ( ) const ( - mockUserIDWithNotifications = "1" - mockUserIDWithoutNotifications = "2" - mockUserIDUnknown = "3" - mockUserIDSysAdmin = "4" - mockUserIDNonSysAdmin = "5" - mattermostSiteURL = "https://somelink.com" + mockUserIDWithNotifications = "1" + mockMattermostIDWithNotifications = "testMattermostUserId012345" + mockUserIDWithoutNotifications = "2" + mockUserIDUnknown = "3" + mockUserIDSysAdmin = "4" + mockUserIDNonSysAdmin = "5" + mattermostSiteURL = "https://somelink.com" ) type mockUserStoreKV struct { @@ -69,8 +70,15 @@ func getMockUserStoreKV() mockUserStoreKV { }, } + value := true withNotifications := connection // copy - withNotifications.Settings = &ConnectionSettings{Notifications: true} + settings := &ConnectionSettings{ + Notifications: value, + SendNotificationsForMention: &value, + SendNotificationsForAssignee: &value, + SendNotificationsForReporter: &value, + } + withNotifications.Settings = settings return mockUserStoreKV{ users: map[types.ID]*User{ @@ -79,9 +87,10 @@ func getMockUserStoreKV() mockUserStoreKV { mockUserIDWithoutNotifications: newuser(mockUserIDWithoutNotifications), }, connections: map[types.ID]*Connection{ - mockUserIDWithNotifications: &withNotifications, - mockUserIDWithoutNotifications: &connection, - "connected_user": &connection, + mockUserIDWithNotifications: &withNotifications, + mockUserIDWithoutNotifications: &connection, + "connected_user": &connection, + mockMattermostIDWithNotifications: &withNotifications, }, } } @@ -165,12 +174,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\tNotifications Status:\n\t- Notifications : on \n\t- Notifications for assignee : on \n\t- Notifications for mention : on \n\t- Notifications for reporter : 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\tNotifications Status:\n\t- Notifications : off \n\t- Notifications for assignee : off \n\t- Notifications for mention : off \n\t- Notifications for reporter : off", }, "unknown setting": { commandArgs: &model.CommandArgs{Command: "/jira settings" + " test", UserId: mockUserIDWithoutNotifications}, @@ -180,22 +189,42 @@ func TestPlugin_ExecuteCommand_Settings(t *testing.T) { "set notifications without value": { commandArgs: &model.CommandArgs{Command: "/jira settings" + " notifications", UserId: mockUserIDWithoutNotifications}, numInstances: 1, - expectedMsg: "`/jira settings notifications [value]`\n* Invalid value. Accepted values are: `on` or `off`.", + expectedMsg: "`/jira settings notifications [assignee|mention|reporter] [value]`\n* Invalid value. Accepted values are: `on` or `off`.", }, "set notification with unknown value": { commandArgs: &model.CommandArgs{Command: "/jira settings notifications test", UserId: mockUserIDWithoutNotifications}, numInstances: 1, - expectedMsg: "`/jira settings notifications [value]`\n* Invalid value. Accepted values are: `on` or `off`.", + expectedMsg: "`/jira settings notifications [assignee|mention|reporter] [value]`\n* Invalid value. Accepted values are: `on` or `off`.", + }, + "enable assignee notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications assignee on", UserId: mockUserIDWithoutNotifications}, + numInstances: 1, + expectedMsg: "Settings updated.\n\tAssignee on.", + }, + "disable assignee notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications assignee off", UserId: mockUserIDWithNotifications}, + numInstances: 1, + expectedMsg: "Settings updated.\n\tAssignee off.", + }, + "enable reporter notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications reporter on", UserId: mockUserIDWithoutNotifications}, + numInstances: 1, + expectedMsg: "Settings updated.\n\tReporter on.", + }, + "disable reporter notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications reporter off", UserId: mockUserIDWithNotifications}, + numInstances: 1, + expectedMsg: "Settings updated.\n\tReporter off.", }, - "enable notifications": { - commandArgs: &model.CommandArgs{Command: "/jira settings notifications on", UserId: mockUserIDWithoutNotifications}, + "enable mention notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications mention on", UserId: mockUserIDWithoutNotifications}, numInstances: 1, - expectedMsg: "Settings updated. Notifications on.", + expectedMsg: "Settings updated.\n\tMention on.", }, - "disable notifications": { - commandArgs: &model.CommandArgs{Command: "/jira settings notifications off", UserId: mockUserIDWithNotifications}, + "disable mention notifications": { + commandArgs: &model.CommandArgs{Command: "/jira settings notifications mention off", UserId: mockUserIDWithNotifications}, numInstances: 1, - expectedMsg: "Settings updated. Notifications off.", + expectedMsg: "Settings updated.\n\tMention off.", }, } for name, tt := range tests { diff --git a/server/kv_mock_test.go b/server/kv_mock_test.go index 51b142f94..8e684b224 100644 --- a/server/kv_mock_test.go +++ b/server/kv_mock_test.go @@ -79,10 +79,17 @@ func (store mockUserStore) StoreConnection(types.ID, types.ID, *Connection) erro return nil } func (store mockUserStore) LoadConnection(types.ID, types.ID) (*Connection, error) { - return &Connection{}, nil + valueTrue := true + return &Connection{ + Settings: &ConnectionSettings{ + SendNotificationsForMention: &valueTrue, + SendNotificationsForAssignee: &valueTrue, + SendNotificationsForReporter: &valueTrue, + }, + }, nil } func (store mockUserStore) LoadMattermostUserID(instanceID types.ID, jiraUserName string) (types.ID, error) { - return "testMattermostUserId012345", nil + return mockMattermostIDWithNotifications, nil } func (store mockUserStore) DeleteConnection(instanceID, mattermostUserID types.ID) error { return nil diff --git a/server/settings.go b/server/settings.go index a3fa3dfe2..69cb875cc 100644 --- a/server/settings.go +++ b/server/settings.go @@ -1,6 +1,8 @@ package main import ( + "strings" + "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-plugin-jira/server/utils/types" @@ -9,17 +11,21 @@ import ( const ( settingOn = "on" settingOff = "off" + + subCommandAssignee = "assignee" + subCommandMention = "mention" + subCommandReporter = "reporter" ) func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, mattermostUserID types.ID, connection *Connection, args []string) *model.CommandResponse { - const helpText = "`/jira settings notifications [value]`\n* Invalid value. Accepted values are: `on` or `off`." + const helpText = "`/jira settings notifications [assignee|mention|reporter] [value]`\n* Invalid value. Accepted values are: `on` or `off`." - if len(args) != 2 { + if len(args) != 3 { return p.responsef(header, helpText) } var value bool - switch args[1] { + switch args[2] { case settingOn: value = true case settingOff: @@ -31,7 +37,17 @@ func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, ma if connection.Settings == nil { connection.Settings = &ConnectionSettings{} } - connection.Settings.Notifications = value + switch args[1] { + case subCommandAssignee: + connection.Settings.SendNotificationsForAssignee = &value + case subCommandMention: + connection.Settings.SendNotificationsForMention = &value + case subCommandReporter: + connection.Settings.SendNotificationsForReporter = &value + default: + return p.responsef(header, helpText) + } + 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) @@ -43,9 +59,20 @@ func (p *Plugin) settingsNotifications(header *model.CommandArgs, instanceID, ma return p.responsef(header, "Your username is not connected to Jira. Please type `jira connect`. %v", err) } notifications := settingOff - if updatedConnection.Settings.Notifications { - notifications = settingOn + switch args[1] { + case subCommandAssignee: + if *updatedConnection.Settings.SendNotificationsForAssignee { + notifications = settingOn + } + case subCommandMention: + if *updatedConnection.Settings.SendNotificationsForMention { + notifications = settingOn + } + case subCommandReporter: + if *updatedConnection.Settings.SendNotificationsForReporter { + notifications = settingOn + } } - return p.responsef(header, "Settings updated. Notifications %s.", notifications) + return p.responsef(header, "Settings updated.\n\t%s %s.", strings.Title(args[1]), notifications) } diff --git a/server/subscribe.go b/server/subscribe.go index 0792f9048..5289a07f4 100644 --- a/server/subscribe.go +++ b/server/subscribe.go @@ -24,11 +24,10 @@ 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" MaxSubscriptionNameLength = 100 ) diff --git a/server/subscribe_test.go b/server/subscribe_test.go index c9ce769a4..a3a539acd 100644 --- a/server/subscribe_test.go +++ b/server/subscribe_test.go @@ -1381,8 +1381,10 @@ func TestGetChannelsSubscribed(t *testing.T) { r := bytes.NewReader(data) bb, err := ioutil.ReadAll(r) require.Nil(t, err) - - wh, err := ParseWebhook(bb) + instanceID := testInstance1.InstanceID + p := &Plugin{} + p.SetAPI(api) + wh, err := ParseWebhook(bb, p, instanceID) assert.Nil(t, err) actual, err := p.getChannelsSubscribed(wh.(*webhook), testInstance1.InstanceID) diff --git a/server/user.go b/server/user.go index e25d5b1c5..67ab71bb3 100644 --- a/server/user.go +++ b/server/user.go @@ -43,15 +43,35 @@ func (c *Connection) JiraAccountID() types.ID { } type ConnectionSettings struct { - Notifications bool `json:"notifications"` + Notifications bool `json:"notifications"` + SendNotificationsForMention *bool `json:"send_notifications_for_mention"` + SendNotificationsForAssignee *bool `json:"send_notifications_for_assignee"` + SendNotificationsForReporter *bool `json:"send_notifications_for_reporter"` } func (s *ConnectionSettings) String() string { - notifications := "off" + assigneeNotifications := "Notifications for assignee : off" + mentionNotifications := "Notifications for mention : off" + reporterNotifications := "Notifications for reporter : off" + notifications := "Notifications : off" + + if s != nil && *s.SendNotificationsForAssignee { + assigneeNotifications = "Notifications for assignee : on" + } + + if s != nil && *s.SendNotificationsForMention { + mentionNotifications = "Notifications for mention : on" + } + + if s != nil && *s.SendNotificationsForReporter { + reporterNotifications = "Notifications for reporter : on" + } + if s != nil && s.Notifications { - notifications = "on" + notifications = "Notifications : on" } - return fmt.Sprintf("\tNotifications: %s", notifications) + + return fmt.Sprintf("\tNotifications Status:\n\t- %s \n\t- %s \n\t- %s \n\t- %s", notifications, assigneeNotifications, mentionNotifications, reporterNotifications) } func NewUser(mattermostUserID types.ID) *User { diff --git a/server/user_cloud.go b/server/user_cloud.go index 4fdd53832..ebcf0d5f5 100644 --- a/server/user_cloud.go +++ b/server/user_cloud.go @@ -92,6 +92,7 @@ func (p *Plugin) httpACUserInteractive(w http.ResponseWriter, r *http.Request, i } mmToken := r.FormValue(argMMToken) + value := true connection := &Connection{ PluginVersion: manifest.Version, User: jira.User{ @@ -102,7 +103,9 @@ func (p *Plugin) httpACUserInteractive(w http.ResponseWriter, r *http.Request, i }, // Set default settings the first time a user connects Settings: &ConnectionSettings{ - Notifications: true, + SendNotificationsForMention: &value, + SendNotificationsForAssignee: &value, + SendNotificationsForReporter: &value, }, } diff --git a/server/user_server.go b/server/user_server.go index f5fa2b9fb..b68ada271 100644 --- a/server/user_server.go +++ b/server/user_server.go @@ -102,9 +102,13 @@ func (p *Plugin) httpOAuth1aComplete(w http.ResponseWriter, r *http.Request, ins return http.StatusInternalServerError, err } connection.User = *juser - + value := true // Set default settings the first time a user connects - connection.Settings = &ConnectionSettings{Notifications: true} + connection.Settings = &ConnectionSettings{ + SendNotificationsForMention: &value, + SendNotificationsForAssignee: &value, + SendNotificationsForReporter: &value, + } err = p.connectUser(instance, types.ID(mattermostUserID), connection) if err != nil { diff --git a/server/user_test.go b/server/user_test.go index b26761de6..dd03eb1e1 100644 --- a/server/user_test.go +++ b/server/user_test.go @@ -11,17 +11,29 @@ import ( ) func TestUserSettings_String(t *testing.T) { + valueTrue := true + valueFalse := false tests := map[string]struct { settings ConnectionSettings expectedOutput string }{ "notifications on": { - settings: ConnectionSettings{Notifications: false}, - expectedOutput: "\tNotifications: off", + settings: ConnectionSettings{ + Notifications: valueTrue, + SendNotificationsForMention: &valueTrue, + SendNotificationsForAssignee: &valueTrue, + SendNotificationsForReporter: &valueTrue, + }, + expectedOutput: "\tNotifications Status:\n\t- Notifications : on \n\t- Notifications for assignee : on \n\t- Notifications for mention : on \n\t- Notifications for reporter : on", }, "notifications off": { - settings: ConnectionSettings{Notifications: true}, - expectedOutput: "\tNotifications: on", + settings: ConnectionSettings{ + Notifications: valueFalse, + SendNotificationsForMention: &valueFalse, + SendNotificationsForAssignee: &valueFalse, + SendNotificationsForReporter: &valueFalse, + }, + expectedOutput: "\tNotifications Status:\n\t- Notifications : off \n\t- Notifications for assignee : off \n\t- Notifications for mention : off \n\t- Notifications for reporter : off", }, } for name, tt := range tests { diff --git a/server/utils.go b/server/utils.go index c414d73f7..39d9bea69 100644 --- a/server/utils.go +++ b/server/utils.go @@ -29,7 +29,7 @@ func (p *Plugin) CreateBotDMPost(instanceID, mattermostUserID types.ID, message, // not connected to Jira, so no need to send a DM, and no need to report an error return nil, nil } - if c.Settings == nil || !c.Settings.Notifications { + if c.Settings == nil { return nil, nil } diff --git a/server/webhook.go b/server/webhook.go index f684dea0f..3a6487736 100644 --- a/server/webhook.go +++ b/server/webhook.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" + "github.com/andygrunwald/go-jira" "github.com/pkg/errors" "github.com/mattermost/mattermost-server/v6/model" @@ -120,6 +121,7 @@ func (wh *webhook) PostNotifications(p *Plugin, instanceID types.ID) ([]*model.P } else { mattermostUserID, err = p.userStore.LoadMattermostUserID(instance.GetID(), notification.jiraUsername) } + if err != nil { continue } @@ -130,6 +132,7 @@ func (wh *webhook) PostNotifications(p *Plugin, instanceID types.ID) ([]*model.P // Not connected to Jira, so can't check permissions continue } + client, err2 := instance.GetClient(c) if err2 != nil { p.errorf("PostNotifications: error while getting jiraClient, err: %v", err2) @@ -144,6 +147,7 @@ func (wh *webhook) PostNotifications(p *Plugin, instanceID types.ID) ([]*model.P } else { _, err = client.GetIssue(wh.Issue.ID, nil) } + if err != nil { p.errorf("PostNotifications: failed to get self: %v", err) continue @@ -199,6 +203,102 @@ func (p *Plugin) GetWebhookURL(jiraURL string, teamID, channelID string) (subURL return subURL, legacyURL, nil } +func (wh *webhook) applyReporterNotification(p *Plugin, instanceID types.ID, reporter *jira.User) { + if !wh.eventTypes.ContainsAny("event_created_comment") { + return + } + + jwhook := wh.JiraWebhook + if reporter == nil || + (reporter.Name != "" && reporter.Name == jwhook.User.Name) || + (reporter.AccountID != "" && reporter.AccountID == jwhook.Comment.UpdateAuthor.AccountID) { + return + } + + if wh.checkNotificationAlreadyExist(reporter.Name, reporter.AccountID) { + return + } + + commentAuthor := mdUser(&jwhook.Comment.UpdateAuthor) + + commentMessage := fmt.Sprintf("%s **commented** on %s:\n>%s", commentAuthor, jwhook.mdKeySummaryLink(), jwhook.Comment.Body) + + c, err := wh.GetUserSetting(p, instanceID, reporter.Name, reporter.AccountID) + if err != nil || c.Settings == nil || !c.Settings.ShouldReceiveReporterNotifications() { + return + } + + wh.notifications = append(wh.notifications, webhookUserNotification{ + jiraUsername: reporter.Name, + jiraAccountID: reporter.AccountID, + message: commentMessage, + postType: PostTypeComment, + commentSelf: jwhook.Comment.Self, + }) +} + +func (wh *webhook) checkNotificationAlreadyExist(username, accountID string) bool { + for _, val := range wh.notifications { + if val.jiraUsername == username && val.jiraAccountID == accountID { + return true + } + } + + return false +} + +func (wh *webhook) GetUserSetting(p *Plugin, instanceID types.ID, jiraAccountID, jiraUsername string) (*Connection, error) { + var err error + instance, err := p.instanceStore.LoadInstance(instanceID) + if err != nil { + return nil, err + } + var mattermostUserID types.ID + if jiraAccountID != "" { + mattermostUserID, err = p.userStore.LoadMattermostUserID(instance.GetID(), jiraAccountID) + } else { + mattermostUserID, err = p.userStore.LoadMattermostUserID(instance.GetID(), jiraUsername) + } + + if err != nil { + return nil, err + } + + c, err := p.userStore.LoadConnection(instanceID, mattermostUserID) + if err != nil { + return nil, err + } + + return c, nil +} + +func (s *ConnectionSettings) ShouldReceiveAssigneeNotifications() bool { + if s.SendNotificationsForAssignee != nil { + return *s.SendNotificationsForAssignee + } + + // Check old setting for backwards compatibility + return s.Notifications +} + +func (s *ConnectionSettings) ShouldReceiveReporterNotifications() bool { + if s.SendNotificationsForReporter != nil { + return *s.SendNotificationsForReporter + } + + // Check old setting for backwards compatibility + return s.Notifications +} + +func (s *ConnectionSettings) ShouldReceiveMentionNotifications() bool { + if s.SendNotificationsForMention != nil { + return *s.SendNotificationsForMention + } + + // Check old setting for backwards compatibility + return s.Notifications +} + func (p *Plugin) getSubscriptionsWebhookURL(instanceID types.ID) string { cf := p.getConfig() v := url.Values{} diff --git a/server/webhook_http.go b/server/webhook_http.go index f818c254f..708b30ce1 100644 --- a/server/webhook_http.go +++ b/server/webhook_http.go @@ -94,7 +94,7 @@ func (p *Plugin) httpWebhook(w http.ResponseWriter, r *http.Request, instanceID return respondErr(w, appErr.StatusCode, appErr) } - wh, err := ParseWebhook(bb) + wh, err := ParseWebhook(bb, p, instanceID) if err == ErrWebhookIgnored { return respondErr(w, http.StatusOK, err) } diff --git a/server/webhook_parser.go b/server/webhook_parser.go index ad1dfc71a..cf50da755 100644 --- a/server/webhook_parser.go +++ b/server/webhook_parser.go @@ -13,12 +13,14 @@ import ( "github.com/pkg/errors" + "github.com/mattermost/mattermost-plugin-jira/server/utils/types" + "github.com/mattermost/mattermost-server/v6/model" ) var webhookWrapperFunc func(wh Webhook) Webhook -func ParseWebhook(bb []byte) (wh Webhook, err error) { +func ParseWebhook(bb []byte, p *Plugin, instanceID types.ID) (wh Webhook, err error) { defer func() { if err == nil || err == ErrWebhookIgnored { return @@ -51,28 +53,28 @@ func ParseWebhook(bb []byte) (wh Webhook, err error) { switch jwh.WebhookEvent { case "jira:issue_created": - wh = parseWebhookCreated(jwh) + wh = parseWebhookCreated(jwh, p, instanceID) case "jira:issue_deleted": wh = parseWebhookDeleted(jwh) case "jira:issue_updated": switch jwh.IssueEventTypeName { case "issue_assigned": - wh = parseWebhookAssigned(jwh, jwh.ChangeLog.Items[0].FromString, jwh.ChangeLog.Items[0].ToString) + wh = parseWebhookAssigned(jwh, p, instanceID, jwh.ChangeLog.Items[0].FromString, jwh.ChangeLog.Items[0].ToString) case "issue_updated", "issue_generic", "issue_resolved", "issue_closed", "issue_work_started", "issue_reopened": - wh = parseWebhookChangeLog(jwh) + wh = parseWebhookChangeLog(jwh, p, instanceID) case "issue_commented": - wh, err = parseWebhookCommentCreated(jwh) + wh, err = parseWebhookCommentCreated(jwh, instanceID, p) case "issue_comment_edited": - wh, err = parseWebhookCommentUpdated(jwh) + wh, err = parseWebhookCommentUpdated(jwh, p, instanceID) case "issue_comment_deleted": wh, err = parseWebhookCommentDeleted(jwh) default: - wh, err = parseWebhookUnspecified(jwh) + wh, err = parseWebhookUnspecified(jwh, p, instanceID) } case commentCreated: - wh, err = parseWebhookCommentCreated(jwh) + wh, err = parseWebhookCommentCreated(jwh, instanceID, p) case commentUpdated: - wh, err = parseWebhookCommentUpdated(jwh) + wh, err = parseWebhookCommentUpdated(jwh, p, instanceID) case commentDeleted: wh, err = parseWebhookCommentDeleted(jwh) default: @@ -84,7 +86,6 @@ func ParseWebhook(bb []byte) (wh Webhook, err error) { if wh == nil { return nil, errors.Errorf("Unsupported webhook data: %v", jwh.WebhookEvent) } - // For HTTP testing, so we can capture the output of the interface if webhookWrapperFunc != nil { wh = webhookWrapperFunc(wh) @@ -93,22 +94,22 @@ func ParseWebhook(bb []byte) (wh Webhook, err error) { return wh, nil } -func parseWebhookUnspecified(jwh *JiraWebhook) (Webhook, error) { +func parseWebhookUnspecified(jwh *JiraWebhook, p *Plugin, instanceID types.ID) (Webhook, error) { if len(jwh.ChangeLog.Items) > 0 { - return parseWebhookChangeLog(jwh), nil + return parseWebhookChangeLog(jwh, p, instanceID), nil } if jwh.Comment.ID != "" { if jwh.Comment.Updated == jwh.Comment.Created { - return parseWebhookCommentCreated(jwh) + return parseWebhookCommentCreated(jwh, instanceID, p) } - return parseWebhookCommentUpdated(jwh) + return parseWebhookCommentUpdated(jwh, p, instanceID) } return nil, errors.Errorf("Unsupported webhook event: %v", jwh.WebhookEvent) } -func parseWebhookChangeLog(jwh *JiraWebhook) Webhook { +func parseWebhookChangeLog(jwh *JiraWebhook, p *Plugin, instanceID types.ID) Webhook { var events []*webhook for _, item := range jwh.ChangeLog.Items { field := item.Field @@ -151,7 +152,7 @@ func parseWebhookChangeLog(jwh *JiraWebhook) Webhook { case field == labelsField: event = parseWebhookUpdatedLabels(jwh, from, to, fromWithDefault, toWithDefault) case field == "assignee": - event = parseWebhookAssigned(jwh, from, to) + event = parseWebhookAssigned(jwh, p, instanceID, from, to) case field == "issuetype": event = parseWebhookUpdatedField(jwh, eventUpdatedIssuetype, field, fieldID, fromWithDefault, toWithDefault) case field == "Fix Version": @@ -182,7 +183,7 @@ func parseWebhookChangeLog(jwh *JiraWebhook) Webhook { } } -func parseWebhookCreated(jwh *JiraWebhook) Webhook { +func parseWebhookCreated(jwh *JiraWebhook, p *Plugin, instanceID types.ID) Webhook { wh := newWebhook(jwh, eventCreated, "**created**") wh.text = jwh.mdIssueDescription() @@ -209,7 +210,7 @@ func parseWebhookCreated(jwh *JiraWebhook) Webhook { wh.fields = fields } - appendNotificationForAssignee(wh) + appendNotificationForAssignee(wh, p, instanceID) return wh } @@ -222,7 +223,7 @@ func parseWebhookDeleted(jwh *JiraWebhook) Webhook { return wh } -func parseWebhookCommentCreated(jwh *JiraWebhook) (Webhook, error) { +func parseWebhookCommentCreated(jwh *JiraWebhook, instanceID types.ID, p *Plugin) (Webhook, error) { // The "comment_xxx" events from Jira Server come incomplete, // i.e. with just minimal metadata. We toss them out since they // are rather useless for our use case. Instead the Jira server @@ -243,13 +244,13 @@ func parseWebhookCommentCreated(jwh *JiraWebhook) (Webhook, error) { text: truncate(quoteIssueComment(jwh.Comment.Body), 3000), } - appendCommentNotifications(wh, "**mentioned** you in a new comment on") + appendCommentNotifications(wh, instanceID, p, "**mentioned** you in a new comment on") return wh, nil } // appendCommentNotifications modifies wh -func appendCommentNotifications(wh *webhook, verb string) { +func appendCommentNotifications(wh *webhook, instanceID types.ID, p *Plugin, verb string) { jwh := wh.JiraWebhook commentAuthor := mdUser(&jwh.Comment.UpdateAuthor) @@ -286,6 +287,11 @@ func appendCommentNotifications(wh *webhook, verb string) { notification.jiraUsername = u } + c, err := wh.GetUserSetting(p, instanceID, notification.jiraUsername, notification.jiraAccountID) + if err != nil || c.Settings == nil || !c.Settings.ShouldReceiveMentionNotifications() { + continue + } + wh.notifications = append(wh.notifications, notification) } @@ -298,6 +304,11 @@ func appendCommentNotifications(wh *webhook, verb string) { return } + c, err := wh.GetUserSetting(p, instanceID, jwh.Issue.Fields.Assignee.Name, jwh.Issue.Fields.Assignee.AccountID) + if err != nil || c.Settings == nil || !c.Settings.ShouldReceiveAssigneeNotifications() { + return + } + wh.notifications = append(wh.notifications, webhookUserNotification{ jiraUsername: jwh.Issue.Fields.Assignee.Name, jiraAccountID: jwh.Issue.Fields.Assignee.AccountID, @@ -334,7 +345,7 @@ func parseWebhookCommentDeleted(jwh *JiraWebhook) (Webhook, error) { }, nil } -func parseWebhookCommentUpdated(jwh *JiraWebhook) (Webhook, error) { +func parseWebhookCommentUpdated(jwh *JiraWebhook, p *Plugin, instanceID types.ID) (Webhook, error) { if jwh.Issue.ID == "" { return nil, ErrWebhookIgnored } @@ -349,7 +360,7 @@ func parseWebhookCommentUpdated(jwh *JiraWebhook) (Webhook, error) { return wh, nil } -func parseWebhookAssigned(jwh *JiraWebhook, from, to string) *webhook { +func parseWebhookAssigned(jwh *JiraWebhook, p *Plugin, instanceID types.ID, from, to string) *webhook { wh := newWebhook(jwh, eventUpdatedAssignee, "**assigned** %s to", jwh.mdIssueAssignee()) fromFixed := from if fromFixed == "" { @@ -361,13 +372,13 @@ func parseWebhookAssigned(jwh *JiraWebhook, from, to string) *webhook { } wh.fieldInfo = webhookField{"assignee", "assignee", fromFixed, toFixed} - appendNotificationForAssignee(wh) + appendNotificationForAssignee(wh, p, instanceID) return wh } // appendNotificationForAssignee modifies wh -func appendNotificationForAssignee(wh *webhook) { +func appendNotificationForAssignee(wh *webhook, p *Plugin, instanceID types.ID) { jwh := wh.JiraWebhook if jwh.Issue.Fields.Assignee == nil { return @@ -379,6 +390,11 @@ func appendNotificationForAssignee(wh *webhook) { return } + c, err := wh.GetUserSetting(p, instanceID, jwh.Issue.Fields.Assignee.Name, jwh.Issue.Fields.Assignee.AccountID) + if err != nil || c.Settings == nil || !c.Settings.ShouldReceiveAssigneeNotifications() { + return + } + wh.notifications = append(wh.notifications, webhookUserNotification{ jiraUsername: jwh.Issue.Fields.Assignee.Name, jiraAccountID: jwh.Issue.Fields.Assignee.AccountID, diff --git a/server/webhook_parser_misc_test.go b/server/webhook_parser_misc_test.go index 2a0334ae3..2ede8bb5e 100644 --- a/server/webhook_parser_misc_test.go +++ b/server/webhook_parser_misc_test.go @@ -10,6 +10,7 @@ import ( "testing" jira "github.com/andygrunwald/go-jira" + "github.com/mattermost/mattermost-server/v6/plugin/plugintest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,7 +21,13 @@ func TestMarkdown(t *testing.T) { defer f.Close() bb, err := ioutil.ReadAll(f) require.Nil(t, err) - wh, err := ParseWebhook(bb) + instanceID := testInstance1.InstanceID + p := &Plugin{} + api := &plugintest.API{} + p.SetAPI(api) + p.instanceStore = p.getMockInstanceStoreKV(1) + p.userStore = getMockUserStoreKV() + wh, err := ParseWebhook(bb, p, instanceID) require.NoError(t, err) w := wh.(*webhook) require.NotNil(t, w) @@ -50,7 +57,13 @@ func TestEventTypeFormat(t *testing.T) { defer f.Close() bb, err := ioutil.ReadAll(f) require.Nil(t, err) - wh, err := ParseWebhook(bb) + instanceID := testInstance1.InstanceID + p := &Plugin{} + api := &plugintest.API{} + p.SetAPI(api) + p.instanceStore = p.getMockInstanceStoreKV(1) + p.userStore = getMockUserStoreKV() + wh, err := ParseWebhook(bb, p, instanceID) require.NoError(t, err) w := wh.(*webhook) require.NotNil(t, w) @@ -70,7 +83,13 @@ func TestNotificationsFormat(t *testing.T) { defer f.Close() bb, err := ioutil.ReadAll(f) require.Nil(t, err) - wh, err := ParseWebhook(bb) + instanceID := testInstance1.InstanceID + p := &Plugin{} + api := &plugintest.API{} + p.SetAPI(api) + p.instanceStore = p.getMockInstanceStoreKV(1) + p.userStore = getMockUserStoreKV() + wh, err := ParseWebhook(bb, p, instanceID) require.NoError(t, err) w := wh.(*webhook) require.NotNil(t, w) @@ -129,7 +148,13 @@ func TestWebhookQuotedComment(t *testing.T) { defer f.Close() bb, err := ioutil.ReadAll(f) require.Nil(t, err) - wh, err := ParseWebhook(bb) + instanceID := testInstance1.InstanceID + p := &Plugin{} + api := &plugintest.API{} + p.SetAPI(api) + p.instanceStore = p.getMockInstanceStoreKV(1) + p.userStore = getMockUserStoreKV() + wh, err := ParseWebhook(bb, p, instanceID) require.NoError(t, err) w := wh.(*webhook) require.NotNil(t, w) diff --git a/server/webhook_worker.go b/server/webhook_worker.go index a008e665f..aedd3ae18 100644 --- a/server/webhook_worker.go +++ b/server/webhook_worker.go @@ -35,20 +35,22 @@ func (ww webhookWorker) process(msg *webhookMessage) (err error) { } }() - wh, err := ParseWebhook(msg.Data) + wh, err := ParseWebhook(msg.Data, ww.p, msg.InstanceID) if err != nil { return err } - if _, _, err = wh.PostNotifications(ww.p, msg.InstanceID); err != nil { - ww.p.errorf("WebhookWorker id: %d, error posting notifications, err: %v", ww.id, err) - } - v := wh.(*webhook) if err = v.JiraWebhook.expandIssue(ww.p, msg.InstanceID); err != nil { return err } + v.applyReporterNotification(ww.p, msg.InstanceID, v.Issue.Fields.Reporter) + + if _, _, err = wh.PostNotifications(ww.p, msg.InstanceID); err != nil { + ww.p.errorf("WebhookWorker id: %d, error posting notifications, err: %v", ww.id, err) + } + channelsSubscribed, err := ww.p.getChannelsSubscribed(v, msg.InstanceID) if err != nil { return err