From 37094acfb3a20d02ab6e25139e7377e898c0d765 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 11 Aug 2016 17:25:08 +0100 Subject: [PATCH 1/3] Implement JIRA issue expanding --- .../matrix-org/go-neb/services/jira/jira.go | 235 ++++++++++++------ 1 file changed, 164 insertions(+), 71 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/jira/jira.go b/src/github.com/matrix-org/go-neb/services/jira/jira.go index a9a7100..3900fc3 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/services/jira/jira.go @@ -11,6 +11,7 @@ import ( "github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/realms/jira" "github.com/matrix-org/go-neb/types" + "html" "net/http" "regexp" "strings" @@ -21,87 +22,165 @@ var issueKeyRegex = regexp.MustCompile("([A-z]+)-([0-9]+)") var projectKeyRegex = regexp.MustCompile("^[A-z]+$") type jiraService struct { - id string - UserID string - Rooms []string + id string + BotUserID string + ClientUserID string + Rooms map[string]struct { // room_id => {} + RealmID string // Determines the JIRA endpoint + Projects map[string]struct { // SYN => {} + Expand bool + Track bool + } + } } -func (s *jiraService) ServiceUserID() string { return s.UserID } -func (s *jiraService) ServiceID() string { return s.id } -func (s *jiraService) ServiceType() string { return "jira" } -func (s *jiraService) RoomIDs() []string { return s.Rooms } +func (s *jiraService) ServiceUserID() string { return s.BotUserID } +func (s *jiraService) ServiceID() string { return s.id } +func (s *jiraService) ServiceType() string { return "jira" } +func (s *jiraService) RoomIDs() []string { + var keys []string + for k := range s.Rooms { + keys = append(keys, k) + } + return keys +} func (s *jiraService) Register() error { return nil } func (s *jiraService) PostRegister(old types.Service) {} + +func (s *jiraService) cmdJiraCreate(roomID, userID string, args []string) (interface{}, error) { + // E.g jira create PROJ "Issue title" "Issue desc" + if len(args) <= 1 { + return nil, errors.New("Missing project key (e.g 'ABC') and/or title") + } + + if !projectKeyRegex.MatchString(args[0]) { + return nil, errors.New("Project key must only contain A-Z.") + } + + pkey := strings.ToUpper(args[0]) // REST API complains if they are not ALL CAPS + + title := args[1] + desc := "" + if len(args) == 3 { + desc = args[2] + } else if len(args) > 3 { // > 3 args is probably a title without quote marks + joinedTitle := strings.Join(args[1:], " ") + title = joinedTitle + } + + r, err := s.projectToRealm(userID, pkey) + if err != nil { + log.WithError(err).Print("Failed to map project key to realm") + return nil, errors.New("Failed to map project key to a JIRA endpoint.") + } + if r == nil { + return nil, errors.New("No known project exists with that project key.") + } + + iss := jira.Issue{ + Fields: &jira.IssueFields{ + Summary: title, + Description: desc, + Project: jira.Project{ + Key: pkey, + }, + // FIXME: This may vary depending on the JIRA install! + Type: jira.IssueType{ + Name: "Bug", + }, + }, + } + cli, err := r.JIRAClient(userID, false) + if err != nil { + return nil, err + } + i, res, err := cli.Issue.Create(&iss) + if err != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "user_id": userID, + "project": pkey, + "realm_id": r.ID(), + }).Print("Failed to create issue") + return nil, errors.New("Failed to create issue") + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + return nil, fmt.Errorf("Failed to create issue: JIRA returned %d", res.StatusCode) + } + + return &matrix.TextMessage{ + "m.notice", + fmt.Sprintf("Created issue: %sbrowse/%s", r.JIRAEndpoint, i.Key), + }, nil +} + +func (s *jiraService) expandIssue(roomID, userID, issueKey string) interface{} { + issueKey = strings.ToUpper(issueKey) + logger := log.WithField("issue_key", issueKey) + // [ISSU-123, ISSU, 123] + groups := issueKeyRegex.FindStringSubmatch(issueKey) + if len(groups) != 3 { + logger.Print("Failed to find issue key") + return nil + } + + projectKey := groups[1] + if !s.Rooms[roomID].Projects[projectKey].Expand { + return nil + } + + // Use the person who *provisioned* the service to check for project keys + // rather than the person who mentioned the issue key, as it is unlikely + // some random who mentioned the issue will have the intended auth. + r, err := s.projectToRealm(s.ClientUserID, projectKey) + if err != nil { + logger.WithError(err).Print("Failed to map project key to realm") + return nil + } + if r == nil { + logger.Print("No known project exists with that project key.") + return nil + } + + logger.WithField("room_id", roomID).Print("Expanding issue") + cli, err := r.JIRAClient(s.ClientUserID, false) + if err != nil { + logger.WithFields(log.Fields{ + log.ErrorKey: err, + "user_id": s.ClientUserID, + }).Print("Failed to retrieve client") + return nil + } + + issue, _, err := cli.Issue.Get(issueKey) + if err != nil { + logger.WithError(err).Print("Failed to GET issue") + return err + } + return matrix.GetHTMLMessage( + "m.notice", + fmt.Sprintf( + "%sbrowse/%s : %s", + r.JIRAEndpoint, issueKey, htmlSummaryForIssue(issue), + ), + ) +} + func (s *jiraService) Plugin(roomID string) plugin.Plugin { return plugin.Plugin{ Commands: []plugin.Command{ plugin.Command{ Path: []string{"jira", "create"}, Command: func(roomID, userID string, args []string) (interface{}, error) { - // E.g jira create PROJ "Issue title" "Issue desc" - if len(args) <= 1 { - return nil, errors.New("Missing project key (e.g 'ABC') and/or title") - } - - if !projectKeyRegex.MatchString(args[0]) { - return nil, errors.New("Project key must only contain A-Z.") - } - - pkey := strings.ToUpper(args[0]) // REST API complains if they are not ALL CAPS - - title := args[1] - desc := "" - if len(args) == 3 { - desc = args[2] - } else if len(args) > 3 { // > 3 args is probably a title without quote marks - joinedTitle := strings.Join(args[1:], " ") - title = joinedTitle - } - - r, err := s.projectToRealm(userID, pkey) - if err != nil { - log.WithError(err).Print("Failed to map project key to realm") - return nil, errors.New("Failed to map project key to a JIRA endpoint.") - } - if r == nil { - return nil, errors.New("No known project exists with that project key.") - } - - iss := jira.Issue{ - Fields: &jira.IssueFields{ - Summary: title, - Description: desc, - Project: jira.Project{ - Key: pkey, - }, - // FIXME: This may vary depending on the JIRA install! - Type: jira.IssueType{ - Name: "Bug", - }, - }, - } - cli, err := r.JIRAClient(userID, false) - if err != nil { - return nil, err - } - i, res, err := cli.Issue.Create(&iss) - if err != nil { - log.WithFields(log.Fields{ - log.ErrorKey: err, - "user_id": userID, - "project": pkey, - "realm_id": r.ID(), - }).Print("Failed to create issue") - return nil, errors.New("Failed to create issue") - } - if res.StatusCode < 200 || res.StatusCode >= 300 { - return nil, fmt.Errorf("Failed to create issue: JIRA returned %d", res.StatusCode) - } - - return &matrix.TextMessage{ - "m.notice", - fmt.Sprintf("Created issue: %s", i.Key), - }, nil + return s.cmdJiraCreate(roomID, userID, args) + }, + }, + }, + Expansions: []plugin.Expansion{ + plugin.Expansion{ + Regexp: issueKeyRegex, + Expand: func(roomID, userID, issueKey string) interface{} { + return s.expandIssue(roomID, userID, issueKey) }, }, }, @@ -174,6 +253,20 @@ func (s *jiraService) projectToRealm(userID, pkey string) (*realms.JIRARealm, er return nil, nil } +func htmlSummaryForIssue(issue *jira.Issue) string { + // form a summary of the issue being affected e.g: + // "Flibble Wibble [P1, In Progress]" + status := html.EscapeString(issue.Fields.Status.Name) + if issue.Fields.Resolution != nil { + status = fmt.Sprintf("%s (%s)", + status, html.EscapeString(issue.Fields.Resolution.Name)) + } + return fmt.Sprintf("%s [%s, %s]", + html.EscapeString(issue.Fields.Summary), + html.EscapeString(issue.Fields.Priority.Name), + status) +} + func init() { types.RegisterService(func(serviceID, webhookEndpointURL string) types.Service { return &jiraService{id: serviceID} From f7fd2d679fa3f72cd73c1278d84377d19ee16034 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 12 Aug 2016 09:31:39 +0100 Subject: [PATCH 2/3] Use the RealmID in the Service rather than all of them --- README.md | 24 +++++++++++++-- .../matrix-org/go-neb/realms/jira/jira.go | 1 - .../matrix-org/go-neb/services/jira/jira.go | 30 ++++++++++++------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 446bc46..23d3db8 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ JIRA installation. Once that is complete, users can OAuth on the target JIRA ins ``` curl -X POST localhost:4050/admin/requestAuthSession --data-binary '{ "RealmID": "jirarealm", - "UserID": "@your_user_id:localhost", + "UserID": "@example:localhost", "Config": { } }' @@ -218,7 +218,27 @@ Follow this link and grant access for NEB to act on your behalf. ### Create a JIRA bot -TODO +``` +curl -X POST localhost:4050/admin/configureService --data-binary '{ + "Type": "jira", + "Id": "jid", + "Config": { + "BotUserID": "@goneb:localhost", + "ClientUserID": "@example:localhost", + "Rooms": { + "!EmwxeXCVubhskuWvaw:localhost": { + "RealmID": "jirarealm", + "Projects": { + "BOTS": { + "Expand": true, + "Track": true + } + } + } + } + } +}' +``` # Developing on go-neb. diff --git a/src/github.com/matrix-org/go-neb/realms/jira/jira.go b/src/github.com/matrix-org/go-neb/realms/jira/jira.go index c28f9cf..a48bdd5 100644 --- a/src/github.com/matrix-org/go-neb/realms/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/realms/jira/jira.go @@ -264,7 +264,6 @@ func (r *JIRARealm) JIRAClient(userID string, allowUnauth bool) (*jira.Client, e } return nil, errors.New("No authenticated session found for " + userID) } - // make an authenticated client auth := r.oauth1Config(r.JIRAEndpoint) httpClient := auth.Client( diff --git a/src/github.com/matrix-org/go-neb/services/jira/jira.go b/src/github.com/matrix-org/go-neb/services/jira/jira.go index 3900fc3..bdc4dc1 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/services/jira/jira.go @@ -129,21 +129,29 @@ func (s *jiraService) expandIssue(roomID, userID, issueKey string) interface{} { return nil } - // Use the person who *provisioned* the service to check for project keys - // rather than the person who mentioned the issue key, as it is unlikely - // some random who mentioned the issue will have the intended auth. - r, err := s.projectToRealm(s.ClientUserID, projectKey) + r, err := database.GetServiceDB().LoadAuthRealm(s.Rooms[roomID].RealmID) if err != nil { - logger.WithError(err).Print("Failed to map project key to realm") + logger.WithFields(log.Fields{ + "realm_id": s.Rooms[roomID].RealmID, + log.ErrorKey: err, + }).Print("Failed to load realm") return nil } - if r == nil { - logger.Print("No known project exists with that project key.") - return nil + jrealm, ok := r.(*realms.JIRARealm) + if !ok { + logger.WithField("realm_id", s.Rooms[roomID].RealmID).Print( + "Realm cannot be typecast to JIRARealm", + ) } + logger.WithFields(log.Fields{ + "room_id": roomID, + "user_id": s.ClientUserID, + }).Print("Expanding issue") - logger.WithField("room_id", roomID).Print("Expanding issue") - cli, err := r.JIRAClient(s.ClientUserID, false) + // Use the person who *provisioned* the service to check for project keys + // rather than the person who mentioned the issue key, as it is unlikely + // some random who mentioned the issue will have the intended auth. + cli, err := jrealm.JIRAClient(s.ClientUserID, false) if err != nil { logger.WithFields(log.Fields{ log.ErrorKey: err, @@ -161,7 +169,7 @@ func (s *jiraService) expandIssue(roomID, userID, issueKey string) interface{} { "m.notice", fmt.Sprintf( "%sbrowse/%s : %s", - r.JIRAEndpoint, issueKey, htmlSummaryForIssue(issue), + jrealm.JIRAEndpoint, issueKey, htmlSummaryForIssue(issue), ), ) } From 5e37053e638845e864901d7921c196fd63e47bb9 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 12 Aug 2016 10:40:29 +0100 Subject: [PATCH 3/3] Kill dangling brackets --- src/github.com/matrix-org/go-neb/realms/jira/jira.go | 6 ++++-- .../matrix-org/go-neb/services/jira/jira.go | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/realms/jira/jira.go b/src/github.com/matrix-org/go-neb/realms/jira/jira.go index 869a2e2..dd930c6 100644 --- a/src/github.com/matrix-org/go-neb/realms/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/realms/jira/jira.go @@ -224,8 +224,10 @@ func (r *JIRARealm) ProjectKeyExists(userID, projectKey string) (bool, error) { return false, errors.New("No response returned") } if res.StatusCode < 200 || res.StatusCode >= 300 { - return false, fmt.Errorf("%srest/api/2/project returned code %d", - r.JIRAEndpoint, res.StatusCode) + return false, fmt.Errorf( + "%srest/api/2/project returned code %d", + r.JIRAEndpoint, res.StatusCode, + ) } for _, p := range projects { diff --git a/src/github.com/matrix-org/go-neb/services/jira/jira.go b/src/github.com/matrix-org/go-neb/services/jira/jira.go index bdc4dc1..21973b2 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/services/jira/jira.go @@ -266,13 +266,17 @@ func htmlSummaryForIssue(issue *jira.Issue) string { // "Flibble Wibble [P1, In Progress]" status := html.EscapeString(issue.Fields.Status.Name) if issue.Fields.Resolution != nil { - status = fmt.Sprintf("%s (%s)", - status, html.EscapeString(issue.Fields.Resolution.Name)) + status = fmt.Sprintf( + "%s (%s)", + status, html.EscapeString(issue.Fields.Resolution.Name), + ) } - return fmt.Sprintf("%s [%s, %s]", + return fmt.Sprintf( + "%s [%s, %s]", html.EscapeString(issue.Fields.Summary), html.EscapeString(issue.Fields.Priority.Name), - status) + status, + ) } func init() {