diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/api.go index 084ea43..522d4c9 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/api.go @@ -198,6 +198,11 @@ func (s *configureServiceHandler) OnIncomingRequest(req *http.Request) (interfac return nil, &errors.HTTPError{err, "Error parsing config JSON", 400} } + err := service.Register() + if err != nil { + return nil, &errors.HTTPError{err, "Failed to register service: " + err.Error(), 500} + } + client, err := s.clients.Client(service.ServiceUserID()) if err != nil { return nil, &errors.HTTPError{err, "Unknown matrix client", 400} diff --git a/src/github.com/matrix-org/go-neb/plugin/plugin.go b/src/github.com/matrix-org/go-neb/plugin/plugin.go index d44a0c1..8ee6842 100644 --- a/src/github.com/matrix-org/go-neb/plugin/plugin.go +++ b/src/github.com/matrix-org/go-neb/plugin/plugin.go @@ -31,7 +31,7 @@ type Command struct { // the appropriate RFC. type Expansion struct { Regexp *regexp.Regexp - Expand func(roomID, matchingText string) interface{} + Expand func(roomID, userID, matchingText string) interface{} } // matches if the arguments start with the path of the command. @@ -95,7 +95,7 @@ func runExpansionsForPlugin(plugin Plugin, event *matrix.Event, body string) []i continue } matches[matchingText] = true - if response := expansion.Expand(event.RoomID, matchingText); response != nil { + if response := expansion.Expand(event.RoomID, event.Sender, matchingText); response != nil { responses = append(responses, response) } } diff --git a/src/github.com/matrix-org/go-neb/plugin/plugin_test.go b/src/github.com/matrix-org/go-neb/plugin/plugin_test.go index 9e70a4e..6afe6ab 100644 --- a/src/github.com/matrix-org/go-neb/plugin/plugin_test.go +++ b/src/github.com/matrix-org/go-neb/plugin/plugin_test.go @@ -26,21 +26,21 @@ func makeTestEvent(msgtype, body string) *matrix.Event { type testResponse struct { RoomID string - Sender string Arguments []string } func makeTestResponse(roomID, sender string, arguments []string) interface{} { - return testResponse{roomID, sender, arguments} + return testResponse{roomID, arguments} } type testExpansion struct { RoomID string + UserID string MatchingText string } -func makeTestExpansion(roomID, matchingText string) interface{} { - return testExpansion{roomID, matchingText} +func makeTestExpansion(roomID, userID, matchingText string) interface{} { + return testExpansion{roomID, userID, matchingText} } func makeTestPlugin(paths [][]string, regexps []*regexp.Regexp) Plugin { @@ -74,7 +74,7 @@ func TestRunCommands(t *testing.T) { "arg1", "arg 2", "arg 3", })} if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } @@ -89,7 +89,7 @@ func TestRunCommandsBestMatch(t *testing.T) { "arg1", })} if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } @@ -105,7 +105,7 @@ func TestRunCommandsMultiplePlugins(t *testing.T) { makeTestResponse(myRoomID, mySender, []string{"first", "arg1"}), } if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } @@ -119,7 +119,7 @@ func TestRunCommandsInvalidShell(t *testing.T) { makeTestResponse(myRoomID, mySender, []string{"'mismatched", `quotes"`}), } if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } @@ -133,12 +133,12 @@ func TestExpansion(t *testing.T) { event := makeTestEvent("m.text", "test banana for scale") got := runCommands(plugins, event) want := []interface{}{ - makeTestExpansion(myRoomID, "anana"), - makeTestExpansion(myRoomID, "ale"), - makeTestExpansion(myRoomID, "ban"), + makeTestExpansion(myRoomID, mySender, "anana"), + makeTestExpansion(myRoomID, mySender, "ale"), + makeTestExpansion(myRoomID, mySender, "ban"), } if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } @@ -151,9 +151,9 @@ func TestExpansionDuplicateMatches(t *testing.T) { event := makeTestEvent("m.text", "badger badger badger") got := runCommands(plugins, event) want := []interface{}{ - makeTestExpansion(myRoomID, "badger"), + makeTestExpansion(myRoomID, mySender, "badger"), } if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want) + t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) } } diff --git a/src/github.com/matrix-org/go-neb/realms/github/github.go b/src/github.com/matrix-org/go-neb/realms/github/github.go index b928282..9b88d5b 100644 --- a/src/github.com/matrix-org/go-neb/realms/github/github.go +++ b/src/github.com/matrix-org/go-neb/realms/github/github.go @@ -19,23 +19,29 @@ type githubRealm struct { RedirectBaseURI string } -type githubSession struct { +// GithubSession represents an authenticated github session +type GithubSession struct { + // AccessToken is the github access token for the user AccessToken string - Scopes string - id string - userID string - realmID string + // Scopes are the set of *ALLOWED* scopes (which may not be the same as the requested scopes) + Scopes string + id string + userID string + realmID string } -func (s *githubSession) UserID() string { +// UserID returns the user_id who authorised with Github +func (s *GithubSession) UserID() string { return s.userID } -func (s *githubSession) RealmID() string { +// RealmID returns the realm ID of the realm which performed the authentication +func (s *GithubSession) RealmID() string { return s.realmID } -func (s *githubSession) ID() string { +// ID returns the session ID +func (s *GithubSession) ID() string { return s.id } @@ -61,7 +67,7 @@ func (r *githubRealm) RequestAuthSession(userID string, req json.RawMessage) int // TODO: Path is from goneb.go - we should probably factor it out. q.Set("redirect_uri", r.RedirectBaseURI+"/realms/redirects/"+r.ID()) u.RawQuery = q.Encode() - session := &githubSession{ + session := &GithubSession{ id: state, // key off the state for redirects userID: userID, realmID: r.ID(), @@ -96,7 +102,7 @@ func (r *githubRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request failWith(logger, w, 400, "Provided ?state= param is not recognised.", err) return } - ghSession, ok := session.(*githubSession) + ghSession, ok := session.(*GithubSession) if !ok { failWith(logger, w, 500, "Unexpected session found.", nil) return @@ -136,7 +142,7 @@ func (r *githubRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request } func (r *githubRealm) AuthSession(id, userID, realmID string) types.AuthSession { - return &githubSession{ + return &GithubSession{ id: id, userID: userID, realmID: realmID, 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 46dc4d2..c0cde71 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 @@ -18,6 +18,7 @@ func (e *echoService) ServiceUserID() string { return e.UserID } func (e *echoService) ServiceID() string { return e.id } func (e *echoService) ServiceType() string { return "echo" } func (e *echoService) RoomIDs() []string { return e.Rooms } +func (e *echoService) Register() error { return nil } func (e *echoService) Plugin(roomID string) plugin.Plugin { return plugin.Plugin{ Commands: []plugin.Command{ 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 d194702..9e3ddb5 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 @@ -4,14 +4,17 @@ import ( "fmt" log "github.com/Sirupsen/logrus" "github.com/google/go-github/github" + "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/matrix" "github.com/matrix-org/go-neb/plugin" + "github.com/matrix-org/go-neb/realms/github" "github.com/matrix-org/go-neb/services/github/webhook" "github.com/matrix-org/go-neb/types" "golang.org/x/oauth2" "net/http" "regexp" "strconv" + "strings" ) // Matches alphanumeric then a /, then more alphanumeric then a #, then a number. @@ -19,29 +22,44 @@ import ( var ownerRepoIssueRegex = regexp.MustCompile("([A-z0-9-_]+)/([A-z0-9-_]+)#([0-9]+)") type githubService struct { - id string - UserID string - Rooms map[string][]string // room_id => ["push","issue","pull_request"] + id string + BotUserID string + GithubUserID string + RealmID string + WebhookRooms map[string][]string // room_id => ["push","issue","pull_request"] } -func (s *githubService) ServiceUserID() string { return s.UserID } +func (s *githubService) ServiceUserID() string { return s.BotUserID } func (s *githubService) ServiceID() string { return s.id } func (s *githubService) ServiceType() string { return "github" } func (s *githubService) RoomIDs() []string { var keys []string - for k := range s.Rooms { + for k := range s.WebhookRooms { keys = append(keys, k) } return keys } func (s *githubService) Plugin(roomID string) plugin.Plugin { return plugin.Plugin{ - Commands: []plugin.Command{}, + Commands: []plugin.Command{ + plugin.Command{ + Path: []string{"github", "create"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + cli := s.githubClientFor(userID, false) + if cli == nil { + // TODO: send starter link + return &matrix.TextMessage{"m.notice", + userID + " : You have not linked your Github account."}, nil + } + return &matrix.TextMessage{"m.notice", strings.Join(args, " ")}, nil + }, + }, + }, Expansions: []plugin.Expansion{ plugin.Expansion{ Regexp: ownerRepoIssueRegex, - Expand: func(roomID, matchingText string) interface{} { - cli := githubClient("") + Expand: func(roomID, userID, matchingText string) interface{} { + cli := s.githubClientFor(userID, true) owner, repo, num, err := ownerRepoNumberFromText(matchingText) if err != nil { log.WithError(err).WithField("text", matchingText).Print( @@ -75,7 +93,7 @@ func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req *http.Reques return } - for roomID, notif := range s.Rooms { + for roomID, notif := range s.WebhookRooms { notifyRoom := false for _, notifyType := range notif { if evType == notifyType { @@ -99,6 +117,60 @@ func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req *http.Reques } w.WriteHeader(200) } +func (s *githubService) Register() error { + if s.RealmID == "" || s.BotUserID == "" { + return fmt.Errorf("RealmID and BotUserID are required") + } + // check realm exists + realm, err := database.GetServiceDB().LoadAuthRealm(s.RealmID) + if err != nil { + return err + } + // make sure the realm is of the type we expect + if realm.Type() != "github" { + return fmt.Errorf("Realm is of type '%s', not 'github'", realm.Type()) + } + return nil +} + +func (s *githubService) githubClientFor(userID string, allowUnauth bool) *github.Client { + token, err := getTokenForUser(s.RealmID, userID) + if err != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "user_id": userID, + "realm_id": s.RealmID, + }).Print("Failed to get token for user") + } + if token != "" { + return githubClient(token) + } else if allowUnauth { + return githubClient("") + } else { + return nil + } +} + +func getTokenForUser(realmID, userID string) (string, error) { + realm, err := database.GetServiceDB().LoadAuthRealm(realmID) + if err != nil { + return "", err + } + if realm.Type() != "github" { + return "", fmt.Errorf("Bad realm type: %s", realm.Type()) + } + + // pull out the token (TODO: should the service know how the realm stores this?) + session, err := database.GetServiceDB().LoadAuthSessionByUser(realm.ID(), userID) + if err != nil { + return "", err + } + ghSession, ok := session.(*realms.GithubSession) + if !ok { + return "", fmt.Errorf("Session is not a github session: %s", session.ID()) + } + return ghSession.AccessToken, nil +} // 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/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go index fe6c453..942279f 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -35,6 +35,7 @@ type Service interface { RoomIDs() []string Plugin(roomID string) plugin.Plugin OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) + Register() error } var servicesByType = map[string]func(string) Service{}