Merge remote-tracking branch 'origin/master' into advanced-permissions-phase-2

Этот коммит содержится в:
Martin Kraft
2018-05-24 08:40:52 -04:00
родитель e46b94fa66 47f3c064db
Коммит 69304fb54d
39 изменённых файлов: 1071 добавлений и 266 удалений

Просмотреть файл

@@ -23,6 +23,7 @@ func (api *API) InitPlugin() {
api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(getPlugins)).Methods("GET")
api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugins.Handle("/statuses", api.ApiSessionRequired(getPluginStatuses)).Methods("GET")
api.BaseRoutes.Plugin.Handle("/activate", api.ApiSessionRequired(activatePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/deactivate", api.ApiSessionRequired(deactivatePlugin)).Methods("POST")
@@ -97,6 +98,26 @@ func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(response.ToJson()))
}
func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
response, err := c.App.GetClusterPluginStatuses()
if err != nil {
c.Err = err
return
}
w.Write([]byte(response.ToJson()))
}
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
@@ -104,7 +125,7 @@ func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}

Просмотреть файл

@@ -39,8 +39,10 @@ type App struct {
Log *mlog.Logger
PluginEnv *pluginenv.Environment
PluginConfigListenerId string
PluginEnv *pluginenv.Environment
PluginConfigListenerId string
IsPluginSandboxSupported bool
pluginStatuses map[string]*model.PluginStatus
EmailBatching *EmailBatchingJob

Просмотреть файл

@@ -370,6 +370,10 @@ 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
}
@@ -387,17 +391,6 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks
me.tempWorkspace = dir
}
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
webappDir := filepath.Join(me.tempWorkspace, "webapp")
me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
return &mockPluginSupervisor{hooks}, nil
}
return pluginenv.DefaultSupervisorProvider(bundle)
})
me.pluginHooks[manifest.Id] = hooks
manifestCopy := *manifest
if manifestCopy.Backend == nil {
manifestCopy.Backend = &model.ManifestBackend{}
@@ -407,6 +400,9 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks
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)
}
@@ -414,6 +410,15 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks
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() {
@@ -449,6 +454,9 @@ func (me *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model
func (me *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) {
return []string{}, nil
}
func (me *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
return nil, nil
}
func (me *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
return nil
}

Просмотреть файл

@@ -36,10 +36,7 @@ func (tl TokenLocation) String() string {
}
func (a *App) IsPasswordValid(password string) *model.AppError {
if license := a.License(); license != nil && *license.Features.PasswordRequirements {
return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings)
}
return utils.IsPasswordValid(password)
return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings)
}
func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {

Просмотреть файл

@@ -85,3 +85,11 @@ func (a *App) IsLeader() bool {
return true
}
}
func (a *App) GetClusterId() string {
if a.Cluster == nil {
return ""
}
return a.Cluster.GetClusterId()
}

Просмотреть файл

@@ -37,6 +37,31 @@ var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func
"zoom": zoom.Asset,
}
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
}
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{},
@@ -77,30 +102,100 @@ func (a *App) setPluginsActive(activate bool) {
continue
}
id := plugin.Manifest.Id
pluginState := &model.PluginState{Enable: false}
if state, ok := a.Config().PluginSettings.PluginStates[id]; ok {
pluginState = state
enabled := false
if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok {
enabled = state.Enable
}
active := a.PluginEnv.IsPluginActive(id)
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 && pluginState.Enable && !active {
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 || !pluginState.Enable) && active {
} 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 {
if err := a.PluginEnv.ActivatePlugin(manifest.Id); err != nil {
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)
}
@@ -115,6 +210,12 @@ func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError {
}
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)
}
@@ -127,6 +228,10 @@ func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError {
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
}
@@ -166,7 +271,8 @@ func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
}
if _, ok := prepackagedPlugins[manifest.Id]; ok && !allowPrepackaged {
_, isPrepackaged := prepackagedPlugins[manifest.Id]
if isPrepackaged && !allowPrepackaged {
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
}
@@ -185,16 +291,33 @@ func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model
}
}
err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id))
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)
}
// Should add manifest validation and error handling here
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)
@@ -240,6 +363,39 @@ 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)
}
@@ -284,10 +440,16 @@ func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
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 activation if inactive.
// 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 {
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
@@ -310,8 +472,8 @@ func (a *App) EnablePlugin(id string) *model.AppError {
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
}
if err := a.activatePlugin(manifest); err != nil {
return err
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) {
@@ -351,6 +513,10 @@ 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}
})
@@ -363,16 +529,18 @@ func (a *App) DisablePlugin(id string) *model.AppError {
}
func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) {
if !*a.Config().PluginSettings.Enable {
return
}
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
@@ -398,13 +566,19 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug
}),
}
if err := sandbox.CheckSupport(); err != nil {
a.IsPluginSandboxSupported = false
mlog.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server. See documentation to learn more: https://developers.mattermost.com/extend/plugins/security/", mlog.Err(err))
} else {
a.IsPluginSandboxSupported = true
}
if supervisorOverride != nil {
options = append(options, pluginenv.SupervisorProvider(supervisorOverride))
} else if err := sandbox.CheckSupport(); err != nil {
mlog.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server. See documentation to learn more: https://developers.mattermost.com/extend/plugins/security/", mlog.Err(err))
options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider))
} else {
} 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 {
@@ -431,12 +605,34 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug
}
a.RemoveConfigListener(a.PluginConfigListenerId)
a.PluginConfigListenerId = a.AddConfigListener(func(_, cfg *model.Config) {
a.PluginConfigListenerId = a.AddConfigListener(func(oldCfg *model.Config, cfg *model.Config) {
if a.PluginEnv == nil {
return
}
a.setPluginsActive(*cfg.PluginSettings.Enable)
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())

Просмотреть файл

@@ -7,8 +7,8 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
@@ -158,6 +158,20 @@ func TestPluginCommands(t *testing.T) {
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,
@@ -216,7 +230,46 @@ func TestPluginBadActivation(t *testing.T) {
t.Run("EnablePlugin bad activation", func(t *testing.T) {
err := th.App.EnablePlugin("foo")
assert.NotNil(t, err)
assert.True(t, strings.Contains(err.DetailedError, "won't activate for some reason"))
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()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = false
})
_, err := th.App.GetPluginStatuses()
require.EqualError(t, err, "GetPluginStatuses: Plugins have been disabled. Please check your logs for details., ")
}
func TestGetPluginStatuses(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = true
})
pluginStatuses, err := th.App.GetPluginStatuses()
require.Nil(t, err)
require.NotNil(t, pluginStatuses)
}

Просмотреть файл

@@ -323,7 +323,10 @@
"UseExperimentalGossip": false,
"ReadOnlyConfig": true,
"GossipPort": 8074,
"StreamingPort": 8075
"StreamingPort": 8075,
"MaxIdleConns": 100,
"MaxIdleConnsPerHost": 128,
"IdleConnTimeoutMilliseconds": 90000
},
"MetricsSettings": {
"Enable": false,

Просмотреть файл

@@ -21,5 +21,6 @@ type ClusterInterface interface {
NotifyMsg(buf []byte)
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
GetLogs(page, perPage int) ([]string, *model.AppError)
GetPluginStatuses() (model.PluginStatuses, *model.AppError)
ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError
}

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "Sie haben nicht die nötigen Berechtigungen um {{.User}} dem Kanal {{.Channel}} hinzuzufügen."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} wurde dem Kanal {{.Channel}} hinzugefügt."
@@ -3778,6 +3782,14 @@
"id": "app.notification.body.intro.direct.generic",
"translation": "Sie haben eine neue Direktnachricht von {{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Sie haben eine neue Direktnachricht."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "Sie haben eine neue Direktnachricht von {{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "Sie haben eine neue Benachrichtigung."
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}. {{.Month}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "KANAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}}.{{.Month}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}. {{.Month}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "KANAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}}.{{.Month}}"
@@ -3806,6 +3826,14 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Neue Direktnachricht von {{.SenderDisplayName}} am {{.Day}}.{{.Month}}.{{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] Benachrichtigung in {{ .TeamName}} am {{.Day}}.{{.Month}}.{{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Neue Benachrichtigung für {{.Day}}. {{.Month}} {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Benachrichtigung in {{ .TeamName}} am {{.Day}}.{{.Month}}.{{.Year}}"
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP-Feld \"Nachnameattribut\" ist erforderlich."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP-Feld \"ID Attribut\" ist erforderlich."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Ungültiger Wert für die MaxPageSize."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Der Kanal konnte nicht gelöscht werden"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problem beim Aktualisieren des zuletzt Aktualisiert Zeitpunkt für Mitglied"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Der bestehende Kanal konnte nicht gefunden werden"

Просмотреть файл

@@ -3854,14 +3854,6 @@
"id": "app.notification.body.intro.direct.generic",
"translation": "You have a new Direct Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "You have a new notification."
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "You have a new Group Message."
@@ -3870,6 +3862,14 @@
"id": "app.notification.body.intro.group_message.generic",
"translation": "You have a new Group Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "You have a new notification."
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3878,14 +3878,6 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "Channel: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "Channel: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3895,12 +3887,16 @@
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] New Direct Message from @{{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}"
"id": "app.notification.body.text.notification.full",
"translation": "Channel: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
"id": "app.notification.body.text.notification.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] New Direct Message from @{{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
@@ -3910,6 +3906,10 @@
"id": "app.notification.subject.group_message.generic",
"translation": "[{{ .SiteName }}] New Group Message on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.plugin.activate.app_error",
"translation": "Unable to activate extracted plugin."
@@ -3926,6 +3926,10 @@
"id": "app.plugin.deactivate.app_error",
"translation": "Unable to deactivate plugin"
},
{
"id": "app.plugin.delete_plugin_status_state.app_error",
"translation": "Unable to delete plugin status state."
},
{
"id": "app.plugin.disabled.app_error",
"translation": "Plugins have been disabled. Please check your logs for details."
@@ -3970,10 +3974,18 @@
"id": "app.plugin.not_installed.app_error",
"translation": "Plugin is not installed"
},
{
"id": "app.plugin.prepackaged.app_error",
"translation": "Cannot install prepackaged plugin"
},
{
"id": "app.plugin.remove.app_error",
"translation": "Unable to delete plugin"
},
{
"id": "app.plugin.set_plugin_status_state.app_error",
"translation": "Unable to set plugin status state."
},
{
"id": "app.plugin.upload_disabled.app_error",
"translation": "Plugins and/or plugin uploads have been disabled."
@@ -4870,6 +4882,10 @@
"id": "model.client.writer.app_error",
"translation": "Unable to build multipart request"
},
{
"id": "model.cluster.is_valid.id.app_error",
"translation": "Invalid Id"
},
{
"id": "model.command.is_valid.create_at.app_error",
"translation": "Create at must be a valid time"
@@ -5118,14 +5134,14 @@
"id": "model.config.is_valid.ldap_id",
"translation": "AD/LDAP field \"ID Attribute\" is required."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP field \"Login ID Attribute\" is required."
},
{
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP field \"Last Name Attribute\" is required."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP field \"Login ID Attribute\" is required."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Invalid max page size value."

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "No tienes suficientes permisos para agregar a {{.User}} en {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "No se pudo encontrar el canal {{.Channel}}. Por favor utiliza el identificador del canal."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} agregado al canal {{.Channel}}."
@@ -3772,11 +3776,19 @@
},
{
"id": "app.notification.body.intro.direct.full",
"translation": "Tienes un nuevo mensaje directo."
"translation": "Tienes un nuevo Mensaje Directo."
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "Tienes un nuevo mensaje directo de {{.SenderName}}"
"translation": "Tienes un nuevo Mensaje Directo de @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Tienes un nuevo Mensaje de Grupo."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "Tienes un nuevo mensaje de Grupo de @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,19 +3796,27 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "Tienes una nueva notificación de {{.SenderName}}"
"translation": "Tienes una nueva notificación de @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
"translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "Canal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "Canal: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
"translation": "Canal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.notification.generic",
@@ -3804,7 +3824,15 @@
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Nuevo Mensaje Directo de {{.SenderDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}"
"translation": "[{{.SiteName}}] Nuevo Mensaje Directo de @{{.SenderDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] Nuevo Mensaje de Grupo en {{ .TeamName}} el {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Nuevo Mensaje de Grupo el {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "El campo AD/LDAP \"Atributo Apellido\" es obligatorio."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "El campo AD/LDAP \"Atributo Login ID\" es obligatorio."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "El valor del tamaño de página no es válido."
@@ -5196,7 +5228,7 @@
},
{
"id": "model.config.is_valid.site_url.app_error",
"translation": "URL del Sitio debe ser una URL válida y empezar con http:// o https://."
"translation": "URL del Sitio debe estar asignado, ser una URL válida y empezar con http:// o https://."
},
{
"id": "model.config.is_valid.site_url_email_batching.app_error",
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "No pudimos eliminar el canal"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problema actualizando el último momento de actualización de los miembros"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "No pudimos encontrar el canal"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "Vous n'avez pas les permissions nécessaires pour ajouter {{.User}} dans {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} a été ajouté dans {{.Channel}}."
@@ -3778,6 +3782,14 @@
"id": "app.notification.body.intro.direct.generic",
"translation": "Vous avez un nouveau message personnel de {{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Vous avez un nouveau message personnel."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "Vous avez un nouveau message personnel de {{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "Vous avez une nouvelle notification."
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.Timezone}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "CANAL : {{.ChannelName}}<br>{{.SenderName}} - {{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.Timezone}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "CANAL : {{.ChannelName}}<br>{{.SenderName}} - {{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}"
@@ -3806,6 +3826,14 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Nouveau message personnel de {{.SenderDisplayName}} du {{.Day}}/{{.Month}}/{{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] Notification dans {{.TeamName}} le {{.Day}}/{{.Month}}/{{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] New Notification for {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{.SiteName}}] Notification dans {{.TeamName}} le {{.Day}}/{{.Month}}/{{.Year}}"
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "Le champ AD/LDAP \"Last Name Attribute\" est obligatoire."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "Le champ AD/LDAP \"ID Attribute\" est obligatoire."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Valeur de la taille maximale de page invalide."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Impossible de supprimer le canal"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problème de mise à jour de la date de dernière mise à jour des membres"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Impossible de trouver le canal existant"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "Non si hanno permessi sufficienti per aggiungere {{.User}} in {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Impossibile trovare il canale {{.Channel}}. Utilizzare il gestore canale per identificarli."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} aggiunto al canale {{.Channel}}."
@@ -3776,7 +3780,15 @@
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "Hai un nuovo messaggio diretto da {{.SenderName}}"
"translation": "Hai un nuovo messaggio diretto da @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Hai un nuovo messaggio di gruppo."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "Hai un nuovo messaggio di gruppo da @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,19 +3796,27 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "Hai una nuova notifica da {{.SenderName}}"
"translation": "Hai una nuova notifica da @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
"translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}/{{.Month}}"
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}/{{.Month}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "Canale: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "CANALE: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
"translation": "Canale: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.generic",
@@ -3804,7 +3824,15 @@
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}"
"translation": "[{{.SiteName}}] Nuovo messaggio diretto da @{{.SenderDisplayName}} il {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] Nuovo messaggio di gruppo in {{ .ChannelName}} del {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Nuovo messaggio di gruppo on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "Il campo AD/LDAP \"Last Name Attribute\" è richiesto."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "Il campo AD/LDAP \"Login ID Attribute\" è richiesto."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Valore non valido: dimensione massima pagina."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Non è possibile cancellare il canale"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problema nell'aggiornamento dell'ultimo aggiornamento utenti"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Non è possibile trovare il canale"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "{{.User}} を {{.Channel}} に追加する権限がありません。"
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} がチャンネル {{.Channel}} に追加されました。"
@@ -3778,6 +3782,14 @@
"id": "app.notification.body.intro.direct.generic",
"translation": "{{.SenderName}} からの新しいダイレクトメッセージがあります"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "新しいダイレクトメッセージがあります。"
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "{{.SenderName}} からの新しいダイレクトメッセージがあります"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "新しい通知があります。"
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "チャンネル: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "チャンネル: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3806,6 +3826,14 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{ .SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.SenderDisplayName}} からの新しいダイレクトメッセージ"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.TeamName}}の通知"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} の新着通知[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} の新着通知"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.TeamName}}の通知"
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP項目 \"苗字(ラストネーム)の属性値\" は必須です。"
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP項目 \"ID属性値\" は必須です。"
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "最大ページサイズの値が不正です。"
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "チャンネルを削除できませんでした"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "メンバーの最終更新時刻の更新に問題があります"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "チャンネルが見付かりませんでした"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "{{.Channel}} 에 {{.User}}를 추가할 권한이 없습니다."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.Channel}} 채널에 {{.User}} 가 추가되었습니다."
@@ -3776,7 +3780,15 @@
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "You have a new direct message from {{.SenderName}}"
"translation": "You have a new Direct Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "한개의 신규 메시지가 있습니다."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "You have a new Group Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,7 +3796,7 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from {{.SenderName}}"
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "채널: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "채널: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3806,9 +3826,17 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다."
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다."
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] 새 알림 {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
"translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다."
},
{
"id": "app.plugin.activate.app_error",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP의 \"Last Name Attribute\" 항목이 필요합니다."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP의 \"ID Attribute\" 항목이 필요합니다."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Invalid max page size value."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "채널을 삭제하지 못했습니다."
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problem updating members last updated time"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "존재하는 채널을 찾지 못했습니다."

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "You don't have enough permissions to add {{.User}} in {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} added to {{.Channel}} channel."
@@ -3776,7 +3780,15 @@
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "You have a new direct message from {{.SenderName}}"
"translation": "You have a new Direct Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "U heeft een nieuw bericht."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "You have a new Group Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,7 +3796,7 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from {{.SenderName}}"
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "KANAAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "KANAAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3806,9 +3826,17 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Nieuwe Notificatie voor {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
"translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}"
},
{
"id": "app.plugin.activate.app_error",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP veld \"Last Name Attribute\" is verplicht."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP veld \"ID Attribute\" is verplicht."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Ongeldige max page size waarde."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Het kanaal kan niet verwijderd worden"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Probleem bij het bijwerken van de leden laatst bijgewerkte tijd"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Het kanaal kon niet gevonden worden"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "You don't have enough permissions to add {{.User}} in {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} added to {{.Channel}} channel."
@@ -3776,7 +3780,15 @@
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "You have a new direct message from {{.SenderName}}"
"translation": "You have a new Direct Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Masz nową wiadomość."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "You have a new Group Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,7 +3796,7 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from {{.SenderName}}"
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "KANAŁ: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "KANAŁ: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3806,9 +3826,17 @@
"id": "app.notification.subject.direct.full",
"translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "Nowe powiadomienie [{{.SiteName}}] z {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
"translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.plugin.activate.app_error",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "Pole AD/LDAP \"Last Name Attribute\" jest wymagane."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "Pole AD/LDAP \"ID Attribute\" jest wymagane."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Nieprawidłowy maksymalny rozmiar strony."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Nie możemy usunąć kanału"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problem podczas aktualizowania daty ostatniej aktualizacji uczestników"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Nie mogliśmy znaleźć istniejącego kanału"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "Você não tem permissão suficiente para adicionar {{.User}} em {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Não foi possível encontrar o canal {{.Channel}}. Por favor utilize o identificador de canais para descobrir canais."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} adicionado ao canal {{.Channel}}."
@@ -3772,11 +3776,19 @@
},
{
"id": "app.notification.body.intro.direct.full",
"translation": "Você tem uma nova mensagem direta."
"translation": "Você tem uma nova Mensagem Direta."
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "Você tem uma nova mensagem direta de {{.SenderName}}"
"translation": "Você tem uma nova Mensagem Direta de @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Você tem uma nova Mensagem de Grupo."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "Você tem uma nova Mensagem de Grupo de @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,19 +3796,27 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "Você tem uma nova notificação de {{.SenderName}}"
"translation": "Você tem uma nova notificação de @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
"translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "Canal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "CANAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
"translation": "Canal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}"
},
{
"id": "app.notification.body.text.notification.generic",
@@ -3804,7 +3824,15 @@
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Nova Mensagem Direta de {{.SenderDisplayName}} em {{.Day}} {{.Month}}, {{.Year}}"
"translation": "[{{.SiteName}}] Nova Mensagem Direta de @{{.SenderDisplayName}} em {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] Nova Mensagem do Grupo {{ .ChannelName}} em {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Nova Mensagem de Grupo em {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "O campo \"Last Name Attribute\" do AD/LDAP é requerido."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "O campo \"Login ID Attribute\" do AD/LDAP é obrigatório."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Valor do tamanho de página máximo inválido."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Não foi possível deletar o canal"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Problema ao atualizar membros na última atualização"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Não foi possível encontrar o canal existente"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "У вас недостаточно прав для добавления {{.User}} в {{.Channel}}."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} добавлен в канал {{.Channel}}."
@@ -3776,7 +3780,15 @@
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "You have a new direct message from {{.SenderName}}"
"translation": "You have a new Direct Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "У вас есть новое личное сообщение."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "You have a new Group Message from @{{.SenderName}}"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,7 +3796,7 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "You have a new notification from {{.SenderName}}"
"translation": "You have a new notification from @{{.SenderName}}"
},
{
"id": "app.notification.body.text.direct.full",
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "КАНАЛ: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "КАНАЛ: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
@@ -3806,9 +3826,17 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] Новое уведомление за {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
"translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.plugin.activate.app_error",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "Требуется поле AD/LDAP \"Last Name Attribute\"."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "Требуется поле AD/LDAP \"ID Attribute\"."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "Неверное значение максимального размера страницы."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Неудачная попытка удалить канал"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Проблема с обновлением времени последнего входа участника"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Не удалось найти существующий канал"

Просмотреть файл

@@ -788,7 +788,7 @@
},
{
"id": "api.command_invite.channel.error",
"translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [channel handle] kullanın (https://about.mattermost.com/default-channel-handle-documentation)."
"translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [kanal kısaltması] kullanın (https://about.mattermost.com/default-channel-handle-documentation)."
},
{
"id": "api.command_invite.desc",
@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "{{.User}} kullanıcısını {{.Channel}} kanalına eklemek için yeterli izinleriniz yok."
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "{{.Channel}} kanalı bulunamadı. Lütfen kanalları belirtmek için kanal kısaltması kullanın."
},
{
"id": "api.command_invite.success",
"translation": "{{.User}} kullanıcısı {{.Channel}} kanalına eklendi."
@@ -948,7 +952,7 @@
},
{
"id": "api.command_mute.error",
"translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [channel handle](https://about.mattermost.com/default-channel-handle-documentation) kullanın."
"translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [kanal kısaltması](https://about.mattermost.com/default-channel-handle-documentation) kullanın."
},
{
"id": "api.command_mute.hint",
@@ -960,7 +964,7 @@
},
{
"id": "api.command_mute.no_channel.error",
"translation": "Belirtilen kanal bulunamadı. Lütfen kanalları belirtmek için [channel handle] kullanın (https://about.mattermost.com/default-channel-handle-documentation)."
"translation": "Belirtilen kanal bulunamadı. Lütfen kanalları belirtmek için [kanal kısaltması] kullanın (https://about.mattermost.com/default-channel-handle-documentation)."
},
{
"id": "api.command_mute.not_member.error",
@@ -3772,11 +3776,19 @@
},
{
"id": "app.notification.body.intro.direct.full",
"translation": "Yeni bir doğrudan iletiniz var."
"translation": "Yeni bir Doğrudan İletiniz var."
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "{{.SenderName}} tarafından gönderilen yeni bir doğrudan iletiniz var."
"translation": "@{{.SenderName}} tarafından gönderilen yeni bir Doğrudan İletiniz var."
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "Yeni bir Grup İletiniz var."
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "@{{.SenderName}} tarafından gönderilen yeni bir Grup İletiniz var."
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,19 +3796,27 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "{{.SenderName}} tarafından gönderilen yeni bir bildiriminiz var."
"translation": "@{{.SenderName}} tarafından gönderilen yeni bir bildiriminiz var."
},
{
"id": "app.notification.body.text.direct.full",
"translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "Kanal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "KANAL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
"translation": "Kanal: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.generic",
@@ -3804,7 +3824,15 @@
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] {{ .SenderDisplayName}} tarafından {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni doğrudan ileti"
"translation": "[{{.SiteName}}] @{{ .SenderDisplayName}} tarafından {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Doğrudan İleti gönderildi"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] {{ .ChannelName}} grubuna {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Grup İletisi gönderildi"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] sitesinde {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Grup İletisi gönderildi"
},
{
"id": "app.notification.subject.notification.full",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "\"Soyad Özniteliği\" AD/LDAP alanı zorunludur."
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "\"Oturum Açma Kodu Özniteliği\" AD/LDAP alanı zorunludur."
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "En büyük sayfa boyutu değeri geçersiz."
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "Kanal silinemedi"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "Üyelerin son güncellenme zamanları güncellenirken sorun çıktı"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "Var olan kanal bulunamadı"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "您没有足够的权限在 {{.Channel}} 添加 {{.User}}。"
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "无法找到频道 {{.Channel}}。请使用频道识别查找频道。"
},
{
"id": "api.command_invite.success",
"translation": "已添加 {{.User}} 到 {{.Channel}} 频道。"
@@ -3772,11 +3776,19 @@
},
{
"id": "app.notification.body.intro.direct.full",
"translation": "你有一条新消息。"
"translation": "你有一条新私信。"
},
{
"id": "app.notification.body.intro.direct.generic",
"translation": "您有来自 {{.SenderName}} 的新私信"
"translation": "您有来自 @{{.SenderName}} 的新私信"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "你有一条新团体消息。"
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "您有来自 @{{.SenderName}} 的新团体消息"
},
{
"id": "app.notification.body.intro.notification.full",
@@ -3784,19 +3796,27 @@
},
{
"id": "app.notification.body.intro.notification.generic",
"translation": "您有来自 {{.SenderName}} 的新通知"
"translation": "您有来自 @{{.SenderName}} 的新通知"
},
{
"id": "app.notification.body.text.direct.full",
"translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
"translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "频道:{{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "频道: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
"translation": "频道{{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}"
},
{
"id": "app.notification.body.text.notification.generic",
@@ -3804,7 +3824,15 @@
},
{
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] 来自 {{ .SenderDisplayName}} 的新私信消息于 {{.Month}} {{.Day}}, {{.Year}}"
"translation": "[{{.SiteName}}] 来自 @{{ .SenderDisplayName}} 于 {{.Month}} {{.Day}}, {{.Year}} 发送的新私信消息"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{ .SiteName }}] 在 [{{ .SiteName }}] 于 {{.Month}} {{.Day}}, {{.Year}} 的新团体消息"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] 于 {{.Month}} {{.Day}}, {{.Year}} 的新团体消息"
},
{
"id": "app.notification.subject.notification.full",
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP 栏 \"姓氏\" 为必填。"
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP 栏 \"登入 ID 属性\" 为必填。"
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "无效的最大页面值。"
@@ -5196,7 +5228,7 @@
},
{
"id": "model.config.is_valid.site_url.app_error",
"translation": "站点网址必须为有效URL并以 http:// 或 https:// 开头"
"translation": "站点网址必须为有效URL 并以 http:// 或 https:// 开头"
},
{
"id": "model.config.is_valid.site_url_email_batching.app_error",
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "我们无法删除频道"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "更新成员上次更新时间出现问题"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "我们找不到现有的频道"

Просмотреть файл

@@ -822,6 +822,10 @@
"id": "api.command_invite.permission.app_error",
"translation": "沒有足夠的權限將 {{.User}} 新增至 {{.Channel}}。"
},
{
"id": "api.command_invite.private_channel.app_error",
"translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels."
},
{
"id": "api.command_invite.success",
"translation": "已將 {{.User}} 新增至 {{.Channel}} 頻道。"
@@ -3778,6 +3782,14 @@
"id": "app.notification.body.intro.direct.generic",
"translation": "您有 1 筆新的直接傳訊,來自{{.SenderName}}。"
},
{
"id": "app.notification.body.intro.group_message.full",
"translation": "您有 1 筆新的直接傳訊。"
},
{
"id": "app.notification.body.intro.group_message.generic",
"translation": "您有 1 筆新的直接傳訊,來自{{.SenderName}}。"
},
{
"id": "app.notification.body.intro.notification.full",
"translation": "您有一筆新通知。"
@@ -3794,6 +3806,14 @@
"id": "app.notification.body.text.direct.generic",
"translation": "{{.Month}} {{.Day}}{{.Hour}}:{{.Minute}} {{.Timezone}}"
},
{
"id": "app.notification.body.text.group_message.full",
"translation": "頻道:{{.ChannelName}}<br>{{.SenderName}} - {{.Month}} {{.Day}}{{.Hour}}:{{.Minute}} {{.Timezone}}"
},
{
"id": "app.notification.body.text.group_message.generic",
"translation": "{{.Month}} {{.Day}}{{.Hour}}:{{.Minute}} {{.Timezone}}"
},
{
"id": "app.notification.body.text.notification.full",
"translation": "頻道:{{.ChannelName}}<br>{{.SenderName}} - {{.Month}} {{.Day}}{{.Hour}}:{{.Minute}} {{.Timezone}}"
@@ -3806,6 +3826,14 @@
"id": "app.notification.subject.direct.full",
"translation": "[{{.SiteName}}] 來自 {{.SenderDisplayName}} 的直接傳訊,發於 {{.Year}} {{.Month}} {{.Day}}"
},
{
"id": "app.notification.subject.group_message.full",
"translation": "[{{.SiteName}}] 來自 {{.TeamName}} 的通知,發於 {{.Year}} {{.Month}} {{.Day}}"
},
{
"id": "app.notification.subject.group_message.generic",
"translation": "[{{.SiteName}}] {{.Year}} {{.Month}} {{.Day}} 的新通知[{{.SiteName}}] {{.Year}} {{.Month}} {{.Day}} 的新通知"
},
{
"id": "app.notification.subject.notification.full",
"translation": "[{{.SiteName}}] 來自 {{.TeamName}} 的通知,發於 {{.Year}} {{.Month}} {{.Day}}"
@@ -5022,6 +5050,10 @@
"id": "model.config.is_valid.ldap_lastname",
"translation": "AD/LDAP 欄位 \"姓氏屬性\" 為必須欄位。"
},
{
"id": "model.config.is_valid.ldap_login_id",
"translation": "AD/LDAP 欄位 \"ID 的屬性\" 為必須欄位。"
},
{
"id": "model.config.is_valid.ldap_max_page_size.app_error",
"translation": "無效的最大分頁大小。"
@@ -5922,10 +5954,6 @@
"id": "store.sql_channel.delete.channel.app_error",
"translation": "無法刪除頻道"
},
{
"id": "store.sql_channel.extra_updated.app_error",
"translation": "更新成員的最後更新時間時遇到問題"
},
{
"id": "store.sql_channel.get.existing.app_error",
"translation": "找不到現有的頻道"

Просмотреть файл

@@ -32,21 +32,20 @@ const (
)
type Channel struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
SchemeId *string `json:"scheme_id"`
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
TeamId string `json:"team_id"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
CreatorId string `json:"creator_id"`
}
type ChannelPatch struct {
@@ -134,6 +133,7 @@ func (o *Channel) PreSave() {
o.CreateAt = GetMillis()
o.UpdateAt = o.CreateAt
o.ExtraUpdateAt = 0
}
func (o *Channel) PreUpdate() {

Просмотреть файл

@@ -3622,6 +3622,18 @@ func (c *Client4) GetPlugins() (*PluginsResponse, *Response) {
}
}
// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting
// to the administrator via the system console.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) {
if r, err := c.DoApiGet(c.GetPluginsRoute(), "/statuses"); err != nil {
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return PluginStatusesFromJson(r.Body), BuildResponse(r)
}
}
// RemovePlugin will deactivate and delete a plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) RemovePlugin(id string) (bool, *Response) {

Просмотреть файл

@@ -86,7 +86,7 @@ func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bo
func (o *ClusterDiscovery) IsValid() *AppError {
if len(o.Id) != 26 {
return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest)
return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if len(o.ClusterName) == 0 {

Просмотреть файл

@@ -460,14 +460,17 @@ func (s *ServiceSettings) SetDefaults() {
}
type ClusterSettings struct {
Enable *bool
ClusterName *string
OverrideHostname *string
UseIpAddress *bool
UseExperimentalGossip *bool
ReadOnlyConfig *bool
GossipPort *int
StreamingPort *int
Enable *bool
ClusterName *string
OverrideHostname *string
UseIpAddress *bool
UseExperimentalGossip *bool
ReadOnlyConfig *bool
GossipPort *int
StreamingPort *int
MaxIdleConns *int
MaxIdleConnsPerHost *int
IdleConnTimeoutMilliseconds *int
}
func (s *ClusterSettings) SetDefaults() {
@@ -502,6 +505,18 @@ func (s *ClusterSettings) SetDefaults() {
if s.StreamingPort == nil {
s.StreamingPort = NewInt(8075)
}
if s.MaxIdleConns == nil {
s.MaxIdleConns = NewInt(100)
}
if s.MaxIdleConnsPerHost == nil {
s.MaxIdleConnsPerHost = NewInt(128)
}
if s.IdleConnTimeoutMilliseconds == nil {
s.IdleConnTimeoutMilliseconds = NewInt(90000)
}
}
type MetricsSettings struct {

Просмотреть файл

@@ -49,7 +49,6 @@ type Features struct {
CustomBrand *bool `json:"custom_brand"`
MHPNS *bool `json:"mhpns"`
SAML *bool `json:"saml"`
PasswordRequirements *bool `json:"password_requirements"`
Elasticsearch *bool `json:"elastic_search"`
Announcement *bool `json:"announcement"`
ThemeManagement *bool `json:"theme_management"`
@@ -74,7 +73,6 @@ func (f *Features) ToMap() map[string]interface{} {
"custom_brand": *f.CustomBrand,
"mhpns": *f.MHPNS,
"saml": *f.SAML,
"password": *f.PasswordRequirements,
"elastic_search": *f.Elasticsearch,
"email_notification_contents": *f.EmailNotificationContents,
"data_retention": *f.DataRetention,
@@ -133,10 +131,6 @@ func (f *Features) SetDefaults() {
f.SAML = NewBool(*f.FutureFeatures)
}
if f.PasswordRequirements == nil {
f.PasswordRequirements = NewBool(*f.FutureFeatures)
}
if f.Elasticsearch == nil {
f.Elasticsearch = NewBool(*f.FutureFeatures)
}

Просмотреть файл

@@ -24,7 +24,6 @@ func TestLicenseFeaturesToMap(t *testing.T) {
CheckTrue(t, m["custom_brand"].(bool))
CheckTrue(t, m["mhpns"].(bool))
CheckTrue(t, m["saml"].(bool))
CheckTrue(t, m["password"].(bool))
CheckTrue(t, m["elastic_search"].(bool))
CheckTrue(t, m["email_notification_contents"].(bool))
CheckTrue(t, m["data_retention"].(bool))
@@ -48,7 +47,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
CheckTrue(t, *f.CustomBrand)
CheckTrue(t, *f.MHPNS)
CheckTrue(t, *f.SAML)
CheckTrue(t, *f.PasswordRequirements)
CheckTrue(t, *f.Elasticsearch)
CheckTrue(t, *f.EmailNotificationContents)
CheckTrue(t, *f.DataRetention)
@@ -71,7 +69,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
*f.CustomBrand = true
*f.MHPNS = true
*f.SAML = true
*f.PasswordRequirements = true
*f.Elasticsearch = true
*f.DataRetention = true
*f.MessageExport = true
@@ -91,7 +88,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) {
CheckTrue(t, *f.CustomBrand)
CheckTrue(t, *f.MHPNS)
CheckTrue(t, *f.SAML)
CheckTrue(t, *f.PasswordRequirements)
CheckTrue(t, *f.Elasticsearch)
CheckTrue(t, *f.EmailNotificationContents)
CheckTrue(t, *f.DataRetention)
@@ -176,7 +172,6 @@ func TestLicenseToFromJson(t *testing.T) {
CheckBool(t, *f1.CustomBrand, *f.CustomBrand)
CheckBool(t, *f1.MHPNS, *f.MHPNS)
CheckBool(t, *f1.SAML, *f.SAML)
CheckBool(t, *f1.PasswordRequirements, *f.PasswordRequirements)
CheckBool(t, *f1.Elasticsearch, *f.Elasticsearch)
CheckBool(t, *f1.DataRetention, *f.DataRetention)
CheckBool(t, *f1.MessageExport, *f.MessageExport)

44
model/plugin_status.go Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
PluginStateNotRunning = 0
PluginStateStarting = 1
PluginStateRunning = 2
PluginStateFailedToStart = 3
PluginStateFailedToStayRunning = 4
PluginStateStopping = 5
)
// PluginStatus provides a cluster-aware view of installed plugins.
type PluginStatus struct {
PluginId string `json:"plugin_id"`
ClusterId string `json:"cluster_id"`
PluginPath string `json:"plugin_path"`
State int `json:"state"`
IsSandboxed bool `json:"is_sandboxed"`
IsPrepackaged bool `json:"is_prepackaged"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
}
type PluginStatuses []*PluginStatus
func (m *PluginStatuses) ToJson() string {
b, _ := json.Marshal(m)
return string(b)
}
func PluginStatusesFromJson(data io.Reader) PluginStatuses {
var m PluginStatuses
json.NewDecoder(data).Decode(&m)
return m
}

Просмотреть файл

@@ -10,44 +10,45 @@ import (
)
const (
WEBSOCKET_EVENT_TYPING = "typing"
WEBSOCKET_EVENT_POSTED = "posted"
WEBSOCKET_EVENT_POST_EDITED = "post_edited"
WEBSOCKET_EVENT_POST_DELETED = "post_deleted"
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created"
WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated"
WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated"
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_GROUP_ADDED = "group_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
WEBSOCKET_EVENT_DELETE_TEAM = "delete_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_UPDATED = "user_updated"
WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated"
WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed"
WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted"
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added"
WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed"
WEBSOCKET_EVENT_RESPONSE = "response"
WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added"
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
WEBSOCKET_EVENT_TYPING = "typing"
WEBSOCKET_EVENT_POSTED = "posted"
WEBSOCKET_EVENT_POST_EDITED = "post_edited"
WEBSOCKET_EVENT_POST_DELETED = "post_deleted"
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created"
WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated"
WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated"
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_GROUP_ADDED = "group_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
WEBSOCKET_EVENT_DELETE_TEAM = "delete_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_UPDATED = "user_updated"
WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated"
WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"
WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed"
WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed"
WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted"
WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message"
WEBSOCKET_EVENT_STATUS_CHANGE = "status_change"
WEBSOCKET_EVENT_HELLO = "hello"
WEBSOCKET_EVENT_WEBRTC = "webrtc"
WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge"
WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added"
WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed"
WEBSOCKET_EVENT_RESPONSE = "response"
WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added"
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed" // EXPERIMENTAL - SUBJECT TO CHANGE
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
)
type WebSocketMessage interface {

Просмотреть файл

@@ -108,7 +108,7 @@ func (env *Environment) IsPluginActive(pluginId string) bool {
}
// Activates the plugin with the given id.
func (env *Environment) ActivatePlugin(id string) error {
func (env *Environment) ActivatePlugin(id string, onError func(error)) error {
env.mutex.Lock()
defer env.mutex.Unlock()
@@ -117,7 +117,7 @@ func (env *Environment) ActivatePlugin(id string) error {
}
if _, ok := env.activePlugins[id]; ok {
return nil
return fmt.Errorf("plugin already active: %v", id)
}
plugins, err := ScanSearchPath(env.searchPath)
if err != nil {
@@ -156,6 +156,14 @@ func (env *Environment) ActivatePlugin(id string) error {
if err := supervisor.Start(api); err != nil {
return errors.Wrapf(err, "unable to start plugin: %v", id)
}
if onError != nil {
go func() {
err := supervisor.Wait()
if err != nil {
onError(err)
}
}()
}
activePlugin.Supervisor = supervisor
}

Просмотреть файл

@@ -56,6 +56,10 @@ func (m *MockSupervisor) Hooks() plugin.Hooks {
return m.Called().Get(0).(plugin.Hooks)
}
func (m *MockSupervisor) Wait() error {
return m.Called().Get(0).(error)
}
func initTmpDir(t *testing.T, files map[string]string) string {
success := false
dir, err := ioutil.TempDir("", "mm-plugin-test")
@@ -130,7 +134,7 @@ func TestEnvironment(t *testing.T) {
activePlugins := env.ActivePlugins()
assert.Len(t, activePlugins, 0)
assert.Error(t, env.ActivatePlugin("x"))
assert.Error(t, env.ActivatePlugin("x", nil))
var api struct{ plugin.API }
var supervisor MockSupervisor
@@ -145,11 +149,11 @@ func TestEnvironment(t *testing.T) {
supervisor.On("Stop").Return(nil)
supervisor.On("Hooks").Return(&hooks)
assert.NoError(t, env.ActivatePlugin("foo"))
assert.NoError(t, env.ActivatePlugin("foo", nil))
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
activePlugins = env.ActivePlugins()
assert.Len(t, activePlugins, 1)
assert.NoError(t, env.ActivatePlugin("foo"))
assert.Error(t, env.ActivatePlugin("foo", nil))
assert.True(t, env.IsPluginActive("foo"))
hooks.On("OnDeactivate").Return(nil)
@@ -157,7 +161,7 @@ func TestEnvironment(t *testing.T) {
assert.Error(t, env.DeactivatePlugin("foo"))
assert.False(t, env.IsPluginActive("foo"))
assert.NoError(t, env.ActivatePlugin("foo"))
assert.NoError(t, env.ActivatePlugin("foo", nil))
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
assert.Equal(t, env.SearchPath(), dir)
@@ -184,7 +188,7 @@ func TestEnvironment_DuplicatePluginError(t *testing.T) {
require.NoError(t, err)
defer env.Shutdown()
assert.Error(t, env.ActivatePlugin("foo"))
assert.Error(t, env.ActivatePlugin("foo", nil))
assert.Empty(t, env.ActivePluginIds())
}
@@ -200,7 +204,7 @@ func TestEnvironment_BadSearchPathError(t *testing.T) {
require.NoError(t, err)
defer env.Shutdown()
assert.Error(t, env.ActivatePlugin("foo"))
assert.Error(t, env.ActivatePlugin("foo", nil))
assert.Empty(t, env.ActivePluginIds())
}
@@ -244,7 +248,7 @@ func TestEnvironment_ActivatePluginErrors(t *testing.T) {
hooks.Mock = mock.Mock{}
provider.Mock = mock.Mock{}
setup()
assert.Error(t, env.ActivatePlugin("foo"))
assert.Error(t, env.ActivatePlugin("foo", nil))
assert.Empty(t, env.ActivePluginIds())
supervisor.AssertExpectations(t)
hooks.AssertExpectations(t)
@@ -285,7 +289,7 @@ func TestEnvironment_ShutdownError(t *testing.T) {
hooks.On("OnDeactivate").Return(fmt.Errorf("test error"))
assert.NoError(t, env.ActivatePlugin("foo"))
assert.NoError(t, env.ActivatePlugin("foo", nil))
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
assert.Len(t, env.Shutdown(), 2)
}
@@ -332,7 +336,7 @@ func TestEnvironment_ConcurrentHookInvocations(t *testing.T) {
}
})
assert.NoError(t, env.ActivatePlugin("foo"))
assert.NoError(t, env.ActivatePlugin("foo", nil))
rec := httptest.NewRecorder()
@@ -391,7 +395,7 @@ func TestEnvironment_HooksForPlugins(t *testing.T) {
Text: "bar",
}, nil)
assert.NoError(t, env.ActivatePlugin("foo"))
assert.NoError(t, env.ActivatePlugin("foo", nil))
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{

Просмотреть файл

@@ -174,6 +174,14 @@ func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) {
bundle := model.BundleInfoForPath(dir)
supervisor, err := sp(bundle)
require.NoError(t, err)
var supervisorWaitErr error
supervisorWaitDone := make(chan bool, 1)
go func() {
supervisorWaitErr = supervisor.Wait()
close(supervisorWaitDone)
}()
require.NoError(t, supervisor.Start(&api))
failed := false
@@ -189,7 +197,21 @@ func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) {
time.Sleep(time.Millisecond * 100)
}
assert.True(t, recovered)
select {
case <-supervisorWaitDone:
require.Fail(t, "supervisor.Wait() unexpectedly returned")
case <-time.After(500 * time.Millisecond):
}
require.NoError(t, supervisor.Stop())
select {
case <-supervisorWaitDone:
require.Nil(t, supervisorWaitErr)
case <-time.After(5000 * time.Millisecond):
require.Fail(t, "supervisor.Wait() failed to return")
}
}
// Crashed plugins should be relaunched at most three times.
@@ -239,6 +261,14 @@ func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFun
bundle := model.BundleInfoForPath(dir)
supervisor, err := sp(bundle)
require.NoError(t, err)
var supervisorWaitErr error
supervisorWaitDone := make(chan bool, 1)
go func() {
supervisorWaitErr = supervisor.Wait()
close(supervisorWaitDone)
}()
require.NoError(t, supervisor.Start(&api))
for attempt := 1; attempt <= 4; attempt++ {
@@ -264,10 +294,19 @@ func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFun
}
if attempt < 4 {
require.Nil(t, supervisorWaitErr)
require.True(t, recovered, "failed to recover after attempt %d", attempt)
} else {
require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt)
}
}
select {
case <-supervisorWaitDone:
require.NotNil(t, supervisorWaitErr)
case <-time.After(500 * time.Millisecond):
require.Fail(t, "supervisor.Wait() failed to return after plugin crashed")
}
require.NoError(t, supervisor.Stop())
}

Просмотреть файл

@@ -32,6 +32,7 @@ type Supervisor struct {
cancel context.CancelFunc
newProcess func(context.Context) (Process, io.ReadWriteCloser, error)
pluginId string
pluginErr error
}
var _ plugin.Supervisor = (*Supervisor)(nil)
@@ -55,6 +56,13 @@ func (s *Supervisor) Start(api plugin.API) error {
}
}
// Waits for the supervisor to stop (on demand or of its own accord), returning any error that
// triggered the supervisor to stop.
func (s *Supervisor) Wait() error {
<-s.done
return s.pluginErr
}
// Stops the plugin.
func (s *Supervisor) Stop() error {
s.cancel()
@@ -70,7 +78,7 @@ func (s *Supervisor) Hooks() plugin.Hooks {
func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) {
defer func() {
s.done <- true
close(s.done)
}()
done := ctx.Done()
for i := 0; i <= MaxProcessRestarts; i++ {
@@ -81,10 +89,11 @@ func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API
default:
start = nil
if i < MaxProcessRestarts {
mlog.Debug("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId))
mlog.Error("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId))
time.Sleep(time.Duration((1 + i*i)) * time.Second)
} else {
mlog.Debug("Plugin terminated unexpectedly too many times", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts))
s.pluginErr = fmt.Errorf("plugin terminated unexpectedly too many times")
mlog.Error("Plugin shutdown", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts), mlog.Err(s.pluginErr))
}
}
}

Просмотреть файл

@@ -7,6 +7,7 @@ package plugin
// type is only relevant to the server, and isn't used by the plugins themselves.
type Supervisor interface {
Start(API) error
Wait() error
Stop() error
Hooks() Hooks
}

Просмотреть файл

@@ -720,6 +720,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
c1.Type = model.CHANNEL_OPEN
c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel)
c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = model.NewId()
u1.Nickname = model.NewId()
@@ -744,6 +747,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&o2))
c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count := (<-ss.Channel().GetMemberCount(o1.ChannelId, true)).Data.(int64)
if count != 2 {
t.Fatal("should have saved 2 members")
@@ -774,6 +780,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
t.Fatal("should have removed 1 member")
}
c1t3 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t3.ExtraUpdateAt, "ExtraUpdateAt should be 0")
member := (<-ss.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(*model.ChannelMember)
if member.ChannelId != o1.ChannelId {
t.Fatal("should have go member")
@@ -782,6 +791,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) {
if err := (<-ss.Channel().SaveMember(&o1)).Err; err == nil {
t.Fatal("Should have been a duplicate")
}
c1t4 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t4.ExtraUpdateAt, "ExtraUpdateAt should be 0")
}
func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
@@ -792,6 +804,9 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
c1.Type = model.CHANNEL_OPEN
c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel)
c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0")
u1 := model.User{}
u1.Email = model.NewId()
u1.Nickname = model.NewId()
@@ -816,6 +831,9 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) {
o2.NotifyProps = model.GetDefaultChannelNotifyProps()
store.Must(ss.Channel().SaveMember(&o2))
c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel)
assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0")
count := (<-ss.Channel().GetMemberCount(o1.ChannelId, false)).Data.(int64)
if count != 2 {
t.Fatal("should have saved 2 members")

Просмотреть файл

@@ -602,6 +602,11 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
if license != nil {
props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
@@ -662,14 +667,6 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable)
}
if *license.Features.PasswordRequirements {
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
}
if *license.Features.Announcement {
props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
props["BannerText"] = *c.AnnouncementSettings.BannerText

Просмотреть файл

@@ -139,7 +139,6 @@ func GetClientLicense(l *model.License) map[string]string {
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
props["CustomBrand"] = strconv.FormatBool(*l.Features.CustomBrand)
props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS)
props["PasswordRequirements"] = strconv.FormatBool(*l.Features.PasswordRequirements)
props["Announcement"] = strconv.FormatBool(*l.Features.Announcement)
props["Elasticsearch"] = strconv.FormatBool(*l.Features.Elasticsearch)
props["DataRetention"] = strconv.FormatBool(*l.Features.DataRetention)