mirror of https://github.com/matrix-org/go-neb.git
Browse Source
Remove Plugin. Replace with Commands() and Expansions()
Remove Plugin. Replace with Commands() and Expansions()
Also split out `types.go` into separate files by the area they cover (services, auth, actions).kegan/remove-plugin
Kegan Dougal
8 years ago
11 changed files with 311 additions and 502 deletions
-
118src/github.com/matrix-org/go-neb/clients/clients.go
-
179src/github.com/matrix-org/go-neb/plugin/plugin.go
-
159src/github.com/matrix-org/go-neb/plugin/plugin_test.go
-
12src/github.com/matrix-org/go-neb/services/echo/echo.go
-
9src/github.com/matrix-org/go-neb/services/giphy/giphy.go
-
25src/github.com/matrix-org/go-neb/services/github/github.go
-
16src/github.com/matrix-org/go-neb/services/guggy/guggy.go
-
29src/github.com/matrix-org/go-neb/services/jira/jira.go
-
36src/github.com/matrix-org/go-neb/types/actions.go
-
56src/github.com/matrix-org/go-neb/types/auth.go
-
66src/github.com/matrix-org/go-neb/types/service.go
@ -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") |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -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) |
|
||||
} |
|
||||
} |
|
@ -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 |
||||
|
} |
@ -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{} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue