Add plugin slash command support (#7941)
* add plugin slash command support * remove unused string * rebase
Этот коммит содержится в:
@@ -46,7 +46,7 @@ func TestPlugin(t *testing.T) {
|
|||||||
*cfg.PluginSettings.EnableUploads = true
|
*cfg.PluginSettings.EnableUploads = true
|
||||||
})
|
})
|
||||||
|
|
||||||
th.App.InitPlugins(pluginDir, webappDir)
|
th.App.InitPlugins(pluginDir, webappDir, nil)
|
||||||
defer func() {
|
defer func() {
|
||||||
th.App.ShutDownPlugins()
|
th.App.ShutDownPlugins()
|
||||||
th.App.PluginEnv = nil
|
th.App.PluginEnv = nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
l4g "github.com/alecthomas/log4go"
|
||||||
@@ -60,6 +61,9 @@ type App struct {
|
|||||||
sessionCache *utils.Cache
|
sessionCache *utils.Cache
|
||||||
roles map[string]*model.Role
|
roles map[string]*model.Role
|
||||||
configListenerId string
|
configListenerId string
|
||||||
|
|
||||||
|
pluginCommands []*PluginCommand
|
||||||
|
pluginCommandsLock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var appCount = 0
|
var appCount = 0
|
||||||
|
|||||||
@@ -4,15 +4,21 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
l4g "github.com/alecthomas/log4go"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"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"
|
||||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||||
"github.com/mattermost/mattermost-server/store/storetest"
|
"github.com/mattermost/mattermost-server/store/storetest"
|
||||||
"github.com/mattermost/mattermost-server/utils"
|
"github.com/mattermost/mattermost-server/utils"
|
||||||
|
|
||||||
l4g "github.com/alecthomas/log4go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestHelper struct {
|
type TestHelper struct {
|
||||||
@@ -22,6 +28,9 @@ type TestHelper struct {
|
|||||||
BasicUser2 *model.User
|
BasicUser2 *model.User
|
||||||
BasicChannel *model.Channel
|
BasicChannel *model.Channel
|
||||||
BasicPost *model.Post
|
BasicPost *model.Post
|
||||||
|
|
||||||
|
tempWorkspace string
|
||||||
|
pluginHooks map[string]plugin.Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
type persistentTestStore struct {
|
type persistentTestStore struct {
|
||||||
@@ -55,6 +64,7 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
|||||||
|
|
||||||
th := &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 })
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
|
||||||
@@ -223,4 +233,61 @@ func (me *TestHelper) TearDown() {
|
|||||||
StopTestStore()
|
StopTestStore()
|
||||||
panic(err)
|
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 *a.Config().ServiceSettings.EnableCommands {
|
||||||
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
|
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
@@ -111,7 +118,7 @@ func (a *App) ListAllCommands(teamId string, T goi18n.TranslateFunc) ([]*model.C
|
|||||||
for _, value := range commandProviders {
|
for _, value := range commandProviders {
|
||||||
if cmd := value.GetCommand(a, T); cmd != nil {
|
if cmd := value.GetCommand(a, T); cmd != nil {
|
||||||
cpy := *cmd
|
cpy := *cmd
|
||||||
if cpy.AutoComplete && !seen[cpy.Id] {
|
if cpy.AutoComplete && !seen[cpy.Trigger] {
|
||||||
cpy.Sanitize()
|
cpy.Sanitize()
|
||||||
seen[cpy.Trigger] = true
|
seen[cpy.Trigger] = true
|
||||||
commands = append(commands, &cpy)
|
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 *a.Config().ServiceSettings.EnableCommands {
|
||||||
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
|
if result := <-a.Srv.Store.Command().GetByTeam(teamId); result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
} else {
|
} else {
|
||||||
teamCmds := result.Data.([]*model.Command)
|
teamCmds := result.Data.([]*model.Command)
|
||||||
for _, cmd := range teamCmds {
|
for _, cmd := range teamCmds {
|
||||||
if !seen[cmd.Id] {
|
if !seen[cmd.Trigger] {
|
||||||
cmd.Sanitize()
|
cmd.Sanitize()
|
||||||
seen[cmd.Trigger] = true
|
seen[cmd.Trigger] = true
|
||||||
commands = append(commands, cmd)
|
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 {
|
if !*a.Config().ServiceSettings.EnableCommands {
|
||||||
return nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
return nil, model.NewAppError("ExecuteCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
|
|||||||
143
app/plugin.go
143
app/plugin.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -101,20 +102,28 @@ func (a *App) ActivatePlugins() {
|
|||||||
|
|
||||||
l4g.Info("Activated %v plugin", id)
|
l4g.Info("Activated %v plugin", id)
|
||||||
} else if !pluginState.Enable && active {
|
} 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())
|
l4g.Error(err.Error())
|
||||||
continue
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin.Manifest.HasClient() {
|
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 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
|
||||||
message.Add("manifest", plugin.Manifest.ClientManifest())
|
message.Add("manifest", manifest.ClientManifest())
|
||||||
a.Publish(message)
|
a.Publish(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
l4g.Info("Deactivated %v plugin", id)
|
l4g.Info("Deactivated %v plugin", manifest.Id)
|
||||||
}
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallPlugin unpacks and installs a plugin but does not activate it.
|
// 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) {
|
if a.PluginEnv.IsPluginActive(id) {
|
||||||
err := a.PluginEnv.DeactivatePlugin(id)
|
err := a.deactivatePlugin(manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if manifest.HasClient() {
|
|
||||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
|
|
||||||
message.Add("manifest", manifest.ClientManifest())
|
|
||||||
a.Publish(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +344,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InitPlugins(pluginPath, webappPath string) {
|
func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) {
|
||||||
if !*a.Config().PluginSettings.Enable {
|
if !*a.Config().PluginSettings.Enable {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -362,7 +365,7 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if env, err := pluginenv.New(
|
options := []pluginenv.Option{
|
||||||
pluginenv.SearchPath(pluginPath),
|
pluginenv.SearchPath(pluginPath),
|
||||||
pluginenv.WebappPath(webappPath),
|
pluginenv.WebappPath(webappPath),
|
||||||
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
||||||
@@ -375,7 +378,13 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
|||||||
},
|
},
|
||||||
}, nil
|
}, 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())
|
l4g.Error("failed to start up plugins: " + err.Error())
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -533,3 +542,101 @@ func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
|||||||
|
|
||||||
return result.Err
|
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) {
|
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||||
return api.app.CreateTeam(team)
|
return api.app.CreateTeam(team)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
|
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPluginKeyValueStore(t *testing.T) {
|
func TestPluginKeyValueStore(t *testing.T) {
|
||||||
@@ -98,3 +100,98 @@ func TestHandlePluginRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
router.ServeHTTP(nil, r)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ func runServer(configFileLocation string) {
|
|||||||
a.LoadLicense()
|
a.LoadLicense()
|
||||||
}
|
}
|
||||||
|
|
||||||
a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
|
a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil)
|
||||||
utils.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
utils.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
||||||
if *cfg.PluginSettings.Enable {
|
if *cfg.PluginSettings.Enable {
|
||||||
a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
|
a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil)
|
||||||
} else {
|
} else {
|
||||||
a.ShutDownPlugins()
|
a.ShutDownPlugins()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4266,6 +4266,10 @@
|
|||||||
"id": "mattermost.working_dir",
|
"id": "mattermost.working_dir",
|
||||||
"translation": "Current working directory is %v"
|
"translation": "Current working directory is %v"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "model.plugin_command.error.app_error",
|
||||||
|
"translation": "An error occurred while trying to execute this command."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "model.plugin_key_value.is_valid.plugin_id.app_error",
|
"id": "model.plugin_key_value.is_valid.plugin_id.app_error",
|
||||||
"translation": "Invalid plugin ID, must be more than {{.Min}} and a of maximum {{.Max}} characters long."
|
"translation": "Invalid plugin ID, must be more than {{.Min}} and a of maximum {{.Max}} characters long."
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ type API interface {
|
|||||||
// struct that the configuration JSON can be unmarshalled to.
|
// struct that the configuration JSON can be unmarshalled to.
|
||||||
LoadPluginConfiguration(dest interface{}) error
|
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 creates a user.
|
||||||
CreateUser(user *model.User) (*model.User, *model.AppError)
|
CreateUser(user *model.User) (*model.User, *model.AppError)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package plugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"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
|
// 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
|
// The Mattermost-User-Id header will be present if (and only if) the request is by an
|
||||||
// authenticated user.
|
// authenticated user.
|
||||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,35 +223,59 @@ func (env *Environment) Shutdown() (errs []error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvironmentHooks struct {
|
type MultiPluginHooks struct {
|
||||||
env *Environment
|
env *Environment
|
||||||
}
|
}
|
||||||
|
|
||||||
func (env *Environment) Hooks() *EnvironmentHooks {
|
type SinglePluginHooks struct {
|
||||||
return &EnvironmentHooks{env}
|
env *Environment
|
||||||
|
pluginId string
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
|
func (env *Environment) Hooks() *MultiPluginHooks {
|
||||||
// encountered will be returned.
|
return &MultiPluginHooks{
|
||||||
func (h *EnvironmentHooks) OnConfigurationChange() (errs []error) {
|
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()
|
h.env.mutex.RLock()
|
||||||
defer h.env.mutex.RUnlock()
|
defer h.env.mutex.RUnlock()
|
||||||
|
|
||||||
for _, activePlugin := range h.env.activePlugins {
|
for _, activePlugin := range h.env.activePlugins {
|
||||||
if activePlugin.Supervisor == nil {
|
if activePlugin.Supervisor == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := activePlugin.Supervisor.Hooks().OnConfigurationChange(); err != nil {
|
if err := f(activePlugin.Supervisor.Hooks()); err != nil {
|
||||||
errs = append(errs, errors.Wrapf(err, "OnConfigurationChange error for %v", activePlugin.BundleInfo.Manifest.Id))
|
errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
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
|
// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
|
||||||
// 404 not found.
|
// 404 not found.
|
||||||
//
|
//
|
||||||
// It expects the request's context to have a plugin_id set.
|
// 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 id := r.Context().Value("plugin_id"); id != nil {
|
||||||
if idstr, ok := id.(string); ok {
|
if idstr, ok := id.(string); ok {
|
||||||
h.env.mutex.RLock()
|
h.env.mutex.RLock()
|
||||||
@@ -264,3 +288,25 @@ func (h *EnvironmentHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
http.NotFound(w, r)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -355,3 +355,51 @@ func TestEnvironment_ConcurrentHookInvocations(t *testing.T) {
|
|||||||
|
|
||||||
wg.Wait()
|
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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ func (m *API) LoadPluginConfiguration(dest interface{}) error {
|
|||||||
return ret.Error(0)
|
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) {
|
func (m *API) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||||
ret := m.Called(user)
|
ret := m.Called(user)
|
||||||
if f, ok := ret.Get(0).(func(*model.User) (*model.User, *model.AppError)); ok {
|
if f, ok := ret.Get(0).(func(*model.User) (*model.User, *model.AppError)); ok {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +19,11 @@ type Hooks struct {
|
|||||||
var _ plugin.Hooks = (*Hooks)(nil)
|
var _ plugin.Hooks = (*Hooks)(nil)
|
||||||
|
|
||||||
func (m *Hooks) OnActivate(api plugin.API) error {
|
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 {
|
func (m *Hooks) OnDeactivate() error {
|
||||||
@@ -32,3 +37,13 @@ func (m *Hooks) OnConfigurationChange() error {
|
|||||||
func (m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
m.Called(w, r)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ func (api *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error
|
|||||||
return nil
|
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 {
|
type APIErrorReply struct {
|
||||||
Error *model.AppError
|
Error *model.AppError
|
||||||
}
|
}
|
||||||
@@ -344,6 +352,22 @@ func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
|
|||||||
return json.Unmarshal(config, dest)
|
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) {
|
func (api *RemoteAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||||
var reply APIUserReply
|
var reply APIUserReply
|
||||||
if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil {
|
if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package rpcplugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -84,6 +85,16 @@ func TestAPI(t *testing.T) {
|
|||||||
assert.Equal(t, "foo", config.Foo)
|
assert.Equal(t, "foo", config.Foo)
|
||||||
assert.Equal(t, "baz", config.Bar.Baz)
|
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) {
|
api.On("CreateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) (*model.Channel, *model.AppError) {
|
||||||
c.Id = "thechannelid"
|
c.Id = "thechannelid"
|
||||||
return c, nil
|
return c, nil
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/rpc"
|
"net/rpc"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,6 +126,20 @@ func (h *LocalHooks) ServeHTTP(args ServeHTTPArgs, reply *struct{}) error {
|
|||||||
return nil
|
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) {
|
func ServeHooks(hooks interface{}, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||||
server := rpc.NewServer()
|
server := rpc.NewServer()
|
||||||
server.Register(&LocalHooks{
|
server.Register(&LocalHooks{
|
||||||
@@ -141,6 +156,7 @@ const (
|
|||||||
remoteOnDeactivate = 1
|
remoteOnDeactivate = 1
|
||||||
remoteServeHTTP = 2
|
remoteServeHTTP = 2
|
||||||
remoteOnConfigurationChange = 3
|
remoteOnConfigurationChange = 3
|
||||||
|
remoteExecuteCommand = 4
|
||||||
maxRemoteHookCount = iota
|
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 {
|
func (h *RemoteHooks) Close() error {
|
||||||
if h.apiCloser != nil {
|
if h.apiCloser != nil {
|
||||||
h.apiCloser.Close()
|
h.apiCloser.Close()
|
||||||
@@ -253,6 +280,8 @@ func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer) (*RemoteHooks, error) {
|
|||||||
remote.implemented[remoteOnConfigurationChange] = true
|
remote.implemented[remoteOnConfigurationChange] = true
|
||||||
case "ServeHTTP":
|
case "ServeHTTP":
|
||||||
remote.implemented[remoteServeHTTP] = true
|
remote.implemented[remoteServeHTTP] = true
|
||||||
|
case "ExecuteCommand":
|
||||||
|
remote.implemented[remoteExecuteCommand] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return remote, nil
|
return remote, nil
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||||
)
|
)
|
||||||
@@ -79,6 +80,17 @@ func TestHooks(t *testing.T) {
|
|||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "bar", string(body))
|
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)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user