Merge branch 'plugins-2'
Этот коммит содержится в:
10
app/app.go
10
app/app.go
@@ -25,7 +25,7 @@ import (
|
||||
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
@@ -42,10 +42,8 @@ type App struct {
|
||||
|
||||
Log *mlog.Logger
|
||||
|
||||
PluginEnv *pluginenv.Environment
|
||||
PluginConfigListenerId string
|
||||
IsPluginSandboxSupported bool
|
||||
pluginStatuses map[string]*model.PluginStatus
|
||||
Plugins *plugin.Environment
|
||||
PluginConfigListenerId string
|
||||
|
||||
EmailBatching *EmailBatchingJob
|
||||
EmailRateLimiter *throttled.GCRARateLimiter
|
||||
@@ -242,8 +240,6 @@ func New(options ...Option) (outApp *App, outErr error) {
|
||||
handlers: make(map[string]webSocketHandler),
|
||||
}
|
||||
|
||||
app.initBuiltInPlugins()
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
@@ -35,7 +33,6 @@ type TestHelper struct {
|
||||
|
||||
tempConfigPath string
|
||||
tempWorkspace string
|
||||
pluginHooks map[string]plugin.Hooks
|
||||
}
|
||||
|
||||
type persistentTestStore struct {
|
||||
@@ -93,7 +90,6 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
|
||||
th := &TestHelper{
|
||||
App: a,
|
||||
pluginHooks: make(map[string]plugin.Hooks),
|
||||
tempConfigPath: tempConfig.Name(),
|
||||
}
|
||||
|
||||
@@ -123,6 +119,19 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
th.App.SetLicense(nil)
|
||||
}
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
@@ -382,65 +391,6 @@ func (me *TestHelper) TearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
type mockPluginSupervisor struct {
|
||||
hooks plugin.Hooks
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Start(api plugin.API) error {
|
||||
return s.hooks.OnActivate(api)
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Wait() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
manifestCopy := *manifest
|
||||
if manifestCopy.Backend == nil {
|
||||
manifestCopy.Backend = &model.ManifestBackend{}
|
||||
}
|
||||
manifestBytes, err := json.Marshal(&manifestCopy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(me.tempWorkspace, "webapp")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (me *TestHelper) ResetRoleMigration() {
|
||||
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
@@ -183,6 +184,16 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan
|
||||
a.InvalidateCacheForUser(channel.CreatorId)
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedId)
|
||||
})
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
}
|
||||
@@ -200,6 +211,16 @@ func (a *App) CreateDirectChannel(userId string, otherUserId string) (*model.Cha
|
||||
a.InvalidateCacheForUser(userId)
|
||||
a.InvalidateCacheForUser(otherUserId)
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedId)
|
||||
})
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("teammate_id", otherUserId)
|
||||
a.Publish(message)
|
||||
@@ -798,6 +819,16 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
if userRequestorId == "" || userId == userRequestorId {
|
||||
a.postJoinChannelMessage(user, channel)
|
||||
} else {
|
||||
@@ -1128,10 +1159,21 @@ func (a *App) JoinChannel(channel *model.Channel, userId string) *model.AppError
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
if _, err := a.AddUserToChannel(user, channel); err != nil {
|
||||
cm, err := a.AddUserToChannel(user, channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
if err := a.postJoinChannelMessage(user, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1312,6 +1354,11 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
cm, err := a.GetChannelMember(channel.Id, userIdToRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmresult := <-a.Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil {
|
||||
return cmresult.Err
|
||||
}
|
||||
@@ -1322,6 +1369,22 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
a.InvalidateCacheForUser(userIdToRemove)
|
||||
a.InvalidateCacheForChannelMembers(channel.Id)
|
||||
|
||||
if a.PluginsReady() {
|
||||
|
||||
var actorUser *model.User
|
||||
if removerUserId != "" {
|
||||
actorUser, err = a.GetUser(removerUserId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
||||
return true
|
||||
}, plugin.UserHasLeftChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil)
|
||||
message.Add("user_id", userIdToRemove)
|
||||
message.Add("remover_id", removerUserId)
|
||||
@@ -1338,6 +1401,7 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
|
||||
func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
|
||||
var err *model.AppError
|
||||
|
||||
if err = a.removeUserFromChannel(userIdToRemove, removerUserId, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1350,6 +1414,11 @@ func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
if userIdToRemove == removerUserId {
|
||||
a.postLeaveChannelMessage(user, channel)
|
||||
} else {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
a.postRemoveFromChannelMessage(removerUserId, user, channel)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
@@ -98,13 +97,7 @@ func pluginSetting(pluginSettings *model.PluginSettings, plugin, key string, def
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if b, err := json.Marshal(settings); err != nil {
|
||||
return defaultValue
|
||||
} else {
|
||||
json.Unmarshal(b, &m)
|
||||
}
|
||||
if value, ok := m[key]; ok {
|
||||
if value, ok := settings[key]; ok {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
@@ -570,59 +563,60 @@ func (a *App) trackLicense() {
|
||||
}
|
||||
|
||||
func (a *App) trackPlugins() {
|
||||
if *a.Config().PluginSettings.Enable {
|
||||
totalActiveCount := -1 // -1 to indicate disabled or error
|
||||
webappActiveCount := 0
|
||||
backendActiveCount := 0
|
||||
totalInactiveCount := -1 // -1 to indicate disabled or error
|
||||
webappInactiveCount := 0
|
||||
backendInactiveCount := 0
|
||||
if a.PluginsReady() {
|
||||
totalEnabledCount := 0
|
||||
webappEnabledCount := 0
|
||||
backendEnabledCount := 0
|
||||
totalDisabledCount := 0
|
||||
webappDisabledCount := 0
|
||||
backendDisabledCount := 0
|
||||
brokenManifestCount := 0
|
||||
settingsCount := 0
|
||||
|
||||
plugins, _ := a.GetPlugins()
|
||||
pluginStates := a.Config().PluginSettings.PluginStates
|
||||
plugins, _ := a.Plugins.Available()
|
||||
|
||||
if plugins != nil {
|
||||
totalActiveCount = len(plugins.Active)
|
||||
|
||||
for _, plugin := range plugins.Active {
|
||||
if plugin.Webapp != nil {
|
||||
webappActiveCount += 1
|
||||
if pluginStates != nil && plugins != nil {
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
brokenManifestCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendActiveCount += 1
|
||||
if state, ok := pluginStates[plugin.Manifest.Id]; ok && state.Enable {
|
||||
totalEnabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendEnabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappEnabledCount += 1
|
||||
}
|
||||
} else {
|
||||
totalDisabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendDisabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappDisabledCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
totalInactiveCount = len(plugins.Inactive)
|
||||
|
||||
for _, plugin := range plugins.Inactive {
|
||||
if plugin.Webapp != nil {
|
||||
webappInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
if plugin.Manifest.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalEnabledCount = -1 // -1 to indicate disabled or error
|
||||
totalDisabledCount = -1 // -1 to indicate disabled or error
|
||||
}
|
||||
|
||||
a.SendDiagnostic(TRACK_PLUGINS, map[string]interface{}{
|
||||
"active_plugins": totalActiveCount,
|
||||
"active_webapp_plugins": webappActiveCount,
|
||||
"active_backend_plugins": backendActiveCount,
|
||||
"inactive_plugins": totalInactiveCount,
|
||||
"inactive_webapp_plugins": webappInactiveCount,
|
||||
"inactive_backend_plugins": backendInactiveCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"enabled_plugins": totalEnabledCount,
|
||||
"enabled_webapp_plugins": webappEnabledCount,
|
||||
"enabled_backend_plugins": backendEnabledCount,
|
||||
"disabled_plugins": totalDisabledCount,
|
||||
"disabled_webapp_plugins": webappDisabledCount,
|
||||
"disabled_backend_plugins": backendDisabledCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"plugins_with_broken_manifests": brokenManifestCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ func newTestServer() (chan string, *httptest.Server) {
|
||||
|
||||
func TestPluginSetting(t *testing.T) {
|
||||
settings := &model.PluginSettings{
|
||||
Plugins: map[string]interface{}{
|
||||
"test": map[string]string{
|
||||
Plugins: map[string]map[string]interface{}{
|
||||
"test": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
|
||||
895
app/plugin.go
895
app/plugin.go
@@ -4,356 +4,135 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
builtinplugin "github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/jira"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/ldapextras"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/zoom"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox"
|
||||
)
|
||||
|
||||
var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){
|
||||
"jira": jira.Asset,
|
||||
"zoom": zoom.Asset,
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *App) SyncPluginsActiveState() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
config := a.Config().PluginSettings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) setPluginStatusState(id string, state int) error {
|
||||
if _, ok := a.pluginStatuses[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.pluginStatuses[id].State = state
|
||||
|
||||
return a.notifyPluginStatusesChanged()
|
||||
}
|
||||
|
||||
func (a *App) initBuiltInPlugins() {
|
||||
plugins := map[string]builtinplugin.Plugin{
|
||||
"ldapextras": &ldapextras.Plugin{},
|
||||
}
|
||||
for id, p := range plugins {
|
||||
mlog.Debug("Initializing built-in plugin", mlog.String("plugin_id", id))
|
||||
api := &BuiltInPluginAPI{
|
||||
id: id,
|
||||
router: a.Srv.Router.PathPrefix("/plugins/" + id).Subrouter(),
|
||||
app: a,
|
||||
if *config.Enable {
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
a.Log.Error("Unable to get available plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
p.Initialize(api)
|
||||
}
|
||||
a.AddConfigListener(func(before, after *model.Config) {
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
|
||||
// Deactivate any plugins that have been disabled.
|
||||
for _, plugin := range a.Plugins.Active() {
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// If it's not enabled we need to deactivate it
|
||||
if !pluginEnabled {
|
||||
a.Plugins.Deactivate(pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
// Activate any plugins that have been enabled
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
plugin.WrapLogger(a.Log).Error("Plugin manifest could not be loaded", mlog.Err(plugin.ManifestError))
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// Activate plugin if enabled
|
||||
if pluginEnabled {
|
||||
if err := a.Plugins.Activate(pluginId); err != nil {
|
||||
plugin.WrapLogger(a.Log).Error("Unable to activate plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // If plugins are disabled, shutdown plugins.
|
||||
a.Plugins.Shutdown()
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) NewPluginAPI(manifest *model.Manifest) plugin.API {
|
||||
return NewPluginAPI(a, manifest)
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
if a.Plugins != nil || !*a.Config().PluginSettings.Enable {
|
||||
a.SyncPluginsActiveState()
|
||||
return
|
||||
}
|
||||
|
||||
a.Log.Info("Starting up plugins")
|
||||
|
||||
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if env, err := plugin.NewEnvironment(a.NewPluginAPI, pluginDir, webappPluginDir, a.Log); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.Plugins = env
|
||||
}
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) {
|
||||
a.SyncPluginsActiveState()
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnConfigurationChange()
|
||||
return true
|
||||
}, plugin.OnConfigurationChangeId)
|
||||
})
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
}
|
||||
|
||||
a.SyncPluginsActiveState()
|
||||
}
|
||||
|
||||
func (a *App) setPluginsActive(activate bool) {
|
||||
if a.PluginEnv == nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t): plugin env not initialized", activate))
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t)", activate), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
a.Plugins.Shutdown()
|
||||
|
||||
enabled := false
|
||||
if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok {
|
||||
enabled = state.Enable
|
||||
}
|
||||
|
||||
a.pluginStatuses[plugin.Manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
if activate && enabled {
|
||||
a.setPluginActive(plugin, activate)
|
||||
} else if !activate {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActiveById(id string, activate bool) {
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginActiveById(%t)", activate), mlog.String("plugin_id", id), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest != nil && plugin.Manifest.Id == id {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActive(plugin *model.BundleInfo, activate bool) {
|
||||
if plugin.Manifest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := plugin.Manifest.Id
|
||||
|
||||
active := a.PluginEnv.IsPluginActive(id)
|
||||
|
||||
if activate {
|
||||
if !active {
|
||||
if err := a.activatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to activate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
}
|
||||
|
||||
} else if !activate {
|
||||
if active {
|
||||
if err := a.deactivatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to deactivate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
} else {
|
||||
if err := a.setPluginStatusState(plugin.Manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
mlog.Error("Plugin status state failed to update", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Activating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
onError := func(err error) {
|
||||
mlog.Debug("Plugin failed to stay running", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStayRunning); err != nil {
|
||||
mlog.Error("Failed to record plugin status", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.ActivatePlugin(manifest.Id, onError); err != nil {
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStart); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateRunning); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
mlog.Info("Activated plugin", mlog.String("plugin_id", manifest.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Deactivating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "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)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
mlog.Info("Deactivated plugin", mlog.String("plugin_id", 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)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
_, isPrepackaged := prepackagedPlugins[manifest.Id]
|
||||
if isPrepackaged && !allowPrepackaged {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(a.PluginEnv.SearchPath(), manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.pluginStatuses[manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: manifest.Id,
|
||||
PluginPath: pluginPath,
|
||||
State: model.PluginStateNotRunning,
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
IsPrepackaged: isPrepackaged,
|
||||
Name: manifest.Name,
|
||||
Description: manifest.Description,
|
||||
Version: manifest.Version,
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetPlugins returned the plugins installed on this server, including the manifests needed to
|
||||
// enable plugins with web functionality.
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
_, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id]
|
||||
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.Plugins = nil
|
||||
}
|
||||
|
||||
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins := a.PluginEnv.ActivePlugins()
|
||||
plugins := a.Plugins.Active()
|
||||
|
||||
manifests := make([]*model.Manifest, len(plugins))
|
||||
for i, plugin := range plugins {
|
||||
@@ -363,99 +142,14 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses := make([]*model.PluginStatus, 0, len(a.pluginStatuses))
|
||||
for _, pluginStatus := range a.pluginStatuses {
|
||||
pluginStatuses = append(pluginStatuses, pluginStatus)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id, false)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged {
|
||||
return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.PluginEnv.IsPluginActive(id) {
|
||||
err := a.deactivatePlugin(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
delete(a.pluginStatuses, manifest.Id)
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
|
||||
// activation if inactive anywhere in the cluster.
|
||||
func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -472,14 +166,17 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ENABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
// This call will cause SyncPluginsActiveState to be called and the plugin to be activated
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
if err.Id == "ent.cluster.save_config.error" {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
|
||||
@@ -492,11 +189,11 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
|
||||
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
|
||||
func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -513,14 +210,16 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
|
||||
})
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -528,331 +227,35 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) {
|
||||
if a.PluginEnv != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Starting up plugins")
|
||||
|
||||
a.pluginStatuses = make(map[string]*model.PluginStatus)
|
||||
|
||||
if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
options := []pluginenv.Option{
|
||||
pluginenv.SearchPath(pluginPath),
|
||||
pluginenv.WebappPath(webappPath),
|
||||
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
||||
return &PluginAPI{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
keyValueStore: &PluginKeyValueStore{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
a.IsPluginSandboxSupported = sandbox.CheckSupport() == nil
|
||||
|
||||
if supervisorOverride != nil {
|
||||
options = append(options, pluginenv.SupervisorProvider(supervisorOverride))
|
||||
} else if a.IsPluginSandboxSupported {
|
||||
options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider))
|
||||
} else {
|
||||
options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider))
|
||||
}
|
||||
|
||||
if env, err := pluginenv.New(options...); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.PluginEnv = env
|
||||
}
|
||||
|
||||
for id, asset := range prepackagedPlugins {
|
||||
if tarball, err := asset("plugin.tar.gz"); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
} else if tarball != nil {
|
||||
a.removePlugin(id, true)
|
||||
if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok && id != "zoom" {
|
||||
if err := a.EnablePlugin(id); err != nil {
|
||||
mlog.Error("Failed to enable prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(oldCfg *model.Config, cfg *model.Config) {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *oldCfg.PluginSettings.Enable != *cfg.PluginSettings.Enable {
|
||||
a.setPluginsActive(*cfg.PluginSettings.Enable)
|
||||
} else {
|
||||
plugins := map[string]bool{}
|
||||
for id := range oldCfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
for id := range cfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
|
||||
for id := range plugins {
|
||||
oldPluginState := oldCfg.PluginSettings.PluginStates[id]
|
||||
pluginState := cfg.PluginSettings.PluginStates[id]
|
||||
|
||||
wasEnabled := oldPluginState != nil && oldPluginState.Enable
|
||||
isEnabled := pluginState != nil && pluginState.Enable
|
||||
|
||||
if wasEnabled != isEnabled {
|
||||
a.setPluginActiveById(id, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
a.setPluginsActive(true)
|
||||
}
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
mlog.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, a.PluginEnv.Hooks().ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(w, r.WithContext(context.WithValue(r.Context(), "plugin_id", params["plugin_id"])))
|
||||
}
|
||||
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, err := range a.PluginEnv.Shutdown() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.PluginEnv = nil
|
||||
}
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
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,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
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
|
||||
}
|
||||
|
||||
func (a *App) PluginsReady() bool {
|
||||
return a.PluginEnv != nil && *a.Config().PluginSettings.Enable
|
||||
return a.Plugins != nil && *a.Config().PluginSettings.Enable
|
||||
}
|
||||
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if !a.PluginsReady() {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
|
||||
if a.Plugins.IsActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
// Loads the plugin's configuration
|
||||
LoadPluginConfiguration(dest interface{}) error
|
||||
|
||||
// The plugin's router
|
||||
PluginRouter() *mux.Router
|
||||
|
||||
// Gets a team by its name
|
||||
GetTeamByName(name string) (*model.Team, *model.AppError)
|
||||
|
||||
// Gets a user by its name
|
||||
GetUserByName(name string) (*model.User, *model.AppError)
|
||||
|
||||
// Gets a channel by its name
|
||||
GetChannelByName(teamId, name string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Gets a direct message channel
|
||||
GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Creates a post
|
||||
CreatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// Get LDAP attributes for a user
|
||||
GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError)
|
||||
|
||||
// Temporary for built-in plugins, copied from api4/context.go ServeHTTP function.
|
||||
// If a request has a valid token for an active session, the session is returned otherwise
|
||||
// it errors.
|
||||
GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError)
|
||||
|
||||
// Returns a localized string. If a request is given, its headers will be used to pick a locale.
|
||||
I18n(id string, r *http.Request) string
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Base provides default implementations for hooks.
|
||||
type Base struct{}
|
||||
|
||||
func (b *Base) OnConfigurationChange() {}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// All implementations should be safe for concurrent use.
|
||||
type Hooks interface {
|
||||
// Invoked when configuration changes may have been made
|
||||
OnConfigurationChange()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package jira
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
type Configuration struct {
|
||||
Enabled bool
|
||||
Attributes []string
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
plugin.Base
|
||||
api plugin.API
|
||||
configuration atomic.Value
|
||||
}
|
||||
|
||||
func (p *Plugin) Initialize(api plugin.API) {
|
||||
p.api = api
|
||||
p.OnConfigurationChange()
|
||||
api.PluginRouter().HandleFunc("/users/{user_id:[A-Za-z0-9]+}/attributes", p.handleGetAttributes).Methods("GET")
|
||||
}
|
||||
|
||||
func (p *Plugin) config() *Configuration {
|
||||
return p.configuration.Load().(*Configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnConfigurationChange() {
|
||||
var configuration Configuration
|
||||
if err := p.api.LoadPluginConfiguration(&configuration); err != nil {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
p.configuration.Store(&configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleGetAttributes(w http.ResponseWriter, r *http.Request) {
|
||||
config := p.config()
|
||||
if !config.Enabled || len(config.Attributes) == 0 {
|
||||
http.Error(w, "This plugin is not configured", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := p.api.GetSessionFromRequest(r)
|
||||
|
||||
if session == nil || err != nil {
|
||||
http.Error(w, "Invalid session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Only requires a valid session, no other permission checks required
|
||||
|
||||
params := mux.Vars(r)
|
||||
id := params["user_id"]
|
||||
|
||||
if len(id) != 26 {
|
||||
http.Error(w, "Invalid user id", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
attributes, err := p.api.GetLdapUserAttributes(id, config.Attributes)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Errored getting attributes: %v", err.Error()), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJson(attributes)))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type Plugin interface {
|
||||
Initialize(API)
|
||||
Hooks
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package zoom
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -5,32 +5,48 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
app *App
|
||||
keyValueStore *PluginKeyValueStore
|
||||
id string
|
||||
app *App
|
||||
logger *mlog.SugarLogger
|
||||
manifest *model.Manifest
|
||||
}
|
||||
|
||||
type PluginKeyValueStore struct {
|
||||
id string
|
||||
app *App
|
||||
func NewPluginAPI(a *App, manifest *model.Manifest) *PluginAPI {
|
||||
return &PluginAPI{
|
||||
id: manifest.Id,
|
||||
manifest: manifest,
|
||||
app: a,
|
||||
logger: a.Log.With(mlog.String("plugin_id", manifest.Id)).Sugar(),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
finalConfig := make(map[string]interface{})
|
||||
|
||||
// First set final config to defaults
|
||||
if api.manifest.SettingsSchema != nil {
|
||||
for _, setting := range api.manifest.SettingsSchema.Settings {
|
||||
finalConfig[strings.ToLower(setting.Key)] = setting.Default
|
||||
}
|
||||
}
|
||||
|
||||
// If we have settings given we override the defaults with them
|
||||
for setting, value := range api.app.Config().PluginSettings.Plugins[api.id] {
|
||||
finalConfig[strings.ToLower(setting)] = value
|
||||
}
|
||||
|
||||
if pluginSettingsJsonBytes, err := json.Marshal(finalConfig); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
return json.Unmarshal(pluginSettingsJsonBytes, dest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +59,14 @@ func (api *PluginAPI) UnregisterCommand(teamId, trigger string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetConfig() *model.Config {
|
||||
return api.app.GetConfig()
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
|
||||
return api.app.SaveConfig(config, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.CreateTeam(team)
|
||||
}
|
||||
@@ -51,6 +75,10 @@ func (api *PluginAPI) DeleteTeam(teamId string) *model.AppError {
|
||||
return api.app.SoftDeleteTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeams() ([]*model.Team, *model.AppError) {
|
||||
return api.app.GetAllTeams()
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeam(teamId)
|
||||
}
|
||||
@@ -63,6 +91,30 @@ func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError
|
||||
return api.app.UpdateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.AddTeamMember(teamId, userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.AddTeamMembers(teamId, userIds, requestorId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *model.AppError {
|
||||
return api.app.RemoveUserFromTeam(teamId, userId, requestorId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMembers(teamId, offset, limit)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMember(teamId, userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateTeamMemberRoles(teamId, userId, newRoles string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.UpdateTeamMemberRoles(teamId, userId, newRoles)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.CreateUser(user)
|
||||
}
|
||||
@@ -104,6 +156,10 @@ func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
return api.app.DeleteChannel(channel, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError) {
|
||||
return api.app.GetPublicChannelsForTeam(teamId, offset, limit)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannel(channelId)
|
||||
}
|
||||
@@ -157,6 +213,10 @@ func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SendEphemeralPost(userId string, post *model.Post) *model.Post {
|
||||
return api.app.SendEphemeralPost(userId, post)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeletePost(postId string) *model.AppError {
|
||||
_, err := api.app.DeletePost(postId, api.id)
|
||||
return err
|
||||
@@ -170,129 +230,35 @@ func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return api.app.UpdatePost(post, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
|
||||
return api.app.SetPluginKey(api.id, key, value)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
return s.app.SetPluginKey(s.id, key, value)
|
||||
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
|
||||
return api.app.GetPluginKey(api.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
return s.app.GetPluginKey(s.id, key)
|
||||
func (api *PluginAPI) KVDelete(key string) *model.AppError {
|
||||
return api.app.DeletePluginKey(api.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Delete(key string) *model.AppError {
|
||||
return s.app.DeletePluginKey(s.id, key)
|
||||
func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
||||
api.app.Publish(&model.WebSocketEvent{
|
||||
Event: fmt.Sprintf("custom_%v_%v", api.id, event),
|
||||
Data: payload,
|
||||
Broadcast: broadcast,
|
||||
})
|
||||
}
|
||||
|
||||
type BuiltInPluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
app *App
|
||||
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Debug(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Info(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
func (api *PluginAPI) LogError(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Error(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
|
||||
if api.app.Ldap == nil {
|
||||
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AuthData == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
|
||||
// Attempt to parse token out of the header
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
token = authHeader[7:]
|
||||
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
token = authHeader[6:]
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
|
||||
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if len(token) == 0 {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
isTokenFromQueryString = true
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session, err := api.app.GetSession(token)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Warn(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
113
app/plugin_commands.go
Обычный файл
113
app/plugin_commands.go
Обычный файл
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
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,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
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 {
|
||||
pluginHooks, err := a.Plugins.HooksForPlugin(pc.PluginId)
|
||||
if err != nil {
|
||||
return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
response, appErr := pluginHooks.ExecuteCommand(plugin.NewBlankContext(), args)
|
||||
return pc.Command, response, appErr
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
125
app/plugin_install.go
Обычный файл
125
app/plugin_install.go
Обычный файл
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check that there is no plugin with the same ID
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string) *model.AppError {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.Plugins.IsActive(id) && manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
a.Plugins.Deactivate(id)
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
app/plugin_key_value_store.go
Обычный файл
61
app/plugin_key_value_store.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
76
app/plugin_requests.go
Обычный файл
76
app/plugin_requests.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
a.Log.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
hooks, err := a.Plugins.HooksForPlugin(params["plugin_id"])
|
||||
if err != nil {
|
||||
a.Log.Error("Access to route for non-existant plugin", mlog.String("missing_plugin_id", params["plugin_id"]), mlog.Err(err))
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, hooks.ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(plugin.NewBlankContext(), w, r)
|
||||
}
|
||||
63
app/plugin_statuses.go
Обычный файл
63
app/plugin_statuses.go
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses, err := a.Plugins.Statuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "Unable to get plugin statuses", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Add our cluster ID
|
||||
for _, status := range pluginStatuses {
|
||||
status.ClusterId = a.GetClusterId()
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,19 +4,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPluginKeyValueStore(t *testing.T) {
|
||||
@@ -73,7 +69,7 @@ func TestHandlePluginRequest(t *testing.T) {
|
||||
var assertions func(*http.Request)
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", func(_ http.ResponseWriter, r *http.Request) {
|
||||
th.App.servePluginRequest(nil, r, func(_ http.ResponseWriter, r *http.Request) {
|
||||
th.App.servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) {
|
||||
assertions(r)
|
||||
})
|
||||
})
|
||||
@@ -103,152 +99,6 @@ 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"))
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateRunning, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type pluginBadActivation struct {
|
||||
testPlugin
|
||||
}
|
||||
|
||||
func (p *pluginBadActivation) OnActivate(api plugin.API) error {
|
||||
return errors.New("won't activate for some reason")
|
||||
}
|
||||
|
||||
func TestPluginBadActivation(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginBadActivation{})
|
||||
|
||||
t.Run("EnablePlugin bad activation", func(t *testing.T) {
|
||||
err := th.App.EnablePlugin("foo")
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just
|
||||
// sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateFailedToStart, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPluginStatusesDisabled(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
35
app/post.go
35
app/post.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/dyatlov/go-opengraph/opengraph"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"golang.org/x/net/html/charset"
|
||||
@@ -161,10 +162,14 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if newPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBePosted(post); newPost == nil {
|
||||
var rejectionReason string
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
post, rejectionReason = hooks.MessageWillBePosted(pluginContext, post)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBePostedId)
|
||||
if post == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
post = newPost
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +182,11 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenPosted(rpost)
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenPosted(pluginContext, rpost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenPostedId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -386,10 +395,14 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if pluginModifiedPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBeUpdated(newPost, oldPost); pluginModifiedPost == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
newPost = pluginModifiedPost
|
||||
var rejectionReason string
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost, oldPost)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBeUpdatedId)
|
||||
if newPost == nil {
|
||||
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +413,11 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenUpdated(newPost, oldPost)
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenUpdated(pluginContext, newPost, oldPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenUpdatedId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ func TestUpdatePostEditAt(t *testing.T) {
|
||||
} else if saved.EditAt == post.EditAt {
|
||||
t.Fatal("should have updated post.EditAt when updating post message")
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
|
||||
func TestUpdatePostTimeLimit(t *testing.T) {
|
||||
|
||||
45
app/team.go
45
app/team.go
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
@@ -429,12 +430,28 @@ func (a *App) joinUserToTeam(team *model.Team, user *model.User) (*model.TeamMem
|
||||
}
|
||||
|
||||
func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId string) *model.AppError {
|
||||
if _, alreadyAdded, err := a.joinUserToTeam(team, user); err != nil {
|
||||
tm, alreadyAdded, err := a.joinUserToTeam(team, user)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if alreadyAdded {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
var actor *model.User
|
||||
if userRequestorId != "" {
|
||||
actor, err = a.GetUser(userRequestorId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedTeam(pluginContext, tm, actor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedTeamId)
|
||||
})
|
||||
}
|
||||
|
||||
if uua := <-a.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
|
||||
return uua.Err
|
||||
}
|
||||
@@ -575,9 +592,8 @@ func (a *App) AddTeamMember(teamId, userId string) (*model.TeamMember, *model.Ap
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var teamMember *model.TeamMember
|
||||
var err *model.AppError
|
||||
if teamMember, err = a.GetTeamMember(teamId, userId); err != nil {
|
||||
teamMember, err := a.GetTeamMember(teamId, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -692,10 +708,8 @@ func (a *App) RemoveUserFromTeam(teamId string, userId string, requestorId strin
|
||||
}
|
||||
|
||||
func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) *model.AppError {
|
||||
var teamMember *model.TeamMember
|
||||
var err *model.AppError
|
||||
|
||||
if teamMember, err = a.GetTeamMember(team.Id, user.Id); err != nil {
|
||||
teamMember, err := a.GetTeamMember(team.Id, user.Id)
|
||||
if err != nil {
|
||||
return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -753,6 +767,21 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string)
|
||||
return result.Err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
var actor *model.User
|
||||
if requestorId != "" {
|
||||
actor, err = a.GetUser(requestorId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasLeftTeamId)
|
||||
})
|
||||
}
|
||||
|
||||
if uua := <-a.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
|
||||
return uua.Err
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user