diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/api.go index 146a820..ff1da59 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/api.go @@ -140,6 +140,9 @@ func (wh *webhookHandler) handle(w http.ResponseWriter, req *http.Request) { w.WriteHeader(500) return } + log.WithFields(log.Fields{ + "service_id": service.ServiceID(), + }).Print("Incoming webhook") service.OnReceiveWebhook(w, req, cli) } 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 dd930c6..26e6fb7 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 @@ -32,6 +32,7 @@ type JIRARealm struct { ConsumerSecret string PublicKeyPEM string // clobbered based on PrivateKeyPEM PrivateKeyPEM string + HasWebhook bool // clobbered based on NEB } // JIRASession represents a single authentication session between a user and a JIRA endpoint. @@ -95,6 +96,7 @@ func (r *JIRARealm) Register() error { if r.JIRAEndpoint == "" { return errors.New("JIRAEndpoint must be specified") } + r.HasWebhook = false // never let the user set this; only NEB can. // Check to see if JIRA endpoint is valid by pinging an endpoint cli, err := r.JIRAClient("", true) 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 22cd933..9e92af6 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 @@ -10,6 +10,7 @@ import ( "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/realms/jira" + "github.com/matrix-org/go-neb/realms/jira/urls" "github.com/matrix-org/go-neb/services/jira/webhook" "github.com/matrix-org/go-neb/types" "html" @@ -222,7 +223,47 @@ func (s *jiraService) Plugin(roomID string) plugin.Plugin { } func (s *jiraService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { - webhook.OnReceiveRequest(w, req, cli) + eventProjectKey, event, httpErr := webhook.OnReceiveRequest(req) + if httpErr != nil { + log.WithError(httpErr).Print("Failed to handle JIRA webhook") + w.WriteHeader(500) + return + } + // grab base jira url + jurl, err := urls.ParseJIRAURL(event.Issue.Self) + if err != nil { + log.WithError(err).Print("Failed to parse base JIRA URL") + w.WriteHeader(500) + return + } + // work out the HTML to send + htmlText := htmlForEvent(event, jurl.Base) + if htmlText == "" { + log.WithField("project", eventProjectKey).Print("Unable to process event for project") + w.WriteHeader(200) + return + } + // send message into each configured room + for roomID, roomConfig := range s.Rooms { + for _, realmConfig := range roomConfig.Realms { + for pkey, projectConfig := range realmConfig.Projects { + if pkey != eventProjectKey || !projectConfig.Track { + continue + } + _, msgErr := cli.SendMessageEvent( + roomID, "m.room.message", matrix.GetHTMLMessage("m.notice", htmlText), + ) + if msgErr != nil { + log.WithFields(log.Fields{ + log.ErrorKey: msgErr, + "project": pkey, + "room_id": roomID, + }).Print("Failed to send notice into room") + } + } + } + } + w.WriteHeader(200) } func (s *jiraService) realmIDForProject(roomID, projectKey string) string { @@ -335,6 +376,31 @@ func htmlSummaryForIssue(issue *jira.Issue) string { ) } +// htmlForEvent formats a webhook event as HTML. Returns an empty string if there is nothing to send/cannot +// be parsed. +func htmlForEvent(whe *webhook.Event, jiraBaseURL string) string { + action := "" + if whe.WebhookEvent == "jira:issue_updated" { + action = "updated" + } else if whe.WebhookEvent == "jira:issue_deleted" { + action = "deleted" + } else if whe.WebhookEvent == "jira:issue_created" { + action = "created" + } else { + return "" + } + + summaryHTML := htmlSummaryForIssue(&whe.Issue) + + return fmt.Sprintf("%s %s %s - %s %s", + html.EscapeString(whe.User.Name), + html.EscapeString(action), + html.EscapeString(whe.Issue.Key), + summaryHTML, + html.EscapeString(jiraBaseURL+"browse/"+whe.Issue.Key), + ) +} + func init() { types.RegisterService(func(serviceID, webhookEndpointURL string) types.Service { return &jiraService{id: serviceID, webhookEndpointURL: webhookEndpointURL} diff --git a/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go b/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go index 2851275..a34765c 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go +++ b/src/github.com/matrix-org/go-neb/services/jira/webhook/webhook.go @@ -1,13 +1,15 @@ package webhook import ( + "encoding/json" "fmt" log "github.com/Sirupsen/logrus" "github.com/andygrunwald/go-jira" + "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/errors" - "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/realms/jira" "net/http" + "strings" ) type jiraWebhook struct { @@ -20,6 +22,14 @@ type jiraWebhook struct { Enabled bool `json:"enabled"` } +// Event represents an incoming JIRA webhook event +type Event struct { + WebhookEvent string `json:"webhookEvent"` + Timestamp int64 `json:"timestamp"` + User jira.User `json:"user"` + Issue jira.Issue `json:"issue"` +} + // RegisterHook checks to see if this user is allowed to track the given projects and then tracks them. func RegisterHook(jrealm *realms.JIRARealm, projects []string, userID, webhookEndpointURL string) error { // Tracking means that a webhook may need to be created on the remote JIRA installation. @@ -72,8 +82,11 @@ func RegisterHook(jrealm *realms.JIRARealm, projects []string, userID, webhookEn // All projects that wish to be tracked are public, but the user cannot create // webhooks. The only way this will work is if we already have a webhook for this // JIRA endpoint. - // TODO: Check for an existing webhook for this realm (flag on realm?) - return fmt.Errorf("Not supported yet") + if !jrealm.HasWebhook { + logger.Print("No webhook exists for this realm.") + return fmt.Errorf("Not authorised to create webhook: not an admin.") + } + return nil } // The user is probably an admin (can query webhooks endpoint) @@ -85,9 +98,23 @@ func RegisterHook(jrealm *realms.JIRARealm, projects []string, userID, webhookEn return createWebhook(jrealm, webhookEndpointURL, userID) } -// OnReceiveRequest is called when JIRA hits NEB with an update -func OnReceiveRequest(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { - w.WriteHeader(200) // Do nothing +// OnReceiveRequest is called when JIRA hits NEB with an update. +// Returns the project key and webhook event, or an error. +func OnReceiveRequest(req *http.Request) (string, *Event, *errors.HTTPError) { + // extract the JIRA webhook event JSON + defer req.Body.Close() + var whe Event + err := json.NewDecoder(req.Body).Decode(&whe) + if err != nil { + return "", nil, &errors.HTTPError{err, "Failed to parse request JSON", 400} + } + + if err != nil { + return "", nil, &errors.HTTPError{err, "Failed to parse JIRA URL", 400} + } + projKey := strings.Split(whe.Issue.Key, "-")[0] + projKey = strings.ToUpper(projKey) + return projKey, &whe, nil } func createWebhook(jrealm *realms.JIRARealm, webhookEndpointURL, userID string) error { @@ -115,7 +142,11 @@ func createWebhook(jrealm *realms.JIRARealm, webhookEndpointURL, userID string) "realm_id": jrealm.ID(), "jira_url": jrealm.JIRAEndpoint, }).Print("Created webhook") - return nil + + // mark this on the realm and persist it. + jrealm.HasWebhook = true + _, err = database.GetServiceDB().StoreAuthRealm(jrealm) + return err } func getWebhook(cli *jira.Client, webhookEndpointURL string) (*jiraWebhook, *errors.HTTPError) {