diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/app.go | 4 | ||||
-rw-r--r-- | app/apptestlib.go | 73 | ||||
-rw-r--r-- | app/command.go | 24 | ||||
-rw-r--r-- | app/plugin.go | 147 | ||||
-rw-r--r-- | app/plugin_api.go | 9 | ||||
-rw-r--r-- | app/plugin_test.go | 97 |
6 files changed, 329 insertions, 25 deletions
diff --git a/app/app.go b/app/app.go index fd313c9c9..959c99306 100644 --- a/app/app.go +++ b/app/app.go @@ -9,6 +9,7 @@ import ( "net/http" "runtime/debug" "strings" + "sync" "sync/atomic" l4g "github.com/alecthomas/log4go" @@ -60,6 +61,9 @@ type App struct { sessionCache *utils.Cache roles map[string]*model.Role configListenerId string + + pluginCommands []*PluginCommand + pluginCommandsLock sync.RWMutex } var appCount = 0 diff --git a/app/apptestlib.go b/app/apptestlib.go index 63a064d7f..618ad809a 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -4,15 +4,21 @@ package app import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" "time" + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/pluginenv" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store/sqlstore" "github.com/mattermost/mattermost-server/store/storetest" "github.com/mattermost/mattermost-server/utils" - - l4g "github.com/alecthomas/log4go" ) type TestHelper struct { @@ -22,6 +28,9 @@ type TestHelper struct { BasicUser2 *model.User BasicChannel *model.Channel BasicPost *model.Post + + tempWorkspace string + pluginHooks map[string]plugin.Hooks } type persistentTestStore struct { @@ -54,7 +63,8 @@ func setupTestHelper(enterprise bool) *TestHelper { } th := &TestHelper{ - App: New(options...), + App: New(options...), + pluginHooks: make(map[string]plugin.Hooks), } th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) @@ -223,4 +233,61 @@ func (me *TestHelper) TearDown() { StopTestStore() panic(err) } + if me.tempWorkspace != "" { + os.RemoveAll(me.tempWorkspace) + } +} + +type mockPluginSupervisor struct { + hooks plugin.Hooks +} + +func (s *mockPluginSupervisor) Start(api plugin.API) error { + return s.hooks.OnActivate(api) +} + +func (s *mockPluginSupervisor) Stop() error { + return nil +} + +func (s *mockPluginSupervisor) Hooks() plugin.Hooks { + return s.hooks +} + +func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) { + if me.tempWorkspace == "" { + dir, err := ioutil.TempDir("", "apptest") + if err != nil { + panic(err) + } + me.tempWorkspace = dir + } + + pluginDir := filepath.Join(me.tempWorkspace, "plugins") + webappDir := filepath.Join(me.tempWorkspace, "webapp") + me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) { + if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok { + return &mockPluginSupervisor{hooks}, nil + } + return pluginenv.DefaultSupervisorProvider(bundle) + }) + + me.pluginHooks[manifest.Id] = hooks + + manifestCopy := *manifest + if manifestCopy.Backend == nil { + manifestCopy.Backend = &model.ManifestBackend{} + } + manifestBytes, err := json.Marshal(&manifestCopy) + if err != nil { + panic(err) + } + + if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil { + panic(err) + } + + if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil { + panic(err) + } } diff --git a/app/command.go b/app/command.go index dc65de6e2..4c26eae71 100644 --- a/app/command.go +++ b/app/command.go @@ -75,6 +75,13 @@ func (a *App) ListAutocompleteCommands(teamId string, T goi18n.TranslateFunc) ([ } } + for _, cmd := range a.PluginCommandsForTeam(teamId) { + if cmd.AutoComplete && !seen[cmd.Trigger] { + seen[cmd.Trigger] = true + commands = append(commands, cmd) + } + } + if *a.Config().ServiceSettings.EnableCommands { if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil { return nil, result.Err @@ -111,7 +118,7 @@ func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.C for _, value := range commandProviders { if cmd := value.GetCommand(a, T); cmd != nil { cpy := *cmd - if cpy.AutoComplete && !seen[cpy.Id] { + if cpy.AutoComplete && !seen[cpy.Trigger] { cpy.Sanitize() seen[cpy.Trigger] = true commands = append(commands, &cpy) @@ -119,13 +126,20 @@ func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.C } } + for _, cmd := range a.PluginCommandsForTeam(teamId) { + if !seen[cmd.Trigger] { + seen[cmd.Trigger] = true + commands = append(commands, cmd) + } + } + if *a.Config().ServiceSettings.EnableCommands { if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil { return nil, result.Err } else { teamCmds := result.Data.([]*model.Command) for _, cmd := range teamCmds { - if !seen[cmd.Id] { + if !seen[cmd.Trigger] { cmd.Sanitize() seen[cmd.Trigger] = true commands = append(commands, cmd) @@ -151,6 +165,12 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, * } } + if cmd, response, err := a.ExecutePluginCommand(args); err != nil { + return nil, err + } else if cmd != nil { + return a.HandleCommandResponse(cmd, args, response, true) + } + if !*a.Config().ServiceSettings.EnableCommands { return nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) } diff --git a/app/plugin.go b/app/plugin.go index f91a2e414..661f6ed5d 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -8,6 +8,7 @@ import ( "context" "crypto/sha256" "encoding/base64" + "fmt" "io" "io/ioutil" "net/http" @@ -101,20 +102,28 @@ func (a *App) ActivatePlugins() { l4g.Info("Activated %v plugin", id) } else if !pluginState.Enable && active { - if err := a.PluginEnv.DeactivatePlugin(id); err != nil { + if err := a.deactivatePlugin(plugin.Manifest); err != nil { l4g.Error(err.Error()) - continue } + } + } +} - if plugin.Manifest.HasClient() { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil) - message.Add("manifest", plugin.Manifest.ClientManifest()) - a.Publish(message) - } +func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { + if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil { + return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) + } - l4g.Info("Deactivated %v plugin", id) - } + a.UnregisterPluginCommands(manifest.Id) + + if manifest.HasClient() { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil) + message.Add("manifest", manifest.ClientManifest()) + a.Publish(message) } + + l4g.Info("Deactivated %v plugin", manifest.Id) + return nil } // InstallPlugin unpacks and installs a plugin but does not activate it. @@ -253,15 +262,9 @@ func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError { } if a.PluginEnv.IsPluginActive(id) { - err := a.PluginEnv.DeactivatePlugin(id) + err := a.deactivatePlugin(manifest) if err != nil { - return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) - } - - if manifest.HasClient() { - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil) - message.Add("manifest", manifest.ClientManifest()) - a.Publish(message) + return err } } @@ -341,7 +344,7 @@ func (a *App) DisablePlugin(id string) *model.AppError { return nil } -func (a *App) InitPlugins(pluginPath, webappPath string) { +func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) { if !*a.Config().PluginSettings.Enable { return } @@ -362,7 +365,7 @@ func (a *App) InitPlugins(pluginPath, webappPath string) { return } - if env, err := pluginenv.New( + options := []pluginenv.Option{ pluginenv.SearchPath(pluginPath), pluginenv.WebappPath(webappPath), pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) { @@ -375,7 +378,13 @@ func (a *App) InitPlugins(pluginPath, webappPath string) { }, }, nil }), - ); err != nil { + } + + if supervisorOverride != nil { + options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) + } + + if env, err := pluginenv.New(options...); err != nil { l4g.Error("failed to start up plugins: " + err.Error()) return } else { @@ -533,3 +542,101 @@ func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError { return result.Err } + +type PluginCommand struct { + Command *model.Command + PluginId string +} + +func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error { + if command.Trigger == "" { + return fmt.Errorf("invalid command") + } + + command = &model.Command{ + Trigger: strings.ToLower(command.Trigger), + TeamId: command.TeamId, + AutoComplete: command.AutoComplete, + AutoCompleteDesc: command.AutoCompleteDesc, + DisplayName: command.DisplayName, + } + + a.pluginCommandsLock.Lock() + defer a.pluginCommandsLock.Unlock() + + for _, pc := range a.pluginCommands { + if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId { + if pc.PluginId == pluginId { + pc.Command = command + return nil + } + } + } + + a.pluginCommands = append(a.pluginCommands, &PluginCommand{ + Command: command, + PluginId: pluginId, + }) + return nil +} + +func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) { + trigger = strings.ToLower(trigger) + + a.pluginCommandsLock.Lock() + defer a.pluginCommandsLock.Unlock() + + var remaining []*PluginCommand + for _, pc := range a.pluginCommands { + if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger { + remaining = append(remaining, pc) + } + } + a.pluginCommands = remaining +} + +func (a *App) UnregisterPluginCommands(pluginId string) { + a.pluginCommandsLock.Lock() + defer a.pluginCommandsLock.Unlock() + + var remaining []*PluginCommand + for _, pc := range a.pluginCommands { + if pc.PluginId != pluginId { + remaining = append(remaining, pc) + } + } + a.pluginCommands = remaining +} + +func (a *App) PluginCommandsForTeam(teamId string) []*model.Command { + a.pluginCommandsLock.RLock() + defer a.pluginCommandsLock.RUnlock() + + var commands []*model.Command + for _, pc := range a.pluginCommands { + if pc.Command.TeamId == "" || pc.Command.TeamId == teamId { + commands = append(commands, pc.Command) + } + } + return commands +} + +func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) { + parts := strings.Split(args.Command, " ") + trigger := parts[0][1:] + trigger = strings.ToLower(trigger) + + a.pluginCommandsLock.RLock() + defer a.pluginCommandsLock.RUnlock() + + for _, pc := range a.pluginCommands { + if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { + response, appErr, err := a.PluginEnv.HooksForPlugin(pc.PluginId).ExecuteCommand(args) + if err != nil { + return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return pc.Command, response, appErr + } + } + return nil, nil, nil +} diff --git a/app/plugin_api.go b/app/plugin_api.go index 9965f770a..21b828368 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -34,6 +34,15 @@ func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error { } } +func (api *PluginAPI) RegisterCommand(command *model.Command) error { + return api.app.RegisterPluginCommand(api.id, command) +} + +func (api *PluginAPI) UnregisterCommand(teamId, trigger string) error { + api.app.UnregisterPluginCommand(api.id, teamId, trigger) + return nil +} + func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) { return api.app.CreateTeam(team) } diff --git a/app/plugin_test.go b/app/plugin_test.go index 5c70cbc4f..4794d2704 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -13,6 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/plugintest" ) func TestPluginKeyValueStore(t *testing.T) { @@ -98,3 +100,98 @@ func TestHandlePluginRequest(t *testing.T) { } router.ServeHTTP(nil, r) } + +type testPlugin struct { + plugintest.Hooks +} + +func (p *testPlugin) OnConfigurationChange() error { + return nil +} + +func (p *testPlugin) OnDeactivate() error { + return nil +} + +type pluginCommandTestPlugin struct { + testPlugin + + TeamId string +} + +func (p *pluginCommandTestPlugin) OnActivate(api plugin.API) error { + if err := api.RegisterCommand(&model.Command{ + Trigger: "foo", + TeamId: p.TeamId, + }); err != nil { + return err + } + if err := api.RegisterCommand(&model.Command{ + Trigger: "foo2", + TeamId: p.TeamId, + }); err != nil { + return err + } + return api.UnregisterCommand(p.TeamId, "foo2") +} + +func (p *pluginCommandTestPlugin) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + if args.Command == "/foo" { + return &model.CommandResponse{ + Text: "bar", + }, nil + } + return nil, model.NewAppError("ExecuteCommand", "this is an error", nil, "", http.StatusBadRequest) +} + +func TestPluginCommands(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.InstallPlugin(&model.Manifest{ + Id: "foo", + }, &pluginCommandTestPlugin{ + TeamId: th.BasicTeam.Id, + }) + + require.Nil(t, th.App.EnablePlugin("foo")) + + resp, err := th.App.ExecuteCommand(&model.CommandArgs{ + Command: "/foo2", + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + }) + require.NotNil(t, err) + assert.Equal(t, http.StatusNotFound, err.StatusCode) + + resp, err = th.App.ExecuteCommand(&model.CommandArgs{ + Command: "/foo", + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + }) + require.Nil(t, err) + assert.Equal(t, "bar", resp.Text) + + resp, err = th.App.ExecuteCommand(&model.CommandArgs{ + Command: "/foo baz", + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + }) + require.NotNil(t, err) + require.Equal(t, "this is an error", err.Message) + assert.Nil(t, resp) + + require.Nil(t, th.App.RemovePlugin("foo")) + + resp, err = th.App.ExecuteCommand(&model.CommandArgs{ + Command: "/foo", + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + }) + require.NotNil(t, err) + assert.Equal(t, http.StatusNotFound, err.StatusCode) +} |