diff --git a/src/github.com/matrix-org/go-neb/services/echo/echo.go b/src/github.com/matrix-org/go-neb/services/echo/echo.go
index d102b20..ccdc5e4 100644
--- a/src/github.com/matrix-org/go-neb/services/echo/echo.go
+++ b/src/github.com/matrix-org/go-neb/services/echo/echo.go
@@ -4,6 +4,7 @@ import (
"github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/types"
+ "net/http"
"strings"
)
@@ -29,6 +30,9 @@ func (e *echoService) Plugin(roomID string) plugin.Plugin {
},
}
}
+func (e *echoService) OnReceiveWebhook(w http.ResponseWriter, req http.Request) {
+ w.WriteHeader(200) // Do nothing
+}
func init() {
types.RegisterService(func(serviceID string) types.Service {
diff --git a/src/github.com/matrix-org/go-neb/services/github/github.go b/src/github.com/matrix-org/go-neb/services/github/github.go
index c81d1a0..9d86414 100644
--- a/src/github.com/matrix-org/go-neb/services/github/github.go
+++ b/src/github.com/matrix-org/go-neb/services/github/github.go
@@ -6,8 +6,10 @@ import (
"github.com/google/go-github/github"
"github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin"
+ "github.com/matrix-org/go-neb/services/github/webhook"
"github.com/matrix-org/go-neb/types"
"golang.org/x/oauth2"
+ "net/http"
"regexp"
"strconv"
)
@@ -60,6 +62,10 @@ func (s *githubService) Plugin(roomID string) plugin.Plugin {
},
}
}
+func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req http.Request) {
+ // defer entirely to the webhook package
+ webhook.OnReceiveRequest(w, req, "")
+}
// githubClient returns a github Client which can perform Github API operations.
// If `token` is empty, a non-authenticated client will be created. This should be
diff --git a/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go b/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go
new file mode 100644
index 0000000..c11cc11
--- /dev/null
+++ b/src/github.com/matrix-org/go-neb/services/github/webhook/webhook.go
@@ -0,0 +1,259 @@
+package webhook
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ log "github.com/Sirupsen/logrus"
+ "github.com/google/go-github/github"
+ "html"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+// OnReceiveRequest processes incoming github webhook requests. The secretToken
+// parameter is optional.
+func OnReceiveRequest(w http.ResponseWriter, r http.Request, secretToken string) {
+ // Verify the HMAC signature if NEB was configured with a secret token
+ eventType := r.Header.Get("X-GitHub-Event")
+ signatureSHA1 := r.Header.Get("X-Hub-Signature")
+ content, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ log.WithError(err).Print("Failed to read Github webhook body")
+ w.WriteHeader(400)
+ return
+ }
+ // Verify request if a secret token has been supplied.
+ if secretToken != "" {
+ sigHex := strings.Split(signatureSHA1, "=")[1]
+ var sigBytes []byte
+ sigBytes, err = hex.DecodeString(sigHex)
+ if err != nil {
+ log.WithError(err).WithField("X-Hub-Signature", sigHex).Print(
+ "Failed to decode signature as hex.")
+ w.WriteHeader(400)
+ return
+ }
+
+ if !checkMAC([]byte(content), sigBytes, []byte(secretToken)) {
+ log.WithFields(log.Fields{
+ "X-Hub-Signature": signatureSHA1,
+ }).Print("Received Github event which failed MAC check.")
+ w.WriteHeader(403)
+ return
+ }
+ }
+
+ log.WithFields(log.Fields{
+ "event_type": eventType,
+ "signature": signatureSHA1,
+ }).Print("Received Github event")
+
+ htmlStr, repo, err := parseGithubEvent(eventType, content)
+ if err != nil {
+ log.WithError(err).Print("Failed to parse github event")
+ w.WriteHeader(500)
+ return
+ }
+
+ if err := handleWebhookEvent(eventType, htmlStr, repo); err != nil {
+ log.WithError(err).Print("Failed to handle Github webhook event")
+ w.WriteHeader(500)
+ return
+ }
+ w.WriteHeader(200)
+}
+
+// checkMAC reports whether messageMAC is a valid HMAC tag for message.
+func checkMAC(message, messageMAC, key []byte) bool {
+ mac := hmac.New(sha1.New, key)
+ mac.Write(message)
+ expectedMAC := mac.Sum(nil)
+ return hmac.Equal(messageMAC, expectedMAC)
+}
+
+// parseGithubEvent parses a github event type and JSON data and returns an explanatory
+// HTML string and the github repository this event affects, or an error.
+func parseGithubEvent(eventType string, data []byte) (string, *github.Repository, error) {
+ if eventType == "pull_request" {
+ var ev github.PullRequestEvent
+ if err := json.Unmarshal(data, &ev); err != nil {
+ return "", nil, err
+ }
+ return pullRequestHTMLMessage(ev), ev.Repo, nil
+ } else if eventType == "issues" {
+ var ev github.IssuesEvent
+ if err := json.Unmarshal(data, &ev); err != nil {
+ return "", nil, err
+ }
+ return issueHTMLMessage(ev), ev.Repo, nil
+ } else if eventType == "push" {
+ var ev github.PushEvent
+ if err := json.Unmarshal(data, &ev); err != nil {
+ return "", nil, err
+ }
+
+ // The 'push' event repository format is subtly different from normal, so munge the bits we need.
+ fullName := *ev.Repo.Owner.Name + "/" + *ev.Repo.Name
+ repo := github.Repository{
+ Owner: &github.User{
+ Login: ev.Repo.Owner.Name,
+ },
+ Name: ev.Repo.Name,
+ FullName: &fullName,
+ }
+ return pushHTMLMessage(ev), &repo, nil
+ } else if eventType == "issue_comment" {
+ var ev github.IssueCommentEvent
+ if err := json.Unmarshal(data, &ev); err != nil {
+ return "", nil, err
+ }
+ return issueCommentHTMLMessage(ev), ev.Repo, nil
+ } else if eventType == "pull_request_review_comment" {
+ var ev github.PullRequestReviewCommentEvent
+ if err := json.Unmarshal(data, &ev); err != nil {
+ return "", nil, err
+ }
+ return prReviewCommentHTMLMessage(ev), ev.Repo, nil
+ }
+ return "", nil, fmt.Errorf("Unrecognized event type")
+}
+
+func handleWebhookEvent(eventType string, htmlStr string, repo *github.Repository) error {
+ return nil
+}
+
+func pullRequestHTMLMessage(p github.PullRequestEvent) string {
+ var actionTarget string
+ if p.PullRequest.Assignee != nil && p.PullRequest.Assignee.Login != nil {
+ actionTarget = fmt.Sprintf(" to %s", *p.PullRequest.Assignee.Login)
+ }
+ return fmt.Sprintf(
+ "[%s] %s %s pull request #%d: %s [%s]%s - %s",
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(*p.Sender.Login),
+ html.EscapeString(*p.Action),
+ *p.Number,
+ html.EscapeString(*p.PullRequest.Title),
+ html.EscapeString(*p.PullRequest.State),
+ html.EscapeString(actionTarget),
+ html.EscapeString(*p.PullRequest.HTMLURL),
+ )
+}
+
+func issueHTMLMessage(p github.IssuesEvent) string {
+ var actionTarget string
+ if p.Issue.Assignee != nil && p.Issue.Assignee.Login != nil {
+ actionTarget = fmt.Sprintf(" to %s", *p.Issue.Assignee.Login)
+ }
+ return fmt.Sprintf(
+ "[%s] %s %s issue #%d: %s [%s]%s - %s",
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(*p.Sender.Login),
+ html.EscapeString(*p.Action),
+ *p.Issue.Number,
+ html.EscapeString(*p.Issue.Title),
+ html.EscapeString(*p.Issue.State),
+ html.EscapeString(actionTarget),
+ html.EscapeString(*p.Issue.HTMLURL),
+ )
+}
+
+func issueCommentHTMLMessage(p github.IssueCommentEvent) string {
+ var kind string
+ if p.Issue.PullRequestLinks == nil {
+ kind = "issue"
+ } else {
+ kind = "pull request"
+ }
+
+ return fmt.Sprintf(
+ "[%s] %s commented on %s's %s #%d: %s - %s",
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(*p.Comment.User.Login),
+ html.EscapeString(*p.Issue.User.Login),
+ kind,
+ *p.Issue.Number,
+ html.EscapeString(*p.Issue.Title),
+ html.EscapeString(*p.Issue.HTMLURL),
+ )
+}
+
+func prReviewCommentHTMLMessage(p github.PullRequestReviewCommentEvent) string {
+ assignee := "None"
+ if p.PullRequest.Assignee != nil {
+ assignee = html.EscapeString(*p.PullRequest.Assignee.Login)
+ }
+ return fmt.Sprintf(
+ "[%s] %s made a line comment on %s's pull request #%d (assignee: %s): %s - %s",
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(*p.Sender.Login),
+ html.EscapeString(*p.PullRequest.User.Login),
+ *p.PullRequest.Number,
+ assignee,
+ html.EscapeString(*p.PullRequest.Title),
+ html.EscapeString(*p.Comment.HTMLURL),
+ )
+}
+
+func pushHTMLMessage(p github.PushEvent) string {
+ // /refs/heads/alice/branch-name => alice/branch-name
+ branch := strings.Replace(*p.Ref, "refs/heads/", "", -1)
+
+ // this branch was deleted, no HeadCommit object and deleted=true
+ if p.HeadCommit == nil && p.Deleted != nil && *p.Deleted {
+ return fmt.Sprintf(
+ `[%s] %s deleted %s`,
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(*p.Pusher.Name),
+ html.EscapeString(branch),
+ )
+ }
+
+ if p.Commits != nil && len(p.Commits) > 1 {
+ // multi-commit message
+ // [] pushed commits to :
+ //
+ var cList []string
+ for _, c := range p.Commits {
+ cList = append(cList, fmt.Sprintf(
+ `%s: %s`,
+ html.EscapeString(nameForAuthor(c.Author)),
+ html.EscapeString(*c.Message),
+ ))
+ }
+ return fmt.Sprintf(
+ `[%s] %s pushed %d commits to %s: %s
%s`,
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(nameForAuthor(p.HeadCommit.Committer)),
+ len(p.Commits),
+ html.EscapeString(branch),
+ html.EscapeString(*p.HeadCommit.URL),
+ strings.Join(cList, "
"),
+ )
+ }
+
+ // single commit message
+ // [] pushed to : -
+ return fmt.Sprintf(
+ `[%s] %s pushed to %s: %s - %s`,
+ html.EscapeString(*p.Repo.FullName),
+ html.EscapeString(nameForAuthor(p.HeadCommit.Committer)),
+ html.EscapeString(branch),
+ html.EscapeString(*p.HeadCommit.Message),
+ html.EscapeString(*p.HeadCommit.URL),
+ )
+}
+
+func nameForAuthor(a *github.CommitAuthor) string {
+ if a == nil {
+ return ""
+ }
+ if a.Login != nil { // prefer to use their GH username than the name they commited as
+ return *a.Login
+ }
+ return *a.Name
+}
diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go
index 7bdc6a8..c5dc8ce 100644
--- a/src/github.com/matrix-org/go-neb/types/types.go
+++ b/src/github.com/matrix-org/go-neb/types/types.go
@@ -3,7 +3,7 @@ package types
import (
"errors"
"github.com/matrix-org/go-neb/plugin"
- // "net/http"
+ "net/http"
"net/url"
)
@@ -32,7 +32,7 @@ type Service interface {
ServiceType() string
RoomIDs() []string
Plugin(roomID string) plugin.Plugin
- // OnReceiveWebhook(req http.Request)
+ OnReceiveWebhook(w http.ResponseWriter, req http.Request)
}
var servicesByType = map[string]func(string) Service{}