diff options
39 files changed, 1064 insertions, 259 deletions
diff --git a/api4/plugin.go b/api4/plugin.go index 37fbf12cd..ab026ab5f 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -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 } diff --git a/app/app.go b/app/app.go index 16470d850..e5a496c6b 100644 --- a/app/app.go +++ b/app/app.go @@ -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 diff --git a/app/apptestlib.go b/app/apptestlib.go index ffd1da055..ec4992a75 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -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 } diff --git a/app/authentication.go b/app/authentication.go index 5c91f8038..087a9b230 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -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 { diff --git a/app/cluster_discovery.go b/app/cluster_discovery.go index f7443680c..250744279 100644 --- a/app/cluster_discovery.go +++ b/app/cluster_discovery.go @@ -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() +} diff --git a/app/plugin.go b/app/plugin.go index 0d3415f4c..f6cb6bdda 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -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 + enabled := false + if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok { + enabled = state.Enable + } + + a.pluginStatuses[plugin.Manifest.Id] = &model.PluginStatus{ + ClusterId: a.GetClusterId(), + PluginId: plugin.Manifest.Id, + PluginPath: filepath.Dir(plugin.ManifestPath), + IsSandboxed: a.IsPluginSandboxSupported, + Name: plugin.Manifest.Name, + Description: plugin.Manifest.Description, + Version: plugin.Manifest.Version, + } + + if activate && enabled { + a.setPluginActive(plugin, activate) + } else if !activate { + a.setPluginActive(plugin, activate) + } + } + + if err := a.notifyPluginStatusesChanged(); err != nil { + mlog.Error("failed to notify plugin status changed", mlog.Err(err)) + } +} + +func (a *App) setPluginActiveById(id string, activate bool) { + plugins, err := a.PluginEnv.Plugins() + if err != nil { + mlog.Error(fmt.Sprintf("Cannot setPluginActiveById(%t)", activate), mlog.String("plugin_id", id), mlog.Err(err)) + return + } - pluginState := &model.PluginState{Enable: false} - if state, ok := a.Config().PluginSettings.PluginStates[id]; ok { - pluginState = state + 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 + } - active := a.PluginEnv.IsPluginActive(id) + id := plugin.Manifest.Id - if activate && pluginState.Enable && !active { + 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 { + if a.PluginEnv != nil { return } - if a.PluginEnv != nil { + 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 supervisorOverride != nil { - options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) - } else if err := sandbox.CheckSupport(); err != nil { + 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)) - options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) } else { + a.IsPluginSandboxSupported = true + } + + if supervisorOverride != nil { + options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) + } else if a.IsPluginSandboxSupported { options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) + } else { + options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) } if env, err := pluginenv.New(options...); err != nil { @@ -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()) diff --git a/app/plugin_test.go b/app/plugin_test.go index 9ad5dc1fa..db5954d4d 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -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) } diff --git a/config/default.json b/config/default.json index c80ff48de..1c4608c03 100644 --- a/config/default.json +++ b/config/default.json @@ -323,7 +323,10 @@ "UseExperimentalGossip": false, "ReadOnlyConfig": true, "GossipPort": 8074, - "StreamingPort": 8075 + "StreamingPort": 8075, + "MaxIdleConns": 100, + "MaxIdleConnsPerHost": 128, + "IdleConnTimeoutMilliseconds": 90000 }, "MetricsSettings": { "Enable": false, diff --git a/einterfaces/cluster.go b/einterfaces/cluster.go index b5ef4772a..dd9c57f11 100644 --- a/einterfaces/cluster.go +++ b/einterfaces/cluster.go @@ -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 } diff --git a/i18n/de.json b/i18n/de.json index df7f4ab21..58c4829e8 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -823,6 +823,10 @@ "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." }, @@ -3779,6 +3783,14 @@ "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." }, @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,6 +3827,14 @@ "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}}" }, @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/en.json b/i18n/en.json index 59a600f23..9a39c60ba 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3855,14 +3855,6 @@ "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." }, @@ -3871,6 +3863,14 @@ "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}}" }, @@ -3879,19 +3879,19 @@ "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.notification.full", + "id": "app.notification.body.text.group_message.full", "translation": "Channel: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.notification.generic", + "id": "app.notification.body.text.group_message.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.group_message.full", + "id": "app.notification.body.text.notification.full", "translation": "Channel: {{.ChannelName}}<br>@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.group_message.generic", + "id": "app.notification.body.text.notification.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { @@ -3899,10 +3899,6 @@ "translation": "[{{.SiteName}}] New Direct Message from @{{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}" }, { - "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" - }, - { "id": "app.notification.subject.group_message.full", "translation": "[{{ .SiteName }}] New Group Message in {{ .ChannelName}} on {{.Month}} {{.Day}}, {{.Year}}" }, @@ -3911,6 +3907,10 @@ "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." }, @@ -3927,6 +3927,10 @@ "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." }, @@ -3971,10 +3975,18 @@ "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." }, @@ -4871,6 +4883,10 @@ "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" }, @@ -5119,14 +5135,14 @@ "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." }, diff --git a/i18n/es.json b/i18n/es.json index 0fb44ec97..af93e16ec 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -823,6 +823,10 @@ "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", @@ -5023,6 +5051,10 @@ "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", @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/fr.json b/i18n/fr.json index c408bc60d..7fca34fd0 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -823,6 +823,10 @@ "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}}." }, @@ -3779,6 +3783,14 @@ "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." }, @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,6 +3827,14 @@ "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}}" }, @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/it.json b/i18n/it.json index a96329287..b063ffbb9 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -823,6 +823,10 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/ja.json b/i18n/ja.json index 2458d7c09..daa007a3a 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -823,6 +823,10 @@ "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}} に追加されました。" }, @@ -3779,6 +3783,14 @@ "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": "新しい通知があります。" }, @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,6 +3827,14 @@ "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}}の通知" }, @@ -5023,6 +5051,10 @@ "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": "最大ページサイズの値が不正です。" }, @@ -5923,10 +5955,6 @@ "translation": "チャンネルを削除できませんでした" }, { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "メンバーの最終更新時刻の更新に問題があります" - }, - { "id": "store.sql_channel.get.existing.app_error", "translation": "チャンネルが見付かりませんでした" }, diff --git a/i18n/ko.json b/i18n/ko.json index cd5e8b9dd..d9c06ffca 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -823,6 +823,10 @@ "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", @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,8 +3827,16 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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": "존재하는 채널을 찾지 못했습니다." }, diff --git a/i18n/nl.json b/i18n/nl.json index 1294dae85..14edb8d66 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -823,6 +823,10 @@ "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", @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,8 +3827,16 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/pl.json b/i18n/pl.json index 3f03eabb6..aa95466a5 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -823,6 +823,10 @@ "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", @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,8 +3827,16 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 290bea915..5b299756a 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -823,6 +823,10 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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" }, diff --git a/i18n/ru.json b/i18n/ru.json index 445d4a9f9..e34d1b8b0 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -823,6 +823,10 @@ "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", @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,8 +3827,16 @@ "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", @@ -5023,6 +5051,10 @@ "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": "Неверное значение максимального размера страницы." }, @@ -5923,10 +5955,6 @@ "translation": "Неудачная попытка удалить канал" }, { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Проблема с обновлением времени последнего входа участника" - }, - { "id": "store.sql_channel.get.existing.app_error", "translation": "Не удалось найти существующий канал" }, diff --git a/i18n/tr.json b/i18n/tr.json index 852bebad2..091f358ef 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -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", @@ -823,6 +823,10 @@ "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", @@ -5023,6 +5051,10 @@ "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." }, @@ -5923,10 +5955,6 @@ "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ı" }, diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 4c9a11deb..43825d869 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -823,6 +823,10 @@ "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", @@ -5023,6 +5051,10 @@ "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", @@ -5923,10 +5955,6 @@ "translation": "我们无法删除频道" }, { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "更新成员上次更新时间出现问题" - }, - { "id": "store.sql_channel.get.existing.app_error", "translation": "我们找不到现有的频道" }, diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index c76f097ba..82ec3587e 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -823,6 +823,10 @@ "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}} 頻道。" }, @@ -3779,6 +3783,14 @@ "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": "您有一筆新通知。" }, @@ -3795,6 +3807,14 @@ "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}}" }, @@ -3807,6 +3827,14 @@ "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}}" }, @@ -5023,6 +5051,10 @@ "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": "無效的最大分頁大小。" }, @@ -5923,10 +5955,6 @@ "translation": "無法刪除頻道" }, { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "更新成員的最後更新時間時遇到問題" - }, - { "id": "store.sql_channel.get.existing.app_error", "translation": "找不到現有的頻道" }, diff --git a/model/channel.go b/model/channel.go index 5e5c741f3..950e910dd 100644 --- a/model/channel.go +++ b/model/channel.go @@ -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() { diff --git a/model/client4.go b/model/client4.go index afd8a6bc4..c2b6ba948 100644 --- a/model/client4.go +++ b/model/client4.go @@ -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) { diff --git a/model/cluster_discovery.go b/model/cluster_discovery.go index 89e5fc95e..5d5b0465d 100644 --- a/model/cluster_discovery.go +++ b/model/cluster_discovery.go @@ -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 { diff --git a/model/config.go b/model/config.go index 7a2125061..7c11860d2 100644 --- a/model/config.go +++ b/model/config.go @@ -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 { diff --git a/model/license.go b/model/license.go index dea326287..b6a6f2ac8 100644 --- a/model/license.go +++ b/model/license.go @@ -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) } diff --git a/model/license_test.go b/model/license_test.go index 93f2ff61a..a9379d78e 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -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) diff --git a/model/plugin_status.go b/model/plugin_status.go new file mode 100644 index 000000000..1ae64ff89 --- /dev/null +++ b/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 +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 08c238480..071975d6c 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -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 { diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go index 947eda86d..f704aa5bb 100644 --- a/plugin/pluginenv/environment.go +++ b/plugin/pluginenv/environment.go @@ -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 } diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go index 91d639f69..8c1397799 100644 --- a/plugin/pluginenv/environment_test.go +++ b/plugin/pluginenv/environment_test.go @@ -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{ diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go index 2ae065621..d225f96fc 100644 --- a/plugin/rpcplugin/rpcplugintest/supervisor.go +++ b/plugin/rpcplugin/rpcplugintest/supervisor.go @@ -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()) } diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go index 6e26d5682..246747c89 100644 --- a/plugin/rpcplugin/supervisor.go +++ b/plugin/rpcplugin/supervisor.go @@ -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)) } } } diff --git a/plugin/supervisor.go b/plugin/supervisor.go index 6cb7445f7..f20df7040 100644 --- a/plugin/supervisor.go +++ b/plugin/supervisor.go @@ -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 } diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 7e0a2d552..eea79d42f 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -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") diff --git a/utils/config.go b/utils/config.go index dd782c0fc..18e25c999 100644 --- a/utils/config.go +++ b/utils/config.go @@ -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 diff --git a/utils/license.go b/utils/license.go index 1d76cf994..8bb214734 100644 --- a/utils/license.go +++ b/utils/license.go @@ -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) |