Add plugin slash command support (#7941)
* add plugin slash command support * remove unused string * rebase
Этот коммит содержится в:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
149
app/plugin.go
149
app/plugin.go
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -101,22 +102,30 @@ 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)
|
||||
}
|
||||
|
||||
l4g.Info("Deactivated %v plugin", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, false)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user