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
-
18src/github.com/matrix-org/go-neb/services/echo/echo.go
-
15src/github.com/matrix-org/go-neb/services/giphy/giphy.go
-
103src/github.com/matrix-org/go-neb/services/github/github.go
-
22src/github.com/matrix-org/go-neb/services/guggy/guggy.go
-
41src/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