diff --git a/src/github.com/matrix-org/go-neb/clients/clients.go b/src/github.com/matrix-org/go-neb/clients/clients.go index 9192289..5408928 100644 --- a/src/github.com/matrix-org/go-neb/clients/clients.go +++ b/src/github.com/matrix-org/go-neb/clients/clients.go @@ -1,16 +1,18 @@ package clients import ( + "net/http" + "net/url" + "strings" + "sync" + log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/api" "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/metrics" "github.com/matrix-org/go-neb/types" - "net/http" - "net/url" - "strings" - "sync" + shellwords "github.com/mattn/go-shellwords" ) type nextBatchStore struct { @@ -179,11 +181,111 @@ func (c *Clients) onMessageEvent(client *matrix.Client, event *matrix.Event) { "service_user_id": client.UserID, }).Warn("Error loading services") } - var plugins []plugin.Plugin + + body, ok := event.Body() + if !ok || body == "" { + return + } + + // filter m.notice to prevent loops + if msgtype, ok := event.MessageType(); !ok || msgtype == "m.notice" { + return + } + + var responses []interface{} + for _, service := range services { - plugins = append(plugins, service.Plugin(client, event.RoomID)) + if body[0] == '!' { // message is a command + args, err := shellwords.Parse(body[1:]) + if err != nil { + args = strings.Split(body[1:], " ") + } + + if response := runCommandForService(service.Commands(client, event.RoomID), event, args); response != nil { + responses = append(responses, response) + } + } else { // message isn't a command, it might need expanding + expansions := runExpansionsForService(service.Expansions(client, event.RoomID), event, body) + responses = append(responses, expansions...) + } + } + + for _, content := range responses { + if _, err := client.SendMessageEvent(event.RoomID, "m.room.message", content); err != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "room_id": event.RoomID, + "user_id": event.Sender, + "content": content, + }).Print("Failed to send command response") + } + } +} + +// runCommandForService runs a single command read from a matrix event. Runs +// the matching command with the longest path. Returns the JSON encodable +// content of a single matrix message event to use as a response or nil if no +// response is appropriate. +func runCommandForService(cmds []types.Command, event *matrix.Event, arguments []string) interface{} { + var bestMatch *types.Command + for _, command := range cmds { + matches := command.Matches(arguments) + betterMatch := bestMatch == nil || len(bestMatch.Path) < len(command.Path) + if matches && betterMatch { + bestMatch = &command + } + } + + if bestMatch == nil { + return nil + } + + cmdArgs := arguments[len(bestMatch.Path):] + log.WithFields(log.Fields{ + "room_id": event.RoomID, + "user_id": event.Sender, + "command": bestMatch.Path, + }).Info("Executing command") + content, err := bestMatch.Command(event.RoomID, event.Sender, cmdArgs) + if err != nil { + if content != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "room_id": event.RoomID, + "user_id": event.Sender, + "command": bestMatch.Path, + "args": cmdArgs, + }).Warn("Command returned both error and content.") + } + metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusFailure) + content = matrix.TextMessage{"m.notice", err.Error()} + } else { + metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusSuccess) } - plugin.OnMessage(plugins, client, event) + + return content +} + +// run the expansions for a matrix event. +func runExpansionsForService(expans []types.Expansion, event *matrix.Event, body string) []interface{} { + var responses []interface{} + + for _, expansion := range expans { + matches := map[string]bool{} + for _, matchingGroups := range expansion.Regexp.FindAllStringSubmatch(body, -1) { + matchingText := matchingGroups[0] // first element is always the complete match + if matches[matchingText] { + // Only expand the first occurance of a matching string + continue + } + matches[matchingText] = true + if response := expansion.Expand(event.RoomID, event.Sender, matchingGroups); response != nil { + responses = append(responses, response) + } + } + } + + return responses } func (c *Clients) onBotOptionsEvent(client *matrix.Client, event *matrix.Event) { diff --git a/src/github.com/matrix-org/go-neb/plugin/plugin.go b/src/github.com/matrix-org/go-neb/plugin/plugin.go deleted file mode 100644 index 0007bec..0000000 --- a/src/github.com/matrix-org/go-neb/plugin/plugin.go +++ /dev/null @@ -1,179 +0,0 @@ -package plugin - -import ( - log "github.com/Sirupsen/logrus" - "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/metrics" - "github.com/mattn/go-shellwords" - "regexp" - "strings" -) - -// A Plugin is a list of commands and expansions to apply to incoming messages. -type Plugin struct { - Commands []Command - Expansions []Expansion -} - -// A Command is something that a user invokes by sending a message starting with '!' -// followed by a list of strings that name the command, followed by a list of argument -// strings. The argument strings may be quoted using '\"' and '\'' in the same way -// that they are quoted in the unix shell. -type Command struct { - Path []string - Arguments []string - Help string - Command func(roomID, userID string, arguments []string) (content interface{}, err error) -} - -// An Expansion is something that actives when the user sends any message -// containing a string matching a given pattern. For example an RFC expansion -// might expand "RFC 6214" into "Adaptation of RFC 1149 for IPv6" and link to -// the appropriate RFC. -type Expansion struct { - Regexp *regexp.Regexp - Expand func(roomID, userID string, matchingGroups []string) interface{} -} - -// matches if the arguments start with the path of the command. -func (command *Command) matches(arguments []string) bool { - if len(arguments) < len(command.Path) { - return false - } - for i, segment := range command.Path { - if segment != arguments[i] { - return false - } - } - return true -} - -// runCommandForPlugin runs a single command read from a matrix event. Runs -// the matching command with the longest path. Returns the JSON encodable -// content of a single matrix message event to use as a response or nil if no -// response is appropriate. -func runCommandForPlugin(plugin Plugin, event *matrix.Event, arguments []string) interface{} { - var bestMatch *Command - for _, command := range plugin.Commands { - matches := command.matches(arguments) - betterMatch := bestMatch == nil || len(bestMatch.Path) < len(command.Path) - if matches && betterMatch { - bestMatch = &command - } - } - - if bestMatch == nil { - return nil - } - - cmdArgs := arguments[len(bestMatch.Path):] - log.WithFields(log.Fields{ - "room_id": event.RoomID, - "user_id": event.Sender, - "command": bestMatch.Path, - }).Info("Executing command") - content, err := bestMatch.Command(event.RoomID, event.Sender, cmdArgs) - if err != nil { - if content != nil { - log.WithFields(log.Fields{ - log.ErrorKey: err, - "room_id": event.RoomID, - "user_id": event.Sender, - "command": bestMatch.Path, - "args": cmdArgs, - }).Warn("Command returned both error and content.") - } - metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusFailure) - content = matrix.TextMessage{"m.notice", err.Error()} - } else { - metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusSuccess) - } - - return content -} - -// run the expansions for a matrix event. -func runExpansionsForPlugin(plugin Plugin, event *matrix.Event, body string) []interface{} { - var responses []interface{} - - for _, expansion := range plugin.Expansions { - matches := map[string]bool{} - for _, matchingGroups := range expansion.Regexp.FindAllStringSubmatch(body, -1) { - matchingText := matchingGroups[0] // first element is always the complete match - if matches[matchingText] { - // Only expand the first occurance of a matching string - continue - } - matches[matchingText] = true - if response := expansion.Expand(event.RoomID, event.Sender, matchingGroups); response != nil { - responses = append(responses, response) - } - } - } - - return responses -} - -// runCommands runs the plugin commands or expansions for a single matrix -// event. Returns a list of JSON encodable contents for the matrix messages -// to use as responses. -// If the message beings with '!' then it is assumed to be a command. Each -// plugin is checked for a matching command, if a match is found then that -// command is run. If more than one plugin has a matching command then all -// of those commands are run. This shouldn't happen unless the same plugin -// is installed multiple times since each plugin will usually have a -// distinct prefix for its commands. -// If the message doesn't begin with '!' then it is checked against the -// expansions for each plugin. -func runCommands(plugins []Plugin, event *matrix.Event) []interface{} { - body, ok := event.Body() - if !ok || body == "" { - return nil - } - - // filter m.notice to prevent loops - if msgtype, ok := event.MessageType(); !ok || msgtype == "m.notice" { - return nil - } - - var responses []interface{} - - if body[0] == '!' { - args, err := shellwords.Parse(body[1:]) - if err != nil { - args = strings.Split(body[1:], " ") - } - - for _, plugin := range plugins { - if response := runCommandForPlugin(plugin, event, args); response != nil { - responses = append(responses, response) - } - } - } else { - for _, plugin := range plugins { - expansions := runExpansionsForPlugin(plugin, event, body) - responses = append(responses, expansions...) - } - } - - return responses -} - -// OnMessage checks the message event to see whether it contains any commands -// or expansions from the listed plugins and processes those commands or -// expansions. -func OnMessage(plugins []Plugin, client *matrix.Client, event *matrix.Event) { - responses := runCommands(plugins, event) - - for _, content := range responses { - _, err := client.SendMessageEvent(event.RoomID, "m.room.message", content) - if err != nil { - log.WithFields(log.Fields{ - log.ErrorKey: err, - "room_id": event.RoomID, - "user_id": event.Sender, - "content": content, - }).Print("Failed to send command 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 deleted file mode 100644 index 6ef4c96..0000000 --- a/src/github.com/matrix-org/go-neb/plugin/plugin_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package plugin - -import ( - "github.com/matrix-org/go-neb/matrix" - "reflect" - "regexp" - "testing" -) - -const ( - myRoomID = "!room:example.com" - mySender = "@user:example.com" -) - -func makeTestEvent(msgtype, body string) *matrix.Event { - return &matrix.Event{ - Sender: mySender, - Type: "m.room.message", - RoomID: myRoomID, - Content: map[string]interface{}{ - "body": body, - "msgtype": msgtype, - }, - } -} - -type testResponse struct { - RoomID string - Arguments []string -} - -func makeTestResponse(roomID, sender string, arguments []string) interface{} { - return testResponse{roomID, arguments} -} - -type testExpansion struct { - RoomID string - UserID string - MatchingGroups []string -} - -func makeTestExpansion(roomID, userID string, matchingGroups []string) interface{} { - return testExpansion{roomID, userID, matchingGroups} -} - -func makeTestPlugin(paths [][]string, regexps []*regexp.Regexp) Plugin { - var commands []Command - for _, path := range paths { - commands = append(commands, Command{ - Path: path, - Command: func(roomID, sender string, arguments []string) (interface{}, error) { - return makeTestResponse(roomID, sender, arguments), nil - }, - }) - } - var expansions []Expansion - for _, re := range regexps { - expansions = append(expansions, Expansion{ - Regexp: re, - Expand: makeTestExpansion, - }) - } - - return Plugin{Commands: commands, Expansions: expansions} -} - -func TestRunCommands(t *testing.T) { - plugins := []Plugin{makeTestPlugin([][]string{ - []string{"test", "command"}, - }, nil)} - event := makeTestEvent("m.text", `!test command arg1 "arg 2" 'arg 3'`) - got := runCommands(plugins, event) - want := []interface{}{makeTestResponse(myRoomID, mySender, []string{ - "arg1", "arg 2", "arg 3", - })} - if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) - } -} - -func TestRunCommandsBestMatch(t *testing.T) { - plugins := []Plugin{makeTestPlugin([][]string{ - []string{"test", "command"}, - []string{"test", "command", "more", "specific"}, - }, nil)} - event := makeTestEvent("m.text", "!test command more specific arg1") - got := runCommands(plugins, event) - want := []interface{}{makeTestResponse(myRoomID, mySender, []string{ - "arg1", - })} - if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) - } -} - -func TestRunCommandsMultiplePlugins(t *testing.T) { - plugins := []Plugin{ - makeTestPlugin([][]string{[]string{"test", "command", "first"}}, nil), - makeTestPlugin([][]string{[]string{"test", "command"}}, nil), - } - event := makeTestEvent("m.text", "!test command first arg1") - got := runCommands(plugins, event) - want := []interface{}{ - makeTestResponse(myRoomID, mySender, []string{"arg1"}), - makeTestResponse(myRoomID, mySender, []string{"first", "arg1"}), - } - if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) - } -} - -func TestRunCommandsInvalidShell(t *testing.T) { - plugins := []Plugin{ - makeTestPlugin([][]string{[]string{"test", "command"}}, nil), - } - event := makeTestEvent("m.text", `!test command 'mismatched quotes"`) - got := runCommands(plugins, event) - want := []interface{}{ - makeTestResponse(myRoomID, mySender, []string{"'mismatched", `quotes"`}), - } - if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) - } -} - -func TestExpansion(t *testing.T) { - plugins := []Plugin{ - makeTestPlugin(nil, []*regexp.Regexp{ - regexp.MustCompile("a[^ ]*"), - regexp.MustCompile("b.."), - }), - } - event := makeTestEvent("m.text", "test banana for scale") - got := runCommands(plugins, event) - want := []interface{}{ - makeTestExpansion(myRoomID, mySender, []string{"anana"}), - makeTestExpansion(myRoomID, mySender, []string{"ale"}), - makeTestExpansion(myRoomID, mySender, []string{"ban"}), - } - if !reflect.DeepEqual(got, want) { - t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want) - } -} - -func TestExpansionDuplicateMatches(t *testing.T) { - plugins := []Plugin{ - makeTestPlugin(nil, []*regexp.Regexp{ - regexp.MustCompile("badger"), - }), - } - event := makeTestEvent("m.text", "badger badger badger") - got := runCommands(plugins, event) - want := []interface{}{ - makeTestExpansion(myRoomID, mySender, []string{"badger"}), - } - if !reflect.DeepEqual(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/services/echo/echo.go b/src/github.com/matrix-org/go-neb/services/echo/echo.go index 66a09ff..7c90b0c 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 @@ -1,10 +1,10 @@ package services import ( + "strings" + "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/types" - "strings" ) type echoService struct { @@ -16,14 +16,12 @@ type echoService struct { func (e *echoService) ServiceUserID() string { return e.serviceUserID } func (e *echoService) ServiceID() string { return e.id } func (e *echoService) ServiceType() string { return "echo" } -func (e *echoService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { - return plugin.Plugin{ - Commands: []plugin.Command{ - plugin.Command{ - Path: []string{"echo"}, - Command: func(roomID, userID string, args []string) (interface{}, error) { - return &matrix.TextMessage{"m.notice", strings.Join(args, " ")}, nil - }, +func (e *echoService) Commands(cli *matrix.Client, roomID string) []types.Command { + return []types.Command{ + types.Command{ + Path: []string{"echo"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return &matrix.TextMessage{"m.notice", strings.Join(args, " ")}, nil }, }, } diff --git a/src/github.com/matrix-org/go-neb/services/giphy/giphy.go b/src/github.com/matrix-org/go-neb/services/giphy/giphy.go index 969b9dd..0030221 100644 --- a/src/github.com/matrix-org/go-neb/services/giphy/giphy.go +++ b/src/github.com/matrix-org/go-neb/services/giphy/giphy.go @@ -13,7 +13,6 @@ import ( log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/plugin" "github.com/matrix-org/go-neb/types" ) @@ -50,14 +49,12 @@ func (s *Service) ServiceUserID() string { return s.serviceUserID } func (s *Service) ServiceID() string { return s.id } func (s *Service) ServiceType() string { return ServiceType } -func (s *Service) Plugin(client *matrix.Client, roomID string) plugin.Plugin { - return plugin.Plugin{ - Commands: []plugin.Command{ - plugin.Command{ - Path: []string{"giphy"}, - Command: func(roomID, userID string, args []string) (interface{}, error) { - return s.cmdGiphy(client, roomID, userID, args) - }, +func (s *Service) Commands(client *matrix.Client, roomID string) []types.Command { + return []types.Command{ + types.Command{ + Path: []string{"giphy"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return s.cmdGiphy(client, roomID, userID, args) }, }, } 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 a829666..4190711 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 @@ -3,17 +3,17 @@ package services import ( "database/sql" "fmt" + "regexp" + "strconv" + "strings" + 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/client" "github.com/matrix-org/go-neb/types" - "regexp" - "strconv" - "strings" ) // Matches alphanumeric then a /, then more alphanumeric then a #, then a number. @@ -126,59 +126,60 @@ func (s *githubService) expandIssue(roomID, userID, owner, repo string, issueNum } } -func (s *githubService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { - return plugin.Plugin{ - Commands: []plugin.Command{ - plugin.Command{ - Path: []string{"github", "create"}, - Command: func(roomID, userID string, args []string) (interface{}, error) { - return s.cmdGithubCreate(roomID, userID, args) - }, +func (s *githubService) Commands(cli *matrix.Client, roomID string) []types.Command { + return []types.Command{ + types.Command{ + Path: []string{"github", "create"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return s.cmdGithubCreate(roomID, userID, args) }, }, - Expansions: []plugin.Expansion{ - plugin.Expansion{ - Regexp: ownerRepoIssueRegex, - Expand: func(roomID, userID string, matchingGroups []string) interface{} { - // There's an optional group in the regex so matchingGroups can look like: - // [foo/bar#55 foo/bar foo bar 55] - // [#55 55] - if len(matchingGroups) != 5 { - log.WithField("groups", matchingGroups).WithField("len", len(matchingGroups)).Print( - "Unexpected number of groups", - ) + } +} + +func (s *githubService) Expansions(cli *matrix.Client, roomID string) []types.Expansion { + return []types.Expansion{ + types.Expansion{ + Regexp: ownerRepoIssueRegex, + Expand: func(roomID, userID string, matchingGroups []string) interface{} { + // There's an optional group in the regex so matchingGroups can look like: + // [foo/bar#55 foo/bar foo bar 55] + // [#55 55] + if len(matchingGroups) != 5 { + log.WithField("groups", matchingGroups).WithField("len", len(matchingGroups)).Print( + "Unexpected number of groups", + ) + return nil + } + if matchingGroups[1] == "" && matchingGroups[2] == "" && matchingGroups[3] == "" { + // issue only match, this only works if there is a default repo + defaultRepo := s.defaultRepo(roomID) + if defaultRepo == "" { return nil } - if matchingGroups[1] == "" && matchingGroups[2] == "" && matchingGroups[3] == "" { - // issue only match, this only works if there is a default repo - defaultRepo := s.defaultRepo(roomID) - if defaultRepo == "" { - return nil - } - segs := strings.Split(defaultRepo, "/") - if len(segs) != 2 { - log.WithFields(log.Fields{ - "room_id": roomID, - "default_repo": defaultRepo, - }).Error("Default repo is malformed") - return nil - } - // Fill in the missing fields in matching groups and fall through into ["foo/bar#11", "foo", "bar", "11"] - matchingGroups = []string{ - defaultRepo + matchingGroups[0], - defaultRepo, - segs[0], - segs[1], - matchingGroups[4], - } - } - num, err := strconv.Atoi(matchingGroups[4]) - if err != nil { - log.WithField("issue_number", matchingGroups[4]).Print("Bad issue number") + segs := strings.Split(defaultRepo, "/") + if len(segs) != 2 { + log.WithFields(log.Fields{ + "room_id": roomID, + "default_repo": defaultRepo, + }).Error("Default repo is malformed") return nil } - return s.expandIssue(roomID, userID, matchingGroups[2], matchingGroups[3], num) - }, + // Fill in the missing fields in matching groups and fall through into ["foo/bar#11", "foo", "bar", "11"] + matchingGroups = []string{ + defaultRepo + matchingGroups[0], + defaultRepo, + segs[0], + segs[1], + matchingGroups[4], + } + } + num, err := strconv.Atoi(matchingGroups[4]) + if err != nil { + log.WithField("issue_number", matchingGroups[4]).Print("Bad issue number") + return nil + } + return s.expandIssue(roomID, userID, matchingGroups[2], matchingGroups[3], num) }, }, } diff --git a/src/github.com/matrix-org/go-neb/services/guggy/guggy.go b/src/github.com/matrix-org/go-neb/services/guggy/guggy.go index 4d6a2d3..71ab92a 100644 --- a/src/github.com/matrix-org/go-neb/services/guggy/guggy.go +++ b/src/github.com/matrix-org/go-neb/services/guggy/guggy.go @@ -4,14 +4,14 @@ import ( "bytes" "encoding/json" "fmt" - log "github.com/Sirupsen/logrus" - "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/plugin" - "github.com/matrix-org/go-neb/types" "io/ioutil" "math" "net/http" "strings" + + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/matrix" + "github.com/matrix-org/go-neb/types" ) type guggyQuery struct { @@ -39,14 +39,12 @@ func (s *guggyService) ServiceUserID() string { return s.serviceUserID } func (s *guggyService) ServiceID() string { return s.id } func (s *guggyService) ServiceType() string { return "guggy" } -func (s *guggyService) Plugin(client *matrix.Client, roomID string) plugin.Plugin { - return plugin.Plugin{ - Commands: []plugin.Command{ - plugin.Command{ - Path: []string{"guggy"}, - Command: func(roomID, userID string, args []string) (interface{}, error) { - return s.cmdGuggy(client, roomID, userID, args) - }, +func (s *guggyService) Commands(client *matrix.Client, roomID string) []types.Command { + return []types.Command{ + types.Command{ + Path: []string{"guggy"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return s.cmdGuggy(client, roomID, userID, args) }, }, } 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 82eb18e..b62ed4f 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 @@ -4,19 +4,19 @@ import ( "database/sql" "errors" "fmt" + "html" + "net/http" + "regexp" + "strings" + log "github.com/Sirupsen/logrus" - "github.com/andygrunwald/go-jira" + jira "github.com/andygrunwald/go-jira" "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/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" - "net/http" - "regexp" - "strings" ) // Matches alphas then a -, then a number. E.g "FOO-123" @@ -199,22 +199,23 @@ func (s *jiraService) expandIssue(roomID, userID string, issueKeyGroups []string ) } -func (s *jiraService) Plugin(cli *matrix.Client, 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) { - return s.cmdJiraCreate(roomID, userID, args) - }, +func (s *jiraService) Commands(cli *matrix.Client, roomID string) []types.Command { + return []types.Command{ + types.Command{ + Path: []string{"jira", "create"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return s.cmdJiraCreate(roomID, userID, args) }, }, - Expansions: []plugin.Expansion{ - plugin.Expansion{ - Regexp: issueKeyRegex, - Expand: func(roomID, userID string, issueKeyGroups []string) interface{} { - return s.expandIssue(roomID, userID, issueKeyGroups) - }, + } +} + +func (s *jiraService) Expansions(cli *matrix.Client, roomID string) []types.Expansion { + return []types.Expansion{ + types.Expansion{ + Regexp: issueKeyRegex, + Expand: func(roomID, userID string, issueKeyGroups []string) interface{} { + return s.expandIssue(roomID, userID, issueKeyGroups) }, }, } diff --git a/src/github.com/matrix-org/go-neb/types/actions.go b/src/github.com/matrix-org/go-neb/types/actions.go new file mode 100644 index 0000000..707fa04 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/types/actions.go @@ -0,0 +1,36 @@ +package types + +import "regexp" + +// A Command is something that a user invokes by sending a message starting with '!' +// followed by a list of strings that name the command, followed by a list of argument +// strings. The argument strings may be quoted using '\"' and '\'' in the same way +// that they are quoted in the unix shell. +type Command struct { + Path []string + Arguments []string + Help string + Command func(roomID, userID string, arguments []string) (content interface{}, err error) +} + +// An Expansion is something that actives when the user sends any message +// containing a string matching a given pattern. For example an RFC expansion +// might expand "RFC 6214" into "Adaptation of RFC 1149 for IPv6" and link to +// the appropriate RFC. +type Expansion struct { + Regexp *regexp.Regexp + Expand func(roomID, userID string, matchingGroups []string) interface{} +} + +// Matches if the arguments start with the path of the command. +func (command *Command) Matches(arguments []string) bool { + if len(arguments) < len(command.Path) { + return false + } + for i, segment := range command.Path { + if segment != arguments[i] { + return false + } + } + return true +} diff --git a/src/github.com/matrix-org/go-neb/types/auth.go b/src/github.com/matrix-org/go-neb/types/auth.go new file mode 100644 index 0000000..27acda0 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/types/auth.go @@ -0,0 +1,56 @@ +package types + +import ( + "encoding/base64" + "encoding/json" + "errors" + "net/http" +) + +// AuthRealm represents a place where a user can authenticate themselves. +// This may static (like github.com) or a specific domain (like matrix.org/jira) +type AuthRealm interface { + ID() string + Type() string + Init() error + Register() error + OnReceiveRedirect(w http.ResponseWriter, req *http.Request) + AuthSession(id, userID, realmID string) AuthSession + RequestAuthSession(userID string, config json.RawMessage) interface{} +} + +var realmsByType = map[string]func(string, string) AuthRealm{} + +// RegisterAuthRealm registers a factory for creating AuthRealm instances. +func RegisterAuthRealm(factory func(string, string) AuthRealm) { + realmsByType[factory("", "").Type()] = factory +} + +// CreateAuthRealm creates an AuthRealm of the given type and realm ID. +// Returns an error if the realm couldn't be created or the JSON cannot be unmarshalled. +func CreateAuthRealm(realmID, realmType string, realmJSON []byte) (AuthRealm, error) { + f := realmsByType[realmType] + if f == nil { + return nil, errors.New("Unknown realm type: " + realmType) + } + base64RealmID := base64.RawURLEncoding.EncodeToString([]byte(realmID)) + redirectURL := baseURL + "realms/redirects/" + base64RealmID + r := f(realmID, redirectURL) + if err := json.Unmarshal(realmJSON, r); err != nil { + return nil, err + } + if err := r.Init(); err != nil { + return nil, err + } + return r, nil +} + +// AuthSession represents a single authentication session between a user and +// an auth realm. +type AuthSession interface { + ID() string + UserID() string + RealmID() string + Authenticated() bool + Info() interface{} +} diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/service.go similarity index 70% rename from src/github.com/matrix-org/go-neb/types/types.go rename to src/github.com/matrix-org/go-neb/types/service.go index ed1ae38..29f4e1f 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/service.go @@ -4,11 +4,11 @@ import ( "encoding/base64" "encoding/json" "errors" - "github.com/matrix-org/go-neb/matrix" - "github.com/matrix-org/go-neb/plugin" "net/http" "strings" "time" + + "github.com/matrix-org/go-neb/matrix" ) // BotOptions for a given bot user in a given room @@ -34,7 +34,8 @@ type Service interface { ServiceID() string // Return the type of service. This string MUST NOT change. ServiceType() string - Plugin(cli *matrix.Client, roomID string) plugin.Plugin + Commands(cli *matrix.Client, roomID string) []Command + Expansions(cli *matrix.Client, roomID string) []Expansion OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) // A lifecycle function which is invoked when the service is being registered. The old service, if one exists, is provided, // along with a Client instance for ServiceUserID(). If this function returns an error, the service will not be registered @@ -51,9 +52,14 @@ type Service interface { // DefaultService NO-OPs the implementation of optional Service interface methods. Feel free to override them. type DefaultService struct{} -// Plugin returns no plugins. -func (s *DefaultService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { - return plugin.Plugin{} +// Commands returns no commands. +func (s *DefaultService) Commands(cli *matrix.Client, roomID string) []Command { + return []Command{} +} + +// Expansions returns no expansions. +func (s *DefaultService) Expansions(cli *matrix.Client, roomID string) []Expansion { + return []Expansion{} } // Register does nothing and returns no error. @@ -122,51 +128,3 @@ func CreateService(serviceID, serviceType, serviceUserID string, serviceJSON []b } return service, nil } - -// AuthRealm represents a place where a user can authenticate themselves. -// This may static (like github.com) or a specific domain (like matrix.org/jira) -type AuthRealm interface { - ID() string - Type() string - Init() error - Register() error - OnReceiveRedirect(w http.ResponseWriter, req *http.Request) - AuthSession(id, userID, realmID string) AuthSession - RequestAuthSession(userID string, config json.RawMessage) interface{} -} - -var realmsByType = map[string]func(string, string) AuthRealm{} - -// RegisterAuthRealm registers a factory for creating AuthRealm instances. -func RegisterAuthRealm(factory func(string, string) AuthRealm) { - realmsByType[factory("", "").Type()] = factory -} - -// CreateAuthRealm creates an AuthRealm of the given type and realm ID. -// Returns an error if the realm couldn't be created or the JSON cannot be unmarshalled. -func CreateAuthRealm(realmID, realmType string, realmJSON []byte) (AuthRealm, error) { - f := realmsByType[realmType] - if f == nil { - return nil, errors.New("Unknown realm type: " + realmType) - } - base64RealmID := base64.RawURLEncoding.EncodeToString([]byte(realmID)) - redirectURL := baseURL + "realms/redirects/" + base64RealmID - r := f(realmID, redirectURL) - if err := json.Unmarshal(realmJSON, r); err != nil { - return nil, err - } - if err := r.Init(); err != nil { - return nil, err - } - return r, nil -} - -// AuthSession represents a single authentication session between a user and -// an auth realm. -type AuthSession interface { - ID() string - UserID() string - RealmID() string - Authenticated() bool - Info() interface{} -}