summaryrefslogtreecommitdiffstats
path: root/app
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 /app
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 'app')
-rw-r--r--app/app.go4
-rw-r--r--app/apptestlib.go73
-rw-r--r--app/command.go24
-rw-r--r--app/plugin.go147
-rw-r--r--app/plugin_api.go9
-rw-r--r--app/plugin_test.go97
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)
+}