summaryrefslogtreecommitdiffstats
path: root/plugin
diff options
context:
space:
mode:
authorChris <ccbrown112@gmail.com>2017-12-08 13:55:41 -0600
committerGitHub <noreply@github.com>2017-12-08 13:55:41 -0600
commit4c17bdff1bb871fb31520b7b547f584c53ed854f (patch)
treeedf1e3295d6ff7d67281efc585b2e913b4efda3d /plugin
parent7ed1177a2b676aa4c93515268642c855cfe57a37 (diff)
downloadchat-4c17bdff1bb871fb31520b7b547f584c53ed854f.tar.gz
chat-4c17bdff1bb871fb31520b7b547f584c53ed854f.tar.bz2
chat-4c17bdff1bb871fb31520b7b547f584c53ed854f.zip
Add plugin slash command support (#7941)
* add plugin slash command support * remove unused string * rebase
Diffstat (limited to 'plugin')
-rw-r--r--plugin/api.go7
-rw-r--r--plugin/hooks.go6
-rw-r--r--plugin/pluginenv/environment.go64
-rw-r--r--plugin/pluginenv/environment_test.go48
-rw-r--r--plugin/plugintest/api.go16
-rw-r--r--plugin/plugintest/hooks.go17
-rw-r--r--plugin/rpcplugin/api.go24
-rw-r--r--plugin/rpcplugin/api_test.go11
-rw-r--r--plugin/rpcplugin/hooks.go29
-rw-r--r--plugin/rpcplugin/hooks_test.go12
10 files changed, 224 insertions, 10 deletions
diff --git a/plugin/api.go b/plugin/api.go
index fee55eeff..437188f6e 100644
--- a/plugin/api.go
+++ b/plugin/api.go
@@ -16,6 +16,13 @@ type API interface {
// struct that the configuration JSON can be unmarshalled to.
LoadPluginConfiguration(dest interface{}) error
+ // RegisterCommand registers a custom slash command. When the command is triggered, your plugin
+ // can fulfill it via the ExecuteCommand hook.
+ RegisterCommand(command *model.Command) error
+
+ // UnregisterCommand unregisters a command previously registered via RegisterCommand.
+ UnregisterCommand(teamId, trigger string) error
+
// CreateUser creates a user.
CreateUser(user *model.User) (*model.User, *model.AppError)
diff --git a/plugin/hooks.go b/plugin/hooks.go
index 04d5c7c14..814609e8c 100644
--- a/plugin/hooks.go
+++ b/plugin/hooks.go
@@ -5,6 +5,8 @@ package plugin
import (
"net/http"
+
+ "github.com/mattermost/mattermost-server/model"
)
// Methods from the Hooks interface can be used by a plugin to respond to events. Methods are likely
@@ -30,4 +32,8 @@ type Hooks interface {
// The Mattermost-User-Id header will be present if (and only if) the request is by an
// authenticated user.
ServeHTTP(http.ResponseWriter, *http.Request)
+
+ // ExecuteCommand executes a command that has been previously registered via the RegisterCommand
+ // API.
+ ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
}
diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go
index 26511c651..f53021f74 100644
--- a/plugin/pluginenv/environment.go
+++ b/plugin/pluginenv/environment.go
@@ -223,35 +223,59 @@ func (env *Environment) Shutdown() (errs []error) {
return
}
-type EnvironmentHooks struct {
+type MultiPluginHooks struct {
env *Environment
}
-func (env *Environment) Hooks() *EnvironmentHooks {
- return &EnvironmentHooks{env}
+type SinglePluginHooks struct {
+ env *Environment
+ pluginId string
}
-// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
-// encountered will be returned.
-func (h *EnvironmentHooks) OnConfigurationChange() (errs []error) {
+func (env *Environment) Hooks() *MultiPluginHooks {
+ return &MultiPluginHooks{
+ env: env,
+ }
+}
+
+func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks {
+ return &SinglePluginHooks{
+ env: env,
+ pluginId: id,
+ }
+}
+
+func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) {
h.env.mutex.RLock()
defer h.env.mutex.RUnlock()
+
for _, activePlugin := range h.env.activePlugins {
if activePlugin.Supervisor == nil {
continue
}
- if err := activePlugin.Supervisor.Hooks().OnConfigurationChange(); err != nil {
- errs = append(errs, errors.Wrapf(err, "OnConfigurationChange error for %v", activePlugin.BundleInfo.Manifest.Id))
+ if err := f(activePlugin.Supervisor.Hooks()); err != nil {
+ errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
}
}
return
}
+// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
+// encountered will be returned.
+func (h *MultiPluginHooks) OnConfigurationChange() []error {
+ return h.invoke(func(hooks plugin.Hooks) error {
+ if err := hooks.OnConfigurationChange(); err != nil {
+ return errors.Wrapf(err, "error calling OnConfigurationChange hook")
+ }
+ return nil
+ })
+}
+
// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
// 404 not found.
//
// It expects the request's context to have a plugin_id set.
-func (h *EnvironmentHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if id := r.Context().Value("plugin_id"); id != nil {
if idstr, ok := id.(string); ok {
h.env.mutex.RLock()
@@ -264,3 +288,25 @@ func (h *EnvironmentHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
http.NotFound(w, r)
}
+
+func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error {
+ h.env.mutex.RLock()
+ defer h.env.mutex.RUnlock()
+
+ if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil {
+ if err := f(activePlugin.Supervisor.Hooks()); err != nil {
+ return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id)
+ }
+ return nil
+ }
+ return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId)
+}
+
+// ExecuteCommand invokes the ExecuteCommand hook for the plugin.
+func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) {
+ err = h.invoke(func(hooks plugin.Hooks) error {
+ resp, appErr = hooks.ExecuteCommand(args)
+ return nil
+ })
+ return
+}
diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go
index 988e5b08f..2a52b3830 100644
--- a/plugin/pluginenv/environment_test.go
+++ b/plugin/pluginenv/environment_test.go
@@ -355,3 +355,51 @@ func TestEnvironment_ConcurrentHookInvocations(t *testing.T) {
wg.Wait()
}
+
+func TestEnvironment_HooksForPlugins(t *testing.T) {
+ dir := initTmpDir(t, map[string]string{
+ "foo/plugin.json": `{"id": "foo", "backend": {}}`,
+ })
+ defer os.RemoveAll(dir)
+
+ var provider MockProvider
+ defer provider.AssertExpectations(t)
+
+ env, err := New(
+ SearchPath(dir),
+ APIProvider(provider.API),
+ SupervisorProvider(provider.Supervisor),
+ )
+ require.NoError(t, err)
+ defer env.Shutdown()
+
+ var api struct{ plugin.API }
+ var supervisor MockSupervisor
+ defer supervisor.AssertExpectations(t)
+ var hooks plugintest.Hooks
+ defer hooks.AssertExpectations(t)
+
+ provider.On("API").Return(&api, nil)
+ provider.On("Supervisor").Return(&supervisor, nil)
+
+ supervisor.On("Start", &api).Return(nil)
+ supervisor.On("Stop").Return(nil)
+ supervisor.On("Hooks").Return(&hooks)
+
+ hooks.On("OnDeactivate").Return(nil)
+ hooks.On("ExecuteCommand", mock.AnythingOfType("*model.CommandArgs")).Return(&model.CommandResponse{
+ Text: "bar",
+ }, nil)
+
+ assert.NoError(t, env.ActivatePlugin("foo"))
+ assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
+
+ resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{
+ Command: "/foo",
+ })
+ assert.Equal(t, "bar", resp.Text)
+ assert.Nil(t, appErr)
+ assert.NoError(t, err)
+
+ assert.Empty(t, env.Shutdown())
+}
diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go
index b00542032..75174a9a6 100644
--- a/plugin/plugintest/api.go
+++ b/plugin/plugintest/api.go
@@ -30,6 +30,22 @@ func (m *API) LoadPluginConfiguration(dest interface{}) error {
return ret.Error(0)
}
+func (m *API) RegisterCommand(command *model.Command) error {
+ ret := m.Called(command)
+ if f, ok := ret.Get(0).(func(*model.Command) error); ok {
+ return f(command)
+ }
+ return ret.Error(0)
+}
+
+func (m *API) UnregisterCommand(teamId, trigger string) error {
+ ret := m.Called(teamId, trigger)
+ if f, ok := ret.Get(0).(func(string, string) error); ok {
+ return f(teamId, trigger)
+ }
+ return ret.Error(0)
+}
+
func (m *API) CreateUser(user *model.User) (*model.User, *model.AppError) {
ret := m.Called(user)
if f, ok := ret.Get(0).(func(*model.User) (*model.User, *model.AppError)); ok {
diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go
index 56d048d6a..9ea11a9fb 100644
--- a/plugin/plugintest/hooks.go
+++ b/plugin/plugintest/hooks.go
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/mock"
+ "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
)
@@ -18,7 +19,11 @@ type Hooks struct {
var _ plugin.Hooks = (*Hooks)(nil)
func (m *Hooks) OnActivate(api plugin.API) error {
- return m.Called(api).Error(0)
+ ret := m.Called(api)
+ if f, ok := ret.Get(0).(func(plugin.API) error); ok {
+ return f(api)
+ }
+ return ret.Error(0)
}
func (m *Hooks) OnDeactivate() error {
@@ -32,3 +37,13 @@ func (m *Hooks) OnConfigurationChange() error {
func (m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.Called(w, r)
}
+
+func (m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
+ ret := m.Called(args)
+ if f, ok := ret.Get(0).(func(*model.CommandArgs) (*model.CommandResponse, *model.AppError)); ok {
+ return f(args)
+ }
+ resp, _ := ret.Get(0).(*model.CommandResponse)
+ err, _ := ret.Get(1).(*model.AppError)
+ return resp, err
+}
diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go
index 76c6e3039..5b5b11a62 100644
--- a/plugin/rpcplugin/api.go
+++ b/plugin/rpcplugin/api.go
@@ -32,6 +32,14 @@ func (api *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error
return nil
}
+func (api *LocalAPI) RegisterCommand(args *model.Command, reply *APITeamReply) error {
+ return api.api.RegisterCommand(args)
+}
+
+func (api *LocalAPI) UnregisterCommand(args *APIUnregisterCommandArgs, reply *APITeamReply) error {
+ return api.api.UnregisterCommand(args.TeamId, args.Trigger)
+}
+
type APIErrorReply struct {
Error *model.AppError
}
@@ -344,6 +352,22 @@ func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
return json.Unmarshal(config, dest)
}
+func (api *RemoteAPI) RegisterCommand(command *model.Command) error {
+ return api.client.Call("LocalAPI.RegisterCommand", command, nil)
+}
+
+type APIUnregisterCommandArgs struct {
+ TeamId string
+ Trigger string
+}
+
+func (api *RemoteAPI) UnregisterCommand(teamId, trigger string) error {
+ return api.client.Call("LocalAPI.UnregisterCommand", &APIUnregisterCommandArgs{
+ TeamId: teamId,
+ Trigger: trigger,
+ }, nil)
+}
+
func (api *RemoteAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
var reply APIUserReply
if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil {
diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go
index f9e474d4a..145ec9005 100644
--- a/plugin/rpcplugin/api_test.go
+++ b/plugin/rpcplugin/api_test.go
@@ -2,6 +2,7 @@ package rpcplugin
import (
"encoding/json"
+ "fmt"
"io"
"net/http"
"testing"
@@ -84,6 +85,16 @@ func TestAPI(t *testing.T) {
assert.Equal(t, "foo", config.Foo)
assert.Equal(t, "baz", config.Bar.Baz)
+ api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(fmt.Errorf("foo")).Once()
+ assert.Error(t, remote.RegisterCommand(&model.Command{}))
+ api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(nil).Once()
+ assert.NoError(t, remote.RegisterCommand(&model.Command{}))
+
+ api.On("UnregisterCommand", "team", "trigger").Return(fmt.Errorf("foo")).Once()
+ assert.Error(t, remote.UnregisterCommand("team", "trigger"))
+ api.On("UnregisterCommand", "team", "trigger").Return(nil).Once()
+ assert.NoError(t, remote.UnregisterCommand("team", "trigger"))
+
api.On("CreateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) (*model.Channel, *model.AppError) {
c.Id = "thechannelid"
return c, nil
diff --git a/plugin/rpcplugin/hooks.go b/plugin/rpcplugin/hooks.go
index 22f26e22e..7b44d0de7 100644
--- a/plugin/rpcplugin/hooks.go
+++ b/plugin/rpcplugin/hooks.go
@@ -11,6 +11,7 @@ import (
"net/rpc"
"reflect"
+ "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
)
@@ -125,6 +126,20 @@ func (h *LocalHooks) ServeHTTP(args ServeHTTPArgs, reply *struct{}) error {
return nil
}
+type HooksExecuteCommandReply struct {
+ Response *model.CommandResponse
+ Error *model.AppError
+}
+
+func (h *LocalHooks) ExecuteCommand(args *model.CommandArgs, reply *HooksExecuteCommandReply) error {
+ if hook, ok := h.hooks.(interface {
+ ExecuteCommand(*model.CommandArgs) (*model.CommandResponse, *model.AppError)
+ }); ok {
+ reply.Response, reply.Error = hook.ExecuteCommand(args)
+ }
+ return nil
+}
+
func ServeHooks(hooks interface{}, conn io.ReadWriteCloser, muxer *Muxer) {
server := rpc.NewServer()
server.Register(&LocalHooks{
@@ -141,6 +156,7 @@ const (
remoteOnDeactivate = 1
remoteServeHTTP = 2
remoteOnConfigurationChange = 3
+ remoteExecuteCommand = 4
maxRemoteHookCount = iota
)
@@ -225,6 +241,17 @@ func (h *RemoteHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+func (h *RemoteHooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
+ if !h.implemented[remoteExecuteCommand] {
+ return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err=ExecuteCommand hook not implemented", http.StatusInternalServerError)
+ }
+ var reply HooksExecuteCommandReply
+ if err := h.client.Call("LocalHooks.ExecuteCommand", args, &reply); err != nil {
+ return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
+ }
+ return reply.Response, reply.Error
+}
+
func (h *RemoteHooks) Close() error {
if h.apiCloser != nil {
h.apiCloser.Close()
@@ -253,6 +280,8 @@ func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer) (*RemoteHooks, error) {
remote.implemented[remoteOnConfigurationChange] = true
case "ServeHTTP":
remote.implemented[remoteServeHTTP] = true
+ case "ExecuteCommand":
+ remote.implemented[remoteExecuteCommand] = true
}
}
return remote, nil
diff --git a/plugin/rpcplugin/hooks_test.go b/plugin/rpcplugin/hooks_test.go
index 37c529510..116038dae 100644
--- a/plugin/rpcplugin/hooks_test.go
+++ b/plugin/rpcplugin/hooks_test.go
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+ "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/plugin/plugintest"
)
@@ -79,6 +80,17 @@ func TestHooks(t *testing.T) {
body, err := ioutil.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "bar", string(body))
+
+ hooks.On("ExecuteCommand", &model.CommandArgs{
+ Command: "/foo",
+ }).Return(&model.CommandResponse{
+ Text: "bar",
+ }, nil)
+ commandResponse, appErr := hooks.ExecuteCommand(&model.CommandArgs{
+ Command: "/foo",
+ })
+ assert.Equal(t, "bar", commandResponse.Text)
+ assert.Nil(t, appErr)
}))
}