diff --git a/api4/channel_test.go b/api4/channel_test.go index 16e432bf0d..6f8f5aeabe 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "sort" "strings" "sync" @@ -4584,8 +4583,6 @@ func TestViewChannelWithoutCollapsedThreads(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn diff --git a/api4/cloud.go b/api4/cloud.go index 2499164fac..d517291d48 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -40,6 +40,7 @@ func (api *API) InitCloud() { api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET") + api.BaseRoutes.Cloud.Handle("/subscription/expand", api.APISessionRequired(GetLicenseExpandStatus)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT") // GET /api/v4/cloud/request-trial @@ -413,6 +414,34 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func GetLicenseExpandStatus(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) { + c.SetPermissionError(model.PermissionManageLicenseInformation) + return + } + + _, token, err := c.App.Srv().GenerateLicenseRenewalLink() + + if err != nil { + c.Err = err + return + } + + res, cloudErr := c.App.Cloud().GetLicenseExpandStatus(c.AppContext.Session().UserId, token) + if cloudErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr) + return + } + + json, jsonErr := json.Marshal(res) + if jsonErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) + return + } + + w.Write(json) +} + func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Channels().License().IsCloud() { c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 75bf89f631..399ee67f45 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -649,6 +649,58 @@ func TestGetCloudProducts(t *testing.T) { }) } +func Test_GetExpandStatsForSubscription(t *testing.T) { + isExpandable := &model.SubscriptionExpandStatus{ + IsExpandable: true, + } + + licenseId := "licenseID" + + t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats(licenseId) + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden") + }) + + t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats("") + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request") + }) +} + func TestGetSelfHostedProducts(t *testing.T) { products := []*model.Product{ { diff --git a/api4/drafts.go b/api4/drafts.go index 102c164b8f..c3640f7a84 100644 --- a/api4/drafts.go +++ b/api4/drafts.go @@ -124,7 +124,6 @@ func deleteDraft(c *Context, w http.ResponseWriter, r *http.Request) { switch { case err.StatusCode == http.StatusNotFound: // If the draft doesn't exist in the server, we don't need to delete. - mlog.Debug("Unable to find the draft", mlog.Err(err)) ReturnStatusOK(w) default: c.Err = err diff --git a/api4/group.go b/api4/group.go index 72c78e7179..59b9ad12c1 100644 --- a/api4/group.go +++ b/api4/group.go @@ -1185,8 +1185,8 @@ func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionDeleteCustomGroup) { - c.SetPermissionError(model.PermissionDeleteCustomGroup) + if !c.App.SessionHasPermissionToGroup(*c.AppContext.Session(), c.Params.GroupId, model.PermissionRestoreCustomGroup) { + c.SetPermissionError(model.PermissionRestoreCustomGroup) return } diff --git a/api4/group_test.go b/api4/group_test.go index a612291d8c..7fdf38147e 100644 --- a/api4/group_test.go +++ b/api4/group_test.go @@ -231,7 +231,13 @@ func TestUndeleteGroup(t *testing.T) { _, response, err := th.Client.DeleteGroup(validGroup.Id) require.NoError(t, err) CheckOKStatus(t, response) + th.RemovePermissionFromRole(model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId) + // shouldn't allow restoring unless user has required permission + _, response, err = th.Client.RestoreGroup(validGroup.Id, "") + require.Error(t, err) + CheckForbiddenStatus(t, response) + th.AddPermissionToRole(model.PermissionRestoreCustomGroup.Id, model.SystemUserRoleId) _, response, err = th.Client.RestoreGroup(validGroup.Id, "") require.NoError(t, err) CheckOKStatus(t, response) diff --git a/api4/post.go b/api4/post.go index ff707c178c..34bcf21c2b 100644 --- a/api4/post.go +++ b/api4/post.go @@ -802,7 +802,8 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) { } auditRec := c.MakeAuditRecord("patchPost", audit.Fail) - auditRec.AddEventParameter("patch", post) + auditRec.AddEventParameter("id", c.Params.PostId) + auditRec.AddEventParameter("patch", post.Auditable()) defer c.LogAuditRecWithLevel(auditRec, app.LevelContent) // Updating the file_ids of a post is not a supported operation and will be ignored diff --git a/api4/post_test.go b/api4/post_test.go index 8594fae136..7f3ec6c442 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -12,7 +12,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" "reflect" "sort" "strings" @@ -2954,8 +2953,6 @@ func TestSetChannelUnread(t *testing.T) { } func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th := Setup(t).InitBasic() defer th.TearDown() th.App.UpdateConfig(func(cfg *model.Config) { diff --git a/api4/user_test.go b/api4/user_test.go index affd768fd7..a20ae682a1 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5794,8 +5794,6 @@ func TestUpdatePassword(t *testing.T) { func TestGetThreadsForUser(t *testing.T) { os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true") defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY") - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th := Setup(t).InitBasic() defer th.TearDown() @@ -6151,8 +6149,6 @@ func TestGetThreadsForUser(t *testing.T) { func TestThreadSocketEvents(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.ConfigStore.SetReadOnlyFF(false) defer th.ConfigStore.SetReadOnlyFF(true) @@ -6533,8 +6529,6 @@ func TestMaintainUnreadRepliesInThread(t *testing.T) { defer th.UnlinkUserFromTeam(th.SystemAdminUser, th.BasicTeam) th.AddUserToChannel(th.SystemAdminUser, th.BasicChannel) defer th.RemoveUserFromChannel(th.SystemAdminUser, th.BasicChannel) - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn @@ -6590,8 +6584,7 @@ func TestMaintainUnreadRepliesInThread(t *testing.T) { func TestThreadCounts(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn @@ -6633,8 +6626,6 @@ func TestThreadCounts(t *testing.T) { func TestSingleThreadGet(t *testing.T) { os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true") defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY") - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th := Setup(t).InitBasic() defer th.TearDown() @@ -6704,8 +6695,7 @@ func TestMaintainUnreadMentionsInThread(t *testing.T) { th.AddUserToChannel(th.SystemAdminUser, th.BasicChannel) defer th.RemoveUserFromChannel(th.SystemAdminUser, th.BasicChannel) client := th.Client - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn @@ -6768,8 +6758,7 @@ func TestMaintainUnreadMentionsInThread(t *testing.T) { func TestReadThreads(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn @@ -6872,8 +6861,7 @@ func TestReadThreads(t *testing.T) { func TestMarkThreadUnreadMentionCount(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn diff --git a/app/app_test.go b/app/app_test.go index 9f4d2fa0a9..05e12792b0 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -168,6 +168,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PermissionCreateCustomGroup.Id, model.PermissionEditCustomGroup.Id, model.PermissionDeleteCustomGroup.Id, + model.PermissionRestoreCustomGroup.Id, model.PermissionManageCustomGroupMembers.Id, }, "system_post_all": { @@ -228,6 +229,7 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PermissionEditCustomGroup.Id, model.PermissionDeleteCustomGroup.Id, model.PermissionManageCustomGroupMembers.Id, + model.PermissionRestoreCustomGroup.Id, model.PermissionListPublicTeams.Id, model.PermissionJoinPublicTeams.Id, model.PermissionCreateDirectChannel.Id, diff --git a/app/channel_test.go b/app/channel_test.go index bf21274763..af162e45b9 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "os" "sort" "strings" "sync" @@ -2127,8 +2126,7 @@ func TestViewChannelCollapsedThreadsTurnedOff(t *testing.T) { th.AddUserToChannel(u2, c1) // Enable CRT - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn @@ -2198,8 +2196,6 @@ func TestViewChannelCollapsedThreadsTurnedOff(t *testing.T) { func TestMarkChannelAsUnreadFromPostCollapsedThreadsTurnedOff(t *testing.T) { // Enable CRT - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th := Setup(t).InitBasic() defer th.TearDown() @@ -2286,8 +2282,7 @@ func TestMarkChannelAsUnreadFromPostCollapsedThreadsTurnedOff(t *testing.T) { } func TestMarkUnreadCRTOffUpdatesThreads(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th := Setup(t).InitBasic() defer th.TearDown() th.App.UpdateConfig(func(cfg *model.Config) { diff --git a/app/export.go b/app/export.go index 6afd544534..8e52fa74e7 100644 --- a/app/export.go +++ b/app/export.go @@ -398,8 +398,15 @@ func (a *App) buildUserNotifyProps(notifyProps model.StringMap) *imports.UserNot func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) + var postProcessCount uint64 + logCheckpoint := time.Now() for { + if time.Since(logCheckpoint) > 5*time.Minute { + ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d posts", postProcessCount)) + logCheckpoint = time.Now() + } + posts, nErr := a.Srv().Store().Post().GetParentsForExportAfter(1000, afterId) if nErr != nil { return nil, model.NewAppError("exportAllPosts", "app.post.get_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) @@ -411,6 +418,7 @@ func (a *App) exportAllPosts(ctx request.CTX, writer io.Writer, withAttachments for _, post := range posts { afterId = post.Id + postProcessCount++ // Skip deleted. if post.DeleteAt != 0 { @@ -677,7 +685,15 @@ func (a *App) buildFavoritedByList(channelID string) ([]string, *model.AppError) func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttachments bool) ([]imports.AttachmentImportData, *model.AppError) { var attachments []imports.AttachmentImportData afterId := strings.Repeat("0", 26) + var postProcessCount uint64 + logCheckpoint := time.Now() + for { + if time.Since(logCheckpoint) > 5*time.Minute { + ctx.Logger().Debug(fmt.Sprintf("Bulk Export: processed %d direct posts", postProcessCount)) + logCheckpoint = time.Now() + } + posts, err := a.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, afterId) if err != nil { return nil, model.NewAppError("exportAllDirectPosts", "app.post.get_direct_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err) @@ -689,6 +705,7 @@ func (a *App) exportAllDirectPosts(ctx request.CTX, writer io.Writer, withAttach for _, post := range posts { afterId = post.Id + postProcessCount++ // Skip deleted. if post.DeleteAt != 0 { diff --git a/app/file.go b/app/file.go index a47e817003..7a66d25010 100644 --- a/app/file.go +++ b/app/file.go @@ -824,6 +824,7 @@ func (t *UploadFileTask) postprocessImage(file io.Reader) { _, aerr := t.writeFile(r, path) if aerr != nil { mlog.Error("Unable to upload", mlog.String("path", path), mlog.Err(aerr)) + r.CloseWithError(aerr) // always returns nil return } } diff --git a/app/migrations.go b/app/migrations.go index 21295aa840..70c11b75fc 100644 --- a/app/migrations.go +++ b/app/migrations.go @@ -46,7 +46,7 @@ func (s *Server) doAdvancedPermissionsMigration() { // If this failed for reasons other than the role already existing, don't mark the migration as done. fetchedRole, err := s.Store().Role().GetByName(context.Background(), role.Name) if err != nil { - mlog.Critical("Failed to migrate role to database.", mlog.Err(err)) + mlog.Fatal("Failed to migrate role to database.", mlog.Err(err)) allSucceeded = false continue } @@ -59,7 +59,7 @@ func (s *Server) doAdvancedPermissionsMigration() { role.Id = fetchedRole.Id if _, err = s.Store().Role().Save(role); err != nil { // Role is not the same, but failed to update. - mlog.Critical("Failed to migrate role to database.", mlog.Err(err)) + mlog.Fatal("Failed to migrate role to database.", mlog.Err(err)) allSucceeded = false } } @@ -81,7 +81,7 @@ func (s *Server) doAdvancedPermissionsMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark advanced permissions migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark advanced permissions migration as completed.", mlog.Err(err)) } } @@ -114,21 +114,21 @@ func (s *Server) doEmojisPermissionsMigration() { // Emoji creation is set to all by default role, err = s.GetRoleByName(context.Background(), model.SystemUserRoleId) if err != nil { - mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) + mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) return } if role != nil { role.Permissions = append(role.Permissions, model.PermissionCreateEmojis.Id, model.PermissionDeleteEmojis.Id) if _, nErr := s.Store().Role().Save(role); nErr != nil { - mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(nErr)) + mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(nErr)) return } } systemAdminRole, err = s.GetRoleByName(context.Background(), model.SystemAdminRoleId) if err != nil { - mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) + mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) return } @@ -138,7 +138,7 @@ func (s *Server) doEmojisPermissionsMigration() { model.PermissionDeleteOthersEmojis.Id, ) if _, err := s.Store().Role().Save(systemAdminRole); err != nil { - mlog.Critical("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) + mlog.Fatal("Failed to migrate emojis creation permissions from mattermost config.", mlog.Err(err)) return } @@ -148,7 +148,7 @@ func (s *Server) doEmojisPermissionsMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark emojis permissions migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark emojis permissions migration as completed.", mlog.Err(err)) } } @@ -167,26 +167,26 @@ func (s *Server) doGuestRolesCreationMigration() { allSucceeded := true if _, err := s.Store().Role().GetByName(context.Background(), model.ChannelGuestRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.ChannelGuestRoleId]); err != nil { - mlog.Critical("Failed to create new guest role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.TeamGuestRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.TeamGuestRoleId]); err != nil { - mlog.Critical("Failed to create new guest role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.SystemGuestRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.SystemGuestRoleId]); err != nil { - mlog.Critical("Failed to create new guest role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new guest role to database.", mlog.Err(err)) allSucceeded = false } } schemes, err := s.Store().Scheme().GetAllPage("", 0, 1000000) if err != nil { - mlog.Critical("Failed to get all schemes.", mlog.Err(err)) + mlog.Fatal("Failed to get all schemes.", mlog.Err(err)) allSucceeded = false } for _, scheme := range schemes { @@ -201,7 +201,7 @@ func (s *Server) doGuestRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(teamGuestRole); err != nil { - mlog.Critical("Failed to create new guest role for custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultTeamGuestRole = savedRole.Name @@ -217,7 +217,7 @@ func (s *Server) doGuestRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(channelGuestRole); err != nil { - mlog.Critical("Failed to create new guest role for custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new guest role for custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultChannelGuestRole = savedRole.Name @@ -225,7 +225,7 @@ func (s *Server) doGuestRolesCreationMigration() { _, err := s.Store().Scheme().Save(scheme) if err != nil { - mlog.Critical("Failed to update custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to update custom scheme.", mlog.Err(err)) allSucceeded = false } } @@ -241,7 +241,7 @@ func (s *Server) doGuestRolesCreationMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark guest roles creation migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark guest roles creation migration as completed.", mlog.Err(err)) } } @@ -260,19 +260,19 @@ func (s *Server) doSystemConsoleRolesCreationMigration() { allSucceeded := true if _, err := s.Store().Role().GetByName(context.Background(), model.SystemManagerRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.SystemManagerRoleId]); err != nil { - mlog.Critical("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemManagerRoleId)) + mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemManagerRoleId)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.SystemReadOnlyAdminRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.SystemReadOnlyAdminRoleId]); err != nil { - mlog.Critical("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemReadOnlyAdminRoleId)) + mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemReadOnlyAdminRoleId)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.SystemUserManagerRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.SystemUserManagerRoleId]); err != nil { - mlog.Critical("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemUserManagerRoleId)) + mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemUserManagerRoleId)) allSucceeded = false } } @@ -287,7 +287,7 @@ func (s *Server) doSystemConsoleRolesCreationMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark system console roles creation migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark system console roles creation migration as completed.", mlog.Err(err)) } } @@ -302,7 +302,7 @@ func (s *Server) doCustomGroupAdminRoleCreationMigration() { allSucceeded := true if _, err := s.Store().Role().GetByName(context.Background(), model.SystemCustomGroupAdminRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.SystemCustomGroupAdminRoleId]); err != nil { - mlog.Critical("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemCustomGroupAdminRoleId)) + mlog.Fatal("Failed to create new role.", mlog.Err(err), mlog.String("role", model.SystemCustomGroupAdminRoleId)) allSucceeded = false } } @@ -317,7 +317,7 @@ func (s *Server) doCustomGroupAdminRoleCreationMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark custom group admin role creation migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark custom group admin role creation migration as completed.", mlog.Err(err)) } } @@ -337,7 +337,7 @@ func (s *Server) doContentExtractionConfigDefaultTrueMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark content extraction config migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark content extraction config migration as completed.", mlog.Err(err)) } } @@ -352,31 +352,31 @@ func (s *Server) doPlaybooksRolesCreationMigration() { allSucceeded := true if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookAdminRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.PlaybookAdminRoleId]); err != nil { - mlog.Critical("Failed to create new playbook admin role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new playbook admin role to database.", mlog.Err(err)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.PlaybookMemberRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.PlaybookMemberRoleId]); err != nil { - mlog.Critical("Failed to create new playbook member role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new playbook member role to database.", mlog.Err(err)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.RunAdminRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.RunAdminRoleId]); err != nil { - mlog.Critical("Failed to create new run admin role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new run admin role to database.", mlog.Err(err)) allSucceeded = false } } if _, err := s.Store().Role().GetByName(context.Background(), model.RunMemberRoleId); err != nil { if _, err := s.Store().Role().Save(roles[model.RunMemberRoleId]); err != nil { - mlog.Critical("Failed to create new run member role to database.", mlog.Err(err)) + mlog.Fatal("Failed to create new run member role to database.", mlog.Err(err)) allSucceeded = false } } schemes, err := s.Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1000000) if err != nil { - mlog.Critical("Failed to get all schemes.", mlog.Err(err)) + mlog.Fatal("Failed to get all schemes.", mlog.Err(err)) allSucceeded = false } @@ -391,7 +391,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(playbookAdminRole); err != nil { - mlog.Critical("Failed to create new playbook admin role for existing custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new playbook admin role for existing custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultPlaybookAdminRole = savedRole.Name @@ -406,7 +406,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(playbookMember); err != nil { - mlog.Critical("Failed to create new playbook member role for existing custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new playbook member role for existing custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultPlaybookMemberRole = savedRole.Name @@ -422,7 +422,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(runAdminRole); err != nil { - mlog.Critical("Failed to create new run admin role for existing custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new run admin role for existing custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultRunAdminRole = savedRole.Name @@ -438,7 +438,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } if savedRole, err := s.Store().Role().Save(runMemberRole); err != nil { - mlog.Critical("Failed to create new run member role for existing custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to create new run member role for existing custom scheme.", mlog.Err(err)) allSucceeded = false } else { scheme.DefaultRunMemberRole = savedRole.Name @@ -446,7 +446,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } _, err := s.Store().Scheme().Save(scheme) if err != nil { - mlog.Critical("Failed to update custom scheme.", mlog.Err(err)) + mlog.Fatal("Failed to update custom scheme.", mlog.Err(err)) allSucceeded = false } } @@ -462,7 +462,7 @@ func (s *Server) doPlaybooksRolesCreationMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark playbook roles creation migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark playbook roles creation migration as completed.", mlog.Err(err)) } } @@ -507,7 +507,7 @@ func (s *Server) doFirstAdminSetupCompleteMigration() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark first admin setup migration as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark first admin setup migration as completed.", mlog.Err(err)) } } @@ -534,7 +534,7 @@ func (s *Server) doRemainingSchemaMigrations() { } if err := s.Store().System().Save(&system); err != nil { - mlog.Critical("Failed to mark the remaining schema migrations as completed.", mlog.Err(err)) + mlog.Fatal("Failed to mark the remaining schema migrations as completed.", mlog.Err(err)) } } @@ -552,7 +552,7 @@ func (s *Server) doAppMigrations() { // migrations. For example, it needs the guest roles migration. err := s.doPermissionsMigrations() if err != nil { - mlog.Critical("(app.App).DoPermissionsMigrations failed", mlog.Err(err)) + mlog.Fatal("(app.App).DoPermissionsMigrations failed", mlog.Err(err)) } s.doContentExtractionConfigDefaultTrueMigration() s.doPlaybooksRolesCreationMigration() diff --git a/app/notification.go b/app/notification.go index cd275134d4..215945405c 100644 --- a/app/notification.go +++ b/app/notification.go @@ -43,7 +43,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea return []string{}, nil } - isCRTAllowed := a.Config().FeatureFlags.CollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled + isCRTAllowed := *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled pchan := make(chan store.StoreResult, 1) go func() { diff --git a/app/notification_test.go b/app/notification_test.go index 1563be191d..f00c038dac 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -5,7 +5,6 @@ package app import ( "fmt" - "os" "testing" "github.com/stretchr/testify/assert" @@ -2734,8 +2733,7 @@ func TestReplyPostNotificationsWithCRT(t *testing.T) { }() // Enable CRT - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn diff --git a/app/permissions_migrations.go b/app/permissions_migrations.go index 34661b5d11..3bd85521fe 100644 --- a/app/permissions_migrations.go +++ b/app/permissions_migrations.go @@ -994,6 +994,30 @@ func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) { return t, nil } +func (a *App) getAddCustomUserGroupsPermissionRestore() (permissionsMap, error) { + t := []permissionTransformation{} + + customGroupPermissions := []string{ + model.PermissionRestoreCustomGroup.Id, + } + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemUserRoleId), + Add: customGroupPermissions, + }) + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemAdminRoleId), + Add: customGroupPermissions, + }) + + t = append(t, permissionTransformation{ + On: isExactRole(model.SystemCustomGroupAdminRoleId), + Add: customGroupPermissions, + }) + return t, nil +} + func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) { transformations := []permissionTransformation{} @@ -1110,6 +1134,7 @@ func (s *Server) doPermissionsMigrations() error { {Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions}, {Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles}, {Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions}, + {Key: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Migration: a.getAddCustomUserGroupsPermissionRestore}, } roles, err := s.Store().Role().GetAll() diff --git a/app/platform/metrics.go b/app/platform/metrics.go index 2253594011..f30edb5093 100644 --- a/app/platform/metrics.go +++ b/app/platform/metrics.go @@ -111,7 +111,7 @@ func (pm *platformMetrics) startMetricsServer() error { go func() { close(notify) if err := pm.server.Serve(l); err != nil && err != http.ErrServerClosed { - pm.logger.Critical(err.Error()) + pm.logger.Fatal(err.Error()) } }() diff --git a/app/plugin_install.go b/app/plugin_install.go index 431c3abebe..b35982b82c 100644 --- a/app/plugin_install.go +++ b/app/plugin_install.go @@ -402,6 +402,8 @@ func (ch *Channels) installExtractedPlugin(manifest *model.Manifest, fromPluginD manifest = updatedManifest } + mlog.Debug("Installing plugin", mlog.String("plugin_id", manifest.Id), mlog.String("version", manifest.Version)) + return manifest, nil } diff --git a/app/post_test.go b/app/post_test.go index 0b9c37a3f8..4630eb75af 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -2357,8 +2357,6 @@ func TestThreadMembership(t *testing.T) { func TestFollowThreadSkipsParticipants(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true @@ -2410,13 +2408,16 @@ func TestFollowThreadSkipsParticipants(t *testing.T) { for _, p := range thread.Participants { require.True(t, p.Id == sysadmin.Id || p.Id == user.Id) } + + threadMembership.PostId = "notfound" + _, err = th.App.GetThreadForUser(threadMembership, false) + require.NotNil(t, err) + assert.Equal(t, http.StatusNotFound, err.StatusCode) } func TestAutofollowBasedOnRootPost(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true @@ -2445,8 +2446,6 @@ func TestAutofollowBasedOnRootPost(t *testing.T) { func TestViewChannelShouldNotUpdateThreads(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true @@ -2528,12 +2527,6 @@ func TestCollapsedThreadFetch(t *testing.T) { }) t.Run("Should not panic on unexpected db error", func(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") - th.App.UpdateConfig(func(cfg *model.Config) { - cfg.FeatureFlags.CollapsedThreads = true - }) - channel := th.CreateChannel(th.Context, th.BasicTeam) th.AddUserToChannel(user2, channel) defer th.App.DeleteChannel(th.Context, channel, user1.Id) @@ -2765,8 +2758,6 @@ func TestSharedChannelSyncForPostActions(t *testing.T) { func TestAutofollowOnPostingAfterUnfollow(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true @@ -2834,8 +2825,6 @@ func TestGetPostIfAuthorized(t *testing.T) { func TestShouldNotRefollowOnOthersReply(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true diff --git a/app/server.go b/app/server.go index fbd433a6db..dc66353ee4 100644 --- a/app/server.go +++ b/app/server.go @@ -1059,7 +1059,7 @@ func (s *Server) Start() error { } if err != nil && err != http.ErrServerClosed { - mlog.Critical("Error starting server", mlog.Err(err)) + mlog.Fatal("Error starting server", mlog.Err(err)) time.Sleep(time.Second) } @@ -1068,7 +1068,7 @@ func (s *Server) Start() error { if *s.platform.Config().ServiceSettings.EnableLocalMode { if err := s.startLocalModeServer(); err != nil { - mlog.Critical(err.Error()) + mlog.Fatal(err.Error()) } } @@ -1100,7 +1100,7 @@ func (s *Server) startLocalModeServer() error { go func() { err = s.localModeServer.Serve(unixListener) if err != nil && err != http.ErrServerClosed { - mlog.Critical("Error starting unix socket server", mlog.Err(err)) + mlog.Fatal("Error starting unix socket server", mlog.Err(err)) } }() return nil diff --git a/app/upload.go b/app/upload.go index ef4bff2b71..318e3ede89 100644 --- a/app/upload.go +++ b/app/upload.go @@ -93,6 +93,7 @@ func (a *App) runPluginsHook(c request.CTX, info *model.FileInfo, file io.Reader if fileErr := a.RemoveFile(tmpPath); fileErr != nil { mlog.Warn("Failed to remove file", mlog.Err(fileErr)) } + r.CloseWithError(err) // always returns nil return err } diff --git a/app/user.go b/app/user.go index 6e12bda34b..991457f7ba 100644 --- a/app/user.go +++ b/app/user.go @@ -316,7 +316,11 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m }, plugin.UserHasBeenCreatedID) }) - // Create/Update the subscriptionHistoryEvent + // For cloud yearly subscriptions, if the current user count of the workspace exceeds the number of seats initially purchased + // (plus the “threshold” of 10%), then a subscriptionHistoryEvent object would need to be created and added to the subscriptionHistory + // table in CWS. This is then used to calculate how much the customers have to pay in addition for the extra users. If the + // workspace is currently on a monthly plan, then this function will not do anything. + go func() { _, err := a.SendSubscriptionHistoryEvent(ruser.Id) if err != nil { @@ -2480,24 +2484,31 @@ func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThre } func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.ThreadMembership, *model.AppError) { - threadMembership, err := a.Srv().Store().Thread().GetMembershipForUser(userId, threadId) - if err != nil { - return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - if threadMembership == nil { - return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.not_found", nil, "thread membership not found/followed", http.StatusNotFound) + threadMembership, nErr := a.Srv().Store().Thread().GetMembershipForUser(userId, threadId) + if nErr != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(nErr, &nfErr): + return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.not_found", nil, "", http.StatusNotFound).Wrap(nErr) + default: + return nil, model.NewAppError("GetThreadMembershipForUser", "app.user.get_thread_membership_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } } return threadMembership, nil } func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) { - thread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled()) - if err != nil { - return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - if thread == nil { - return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound) + thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled()) + if nErr != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(nErr, &nfErr): + return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound) + default: + return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } } + a.sanitizeProfiles(thread.Participants, false) thread.Post.SanitizeProps() return thread, nil diff --git a/app/user_test.go b/app/user_test.go index 788ae11275..086e71e25f 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -8,7 +8,7 @@ import ( "context" "encoding/json" "errors" - "os" + "net/http" "path/filepath" "strings" "testing" @@ -1673,8 +1673,6 @@ func TestPatchUser(t *testing.T) { } func TestUpdateThreadReadForUser(t *testing.T) { - os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true") - defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS") t.Run("Ensure thread membership is created and followed", func(t *testing.T) { th := Setup(t).InitBasic() @@ -1703,6 +1701,10 @@ func TestUpdateThreadReadForUser(t *testing.T) { require.Nil(t, appErr) require.NotNil(t, threadMembership) assert.True(t, threadMembership.Following) + + _, appErr = th.App.GetThreadMembershipForUser(th.BasicUser.Id, "notfound") + require.NotNil(t, appErr) + assert.Equal(t, http.StatusNotFound, appErr.StatusCode) }) t.Run("Ensure no panic on error", func(t *testing.T) { diff --git a/build/Dockerfile b/build/Dockerfile index d373e54fdc..aaa82b99ca 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV PATH="/mattermost/bin:${PATH}" ARG PUID=2000 ARG PGID=2000 -ARG MM_PACKAGE="https://releases.mattermost.com/7.5.1/mattermost-7.5.1-linux-amd64.tar.gz?src=docker" +ARG MM_PACKAGE="https://releases.mattermost.com/7.5.2/mattermost-7.5.2-linux-amd64.tar.gz?src=docker" # # Install needed packages and indirect dependencies RUN apt-get update \ diff --git a/cmd/mattermost/commands/db.go b/cmd/mattermost/commands/db.go index db33032297..c0d186c265 100644 --- a/cmd/mattermost/commands/db.go +++ b/cmd/mattermost/commands/db.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/store/sqlstore" @@ -100,7 +101,7 @@ func initDbCmdF(command *cobra.Command, _ []string) error { } func resetCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization()) if err != nil { return err } diff --git a/cmd/mattermost/commands/export.go b/cmd/mattermost/commands/export.go index fa0f251e1b..d5573ee975 100644 --- a/cmd/mattermost/commands/export.go +++ b/cmd/mattermost/commands/export.go @@ -10,6 +10,7 @@ import ( "path/filepath" "time" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/model" @@ -93,7 +94,7 @@ func init() { } func scheduleExportCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization()) if err != nil { return err } @@ -153,7 +154,7 @@ func scheduleExportCmdF(command *cobra.Command, args []string) error { func buildExportCmdF(format string) func(command *cobra.Command, args []string) error { return func(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization()) license := a.Srv().License() if err != nil { return err @@ -201,7 +202,7 @@ func buildExportCmdF(format string) func(command *cobra.Command, args []string) } func bulkExportCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.SkipPostInitialization()) if err != nil { return err } diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go index e93d9640fe..1c7e8baf07 100644 --- a/cmd/mattermost/commands/init.go +++ b/cmd/mattermost/commands/init.go @@ -14,8 +14,8 @@ import ( "github.com/mattermost/mattermost-server/v6/utils" ) -func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool) (*app.App, error) { - a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), readOnlyConfigStore) +func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) { + a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), readOnlyConfigStore, options...) if err != nil { // Returning an error just prints the usage message, so actually panic panic(err) @@ -27,25 +27,19 @@ func initDBCommandContextCobra(command *cobra.Command, readOnlyConfigStore bool) return a, nil } -func InitDBCommandContextCobra(command *cobra.Command) (*app.App, error) { - return initDBCommandContextCobra(command, true) +func InitDBCommandContextCobra(command *cobra.Command, options ...app.Option) (*app.App, error) { + return initDBCommandContextCobra(command, true, options...) } -func InitDBCommandContextCobraReadWrite(command *cobra.Command) (*app.App, error) { - return initDBCommandContextCobra(command, false) -} - -func initDBCommandContext(configDSN string, readOnlyConfigStore bool) (*app.App, error) { +func initDBCommandContext(configDSN string, readOnlyConfigStore bool, options ...app.Option) (*app.App, error) { if err := utils.TranslationsPreInit(); err != nil { return nil, err } model.AppErrorInit(i18n.T) - s, err := app.NewServer( - // The option order is important as app.Config option reads app.StartMetrics option. - app.StartMetrics, - app.Config(configDSN, readOnlyConfigStore, nil), - ) + // The option order is important as app.Config option reads app.StartMetrics option. + options = append(options, app.Config(configDSN, readOnlyConfigStore, nil)) + s, err := app.NewServer(options...) if err != nil { return nil, err } diff --git a/cmd/mattermost/commands/jobserver.go b/cmd/mattermost/commands/jobserver.go index 488b3f8fe7..be59971aab 100644 --- a/cmd/mattermost/commands/jobserver.go +++ b/cmd/mattermost/commands/jobserver.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/audit" "github.com/mattermost/mattermost-server/v6/config" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -34,7 +35,7 @@ func jobserverCmdF(command *cobra.Command, args []string) error { noSchedule, _ := command.Flags().GetBool("noschedule") // Initialize - a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), false) + a, err := initDBCommandContext(getConfigDSN(command, config.GetEnvironment()), false, app.StartMetrics) if err != nil { return err } diff --git a/cmd/mattermost/commands/test.go b/cmd/mattermost/commands/test.go index 75c49a4ee7..1a50d53221 100644 --- a/cmd/mattermost/commands/test.go +++ b/cmd/mattermost/commands/test.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/mattermost/mattermost-server/v6/api4" + "github.com/mattermost/mattermost-server/v6/app" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/i18n" "github.com/mattermost/mattermost-server/v6/wsapi" @@ -46,7 +47,7 @@ func init() { } func webClientTestsCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.StartMetrics) if err != nil { return err } @@ -70,7 +71,7 @@ func webClientTestsCmdF(command *cobra.Command, args []string) error { } func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error { - a, err := InitDBCommandContextCobra(command) + a, err := InitDBCommandContextCobra(command, app.StartMetrics) if err != nil { return err } diff --git a/config/client.go b/config/client.go index b8160993fd..e26c2bea40 100644 --- a/config/client.go +++ b/config/client.go @@ -92,6 +92,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations) + props["CWSURL"] = *c.CloudSettings.CWSURL + // Set default values for all options that require a license. props["ExperimentalEnableAuthenticationTransfer"] = "true" props["LdapNicknameAttributeSet"] = "false" @@ -123,7 +125,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DataRetentionFileRetentionDays"] = "0" props["DataRetentionEnableBoardsDeletion"] = "false" props["DataRetentionBoardsRetentionDays"] = "0" - props["CWSURL"] = "" props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",") props["IsDefaultMarketplace"] = strconv.FormatBool(*c.PluginSettings.MarketplaceURL == model.PluginSettingsDefaultMarketplaceURL) @@ -195,10 +196,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["DataRetentionBoardsRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.BoardsRetentionDays), 10) } - if license.IsCloud() { - props["CWSURL"] = *c.CloudSettings.CWSURL - } - if *license.Features.SharedChannels { props["ExperimentalSharedChannels"] = strconv.FormatBool(*c.ExperimentalSettings.EnableSharedChannels) props["ExperimentalRemoteClusterService"] = strconv.FormatBool(c.FeatureFlags.EnableRemoteClusterService && *c.ExperimentalSettings.EnableRemoteClusterService) diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index d58e8c92dd..0c70c02cf3 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -17,6 +17,7 @@ type CloudInterface interface { ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error GetCloudCustomer(userID string) (*model.CloudCustomer, error) + GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error) diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 4b89135dc0..a447399cd8 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -325,6 +325,29 @@ func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.In return r0, r1 } +// GetLicenseExpandStatus provides a mock function with given fields: userID, token +func (_m *CloudInterface) GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) { + ret := _m.Called(userID, token) + + var r0 *model.SubscriptionExpandStatus + if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionExpandStatus); ok { + r0 = rf(userID, token) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.SubscriptionExpandStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(userID, token) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetLicenseRenewalStatus provides a mock function with given fields: userID, token func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) error { ret := _m.Called(userID, token) diff --git a/go.mod b/go.mod index c157155e51..2e04b4e93c 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 github.com/lib/pq v1.10.7 + github.com/mattermost/focalboard/server v0.0.0-20221222174020-fd4cf95f8ac9 github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 github.com/mattermost/gziphandler v0.0.1 github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d @@ -85,6 +86,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bits-and-blooms/bitset v1.3.3 // indirect github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/blevesearch/bleve_index_api v1.0.5 // indirect github.com/blevesearch/geo v0.1.15 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect @@ -131,10 +133,10 @@ require ( github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5 // indirect + github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -179,7 +181,7 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect - lukechampine.com/uint128 v1.1.1 // indirect + lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.36.0 // indirect modernc.org/ccgo/v3 v3.16.6 // indirect modernc.org/libc v1.16.7 // indirect diff --git a/go.sum b/go.sum index 5bfeee94ff..196d356a53 100644 --- a/go.sum +++ b/go.sum @@ -216,6 +216,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/bleve/v2 v2.3.6-0.20221111171245-56dc9b25507e h1:r/cWPLUPgAM3SWWniQ6j0Hzb++h+uEycDD9UOUuC1Vk= github.com/blevesearch/bleve/v2 v2.3.6-0.20221111171245-56dc9b25507e/go.mod h1:mfCWvuwg/XnPVZHEejATm5TyFqyeLmm8p9Y3xDvwz4k= github.com/blevesearch/bleve_index_api v1.0.3/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= @@ -979,6 +981,8 @@ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsI github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattermost/focalboard/server v0.0.0-20221222174020-fd4cf95f8ac9 h1:UE3KuILWJwTnaXAy2YHqkCu1f7i+VdX2VNukBGYbPuI= +github.com/mattermost/focalboard/server v0.0.0-20221222174020-fd4cf95f8ac9/go.mod h1:h1HQ8UVoNMyDHzjPD7UtYbTPMWjP6d1qJZuLdT6ElNg= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/gziphandler v0.0.1 h1:uXHcXF5agnQ6bXabvpiwwwZOlCYoa7mKHH0lxns/o8w= @@ -987,6 +991,8 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= github.com/mattermost/logr/v2 v2.0.15 h1:+WNbGcsc3dBao65eXlceB6dTILNJRIrvubnsTl3zBew= github.com/mattermost/logr/v2 v2.0.15/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg= +github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb h1:q1qXKVv59rA2gcQ7lVLc5OlWBmfsR3i8mdGD5EZesyk= +github.com/mattermost/mattermost-plugin-api v0.0.29-0.20220801143717-73008cfda2fb/go.mod h1:PIeo40t9VTA4Wu1FwjzH7QmcgC3SRyk/ohCwJw4/oSo= github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e h1:VfNz+fvJ3DxOlALM22Eea8ONp5jHrybKBCcCtDPVlss= github.com/mattermost/morph v1.0.5-0.20221115094356-4c18a75b1f5e/go.mod h1:xo0ljDknTpPxEdhhrUdwhLCexIsYyDKS6b41HqG8wGU= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o= @@ -1028,7 +1034,6 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= @@ -2282,8 +2287,9 @@ k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= diff --git a/i18n/bg.json b/i18n/bg.json index 1e2ac3b534..f9fa12c015 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -1887,14 +1887,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Изтече времето на AD/LDAP задачата за синхронизация." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не може да се анализира заданието за износ на съобщения ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не може да се анализира заданието за износ на съобщения BatchSize." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Лицензът ви не поддържа ID заредени изскачащи известия." diff --git a/i18n/de.json b/i18n/de.json index d2ebc4bbd5..82a8cccb82 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -6355,14 +6355,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP-Synchronisierungs-Job-Timeout erreicht." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Konnte ExportFromTimestamp des Nachrichten-Export-Jobs nicht verarbeiten." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Konnte BatchSize des Nachrichten-Export-Jobs nicht verarbeiten." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster-API-Endpunkt nicht gefunden." @@ -9729,5 +9721,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Fehler beim Abrufen von Rollen während der Validierung." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Das Portal ist für selbst gehostete Anmeldungen nicht verfügbar." } ] diff --git a/i18n/en.json b/i18n/en.json index 25f8a93b61..4cb1c9c634 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -7571,14 +7571,6 @@ "id": "ent.id_loaded.license_disable.app_error", "translation": "Your license does not support ID Loaded Push Notifications." }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, { "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronization job timeout." diff --git a/i18n/en_AU.json b/i18n/en_AU.json index 3283b75397..e03b05824f 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -3251,14 +3251,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronisation job timeout." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Your licence does not support ID Loaded Push Notifications." @@ -9693,5 +9685,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Error fetching roles during validation." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal unavailable for self-hosted signup." } ] diff --git a/i18n/es.json b/i18n/es.json index 68dd0230be..4de9de9d37 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -6571,14 +6571,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "El tiempo de espera del trabajo de sincronización AD/LDAP fue alcanzado." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "No se pudo interpretar la fecha y hora de inicio de la exportación de mensajes." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "No se pudo interpretar el tamaño de bloque de datos de la exportación de mensajes." - }, { "id": "ent.cluster.404.app_error", "translation": "No se encontró el endpoint del API para el agrupamiento de servidores." diff --git a/i18n/fa.json b/i18n/fa.json index cdee577b6c..f348528ffb 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -1919,14 +1919,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "مهلت زمانی هماهنگ سازی AD/LDAP رسیده است." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "تجزیه و تحلیل کار صادرات پیام ExportFromTimestamp امکان پذیر نیست." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "تجزیه و تحلیل کار صادرات پیام صادرات از مهر زمان انجام نمی شود.." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "مجوز شما از ID Loaded Push Notifications پشتیبانی نمی کند." diff --git a/i18n/fr.json b/i18n/fr.json index 107121d484..356a12a5e3 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -6343,14 +6343,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Temps d'attente pour la tâche de synchronisation AD/LDAP atteint." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Impossible d'interpréter le paramètre ExportFromTimestamp de la tâche d'exportation de messages." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Impossible d'interpréter le paramètre BatchSize de la tâche d'exportation de messages." - }, { "id": "ent.cluster.404.app_error", "translation": "Le nœud d'API cluster est introuvable." diff --git a/i18n/hu.json b/i18n/hu.json index 3ddcd9f7b7..c010bb1704 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -3291,14 +3291,6 @@ "id": "model.upload_session.is_valid.id.app_error", "translation": "Érvénytelen érték a Id -nak" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Nem sikerült feldolgozni az üzenet export munka ExportFromTimestamp értékét." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Nem sikerült feldolgozni az üzenet export BatchSize értékét." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Az Ön licensze nem támogatja az azonosítóval betöltött push értesítéseket." diff --git a/i18n/it.json b/i18n/it.json index 762c7d9bcc..14f201c891 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -6587,14 +6587,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Raggiunto timeout nel lavoro di sincronizzazione AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Impossibile analizzare il messaggio del lavoro di esportazione ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Impossibile analizzare il messaggio del lavoro di esportazione BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API non trovate." diff --git a/i18n/ja.json b/i18n/ja.json index 4df39843dd..7625ed5305 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -289,7 +289,7 @@ }, { "id": "api.channel.update_channel_member_roles.scheme_role.app_error", - "translation": "与えられた役割はスキームによって管理されているため、チャンネルメンバーへ直接適用することはできません。" + "translation": "与えられたロールはスキームによって管理されているため、チャンネルメンバーへ直接適用することはできません。" }, { "id": "api.channel.update_channel_scheme.license.error", @@ -301,7 +301,7 @@ }, { "id": "api.channel.update_team_member_roles.scheme_role.app_error", - "translation": "与えられた役割はスキームによって管理されているため、チームメンバーへ直接適用することはできません。" + "translation": "与えられたロールはスキームによって管理されているため、チームメンバーへ直接適用することはできません。" }, { "id": "api.command.admin_only.app_error", @@ -2657,19 +2657,19 @@ }, { "id": "app.import.validate_role_import_data.description_invalid.error", - "translation": "役割の説明が不正です。" + "translation": "ロールの説明が不正です。" }, { "id": "app.import.validate_role_import_data.display_name_invalid.error", - "translation": "役割の表示名が不正です。" + "translation": "ロールの表示名が不正です。" }, { "id": "app.import.validate_role_import_data.invalid_permission.error", - "translation": "権限もしくは役割が不正です。" + "translation": "ロールもしくは役割が不正です。" }, { "id": "app.import.validate_role_import_data.name_invalid.error", - "translation": "役割の名前が不正です。" + "translation": "ロールの名前が不正です。" }, { "id": "app.import.validate_scheme_import_data.description_invalid.error", @@ -2693,7 +2693,7 @@ }, { "id": "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", - "translation": "このスコープのスキームに誤った役割が与えられました。" + "translation": "このスコープのスキームに誤ったロールが与えられました。" }, { "id": "app.import.validate_team_import_data.description_length.error", @@ -2753,7 +2753,7 @@ }, { "id": "app.import.validate_user_channels_import_data.invalid_roles.error", - "translation": "ユーザーのチャネルメンバーシップの役割が不正です。" + "translation": "ユーザーのチャネルメンバーシップのロールが不正です。" }, { "id": "app.import.validate_user_import_data.auth_data_and_password.error", @@ -2825,7 +2825,7 @@ }, { "id": "app.import.validate_user_import_data.roles_invalid.error", - "translation": "ユーザーの役割が正しくありません。" + "translation": "ユーザーのロールが正しくありません。" }, { "id": "app.import.validate_user_import_data.username_invalid.error", @@ -2837,7 +2837,7 @@ }, { "id": "app.import.validate_user_teams_import_data.invalid_roles.error", - "translation": "ユーザーのチームメンバーシップの役割が不正です。" + "translation": "ユーザーのチームメンバーシップのロールが不正です。" }, { "id": "app.import.validate_user_teams_import_data.team_name_missing.error", @@ -2933,7 +2933,7 @@ }, { "id": "app.role.check_roles_exist.role_not_found", - "translation": "指定された役割は存在しません" + "translation": "指定されたロールは存在しません" }, { "id": "app.save_config.app_error", @@ -5709,7 +5709,7 @@ }, { "id": "api.channel.update_team_member_roles.changing_guest_role.app_error", - "translation": "不正なチームメンバ更新: 手動でゲストの役割を追加/削除することはできません。" + "translation": "不正なチームメンバ更新: 手動でゲストのロールを追加/削除することはできません。" }, { "id": "api.channel.update_channel_privacy.default_channel_error", @@ -5721,7 +5721,7 @@ }, { "id": "api.channel.update_channel_member_roles.changing_guest_role.app_error", - "translation": "不正なチャンネルメンバー更新: 手動でゲストの役割を追加/削除することはできません。" + "translation": "不正なチャンネルメンバー更新: 手動でゲストのロールを追加/削除することはできません。" }, { "id": "api.channel.update_channel.typechange.app_error", @@ -6437,27 +6437,27 @@ }, { "id": "app.role.save.invalid_role.app_error", - "translation": "役割が不正です。" + "translation": "ロールが不正です。" }, { "id": "app.role.save.insert.app_error", - "translation": "新しい役割を保存できませんでした。" + "translation": "新しいロールを保存できませんでした。" }, { "id": "app.role.permanent_delete_all.app_error", - "translation": "すべての役割を完全に削除できませんでした。" + "translation": "すべてのロールを完全に削除できませんでした。" }, { "id": "app.role.get_by_names.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "app.role.get_by_name.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "app.role.get.app_error", - "translation": "役割を取得できませんでした。" + "translation": "ロールを取得できませんでした。" }, { "id": "model.config.is_valid.directory.app_error", @@ -6563,14 +6563,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP同期処理がタイムアウトしました。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "メッセージエクスポートジョブのExportFromTimestampを解析できませんでした。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "メッセージエクスポートジョブのBatchSizeを解析できませんでした。" - }, { "id": "ent.cluster.404.app_error", "translation": "クラスターAPIエンドポイントが見つかりませんでした。" @@ -7181,7 +7173,7 @@ }, { "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", - "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどの役割の人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\nトライアル開始 をクリックすると、[Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/) と [プライバシーポリシー](https://mattermost.com/privacy-policy/)に同意したことになり、製品に関する電子メールを受信するようになります。" + "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどのロールの人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\nトライアル開始 をクリックすると、[Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/) と [プライバシーポリシー](https://mattermost.com/privacy-policy/)に同意したことになり、製品に関する電子メールを受信するようになります。" }, { "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", @@ -7189,7 +7181,7 @@ }, { "id": "api.server.warn_metric.number_of_channels_50.notification_body", - "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどの役割の人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\n問い合わせ をクリックすると、あなたの情報が Mattermost, Inc. へ共有されます。詳しくは[説明文書](https://mattermost.com/pl/default-admin-advisory)を参照してください" + "translation": "チャンネルはコミュニケーションの改善をサポートするものですが、Mattermost全体でチャンネルの作成や参加が多くなるにつれ、システムを整理されたものにし続けることが課題になってきます。高度な権限設定により、どのユーザー、もしくはどのロールの人が何のアクションを実行可能かを設定することができます。例えば、チャンネル設定やメンバーの管理や、@channel、@hereなどのタグによるグループへの発信、新たなウェブフックの作成などを制限できます。\n\n詳しくは[高度な権限設定の利用に関する説明](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)を参照してください\n\n問い合わせ をクリックすると、あなたの情報が Mattermost, Inc. へ共有されます。詳しくは[説明文書](https://mattermost.com/pl/default-admin-advisory)を参照してください" }, { "id": "api.server.warn_metric.number_of_channels_50.notification_title", @@ -8885,11 +8877,11 @@ }, { "id": "model.user.is_valid.roles_limit.app_error", - "translation": "{{.Limit}}文字以上の不正なユーザーの役割です。" + "translation": "{{.Limit}}文字以上の不正なユーザーのロールです。" }, { "id": "model.team_member.is_valid.roles_limit.app_error", - "translation": "{{.Limit}} 文字より長い不正なチームメンバーの役割です。" + "translation": "{{.Limit}} 文字より長い不正なチームメンバーのロールです。" }, { "id": "model.session.is_valid.user_id.app_error", @@ -8897,7 +8889,7 @@ }, { "id": "model.channel_member.is_valid.roles_limit.app_error", - "translation": "{{.Limit}} 文字より長い不正なチャンネルメンバーの役割です。" + "translation": "{{.Limit}} 文字より長い不正なチャンネルメンバーのロールです。" }, { "id": "model.session.is_valid.roles_limit.app_error", @@ -8937,7 +8929,7 @@ }, { "id": "app.role.get_all.app_error", - "translation": "全ての役割を取得できませんでした。" + "translation": "全てのロールを取得できませんでした。" }, { "id": "api.user.view_archived_channels.get_users_in_channel.app_error", @@ -9674,5 +9666,57 @@ { "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "アーカイブされたチャンネルでは、確認応答を削除することはできません。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "開発チーム間で透明性の高いワークフローを作成し、機能開発プロセスをシームレスにすることができます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Jira BotやGitHub Botと統合し、生産性を高めましょう。これらはあなたのためにダウンロードされます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Boards、Playbooks、Botと簡単に接続できる Feature Release チャンネルでチームとチャットできます。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "スタンドアップなどの定期的なミーティングには Meeting Agenda ボードテンプレート、タスクの進捗管理には Project Task ボードをご利用ください。" + }, + { + "id": "worktemplate.category.product_teams", + "translation": "製品チーム" + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "不正な優先度" + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "作業テンプレートを取得できませんでした" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "作業テンプレートのカテゴリを取得できませんでした" + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "検証中のロール取得時にエラーが発生しました。" + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "請求書を見る" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "ワークスペース {{.WorkspaceName}} がアップグレードされました。" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "ワークスペース {{.WorkspaceName}} がアップグレードされました。{{.Date}} から課金されます" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "セルフホスティングの利用登録では、ポータルは利用できません。" } ] diff --git a/i18n/ko.json b/i18n/ko.json index 82c9a20ffd..ad4733a1aa 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -6519,14 +6519,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Reached AD/LDAP synchronization job timeout." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Could not parse message export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Could not parse message export job BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API endpoint not found." @@ -7929,7 +7921,7 @@ }, { "id": "api.cloud.delinquency_email.missing_email_to_trigger", - "translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다." + "translation": "연체 알림 전자우편을 보내기 위한 필수 항목이 누락되었습니다." }, { "id": "api.error_set_first_admin_complete_setup", @@ -7958,5 +7950,89 @@ { "id": "sharedchannel.cannot_deliver_post", "translation": "{{.Remote}} 원격 사이트가 오프라인이기 때문에 하나 또는 그 이상의 포스트가 전송되지 않았습니다. 포스트는 해당 사이트가 온라인일 때 전송될 것입니다." + }, + { + "id": "app.notification.body.thread.title", + "translation": "{{.SenderName}}님이 글타래에 답장을 남겼습니다" + }, + { + "id": "app.notification.body.thread_channel.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 지켜보는 중인 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_channel_full.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 {{.ChannelName}} 채널에 있는 지켜보는 중인 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_gm.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 그룹 글타래에 답장을 남겼습니다." + }, + { + "id": "app.notification.body.thread_dm.subTitle", + "translation": "자리를 비운 동안, {{.SenderName}}님이 당신이 직접 보낸 메시지에 답장을 남겼습니다." + }, + { + "id": "app.channel.autofollow.app_error", + "translation": "언급된 사용자의 글타래 권한을 갱신하지 못했습니다" + }, + { + "id": "api.getThreadsForUser.bad_params", + "translation": "getThreadsForUser의 Before와 After 매개변수는 상호 배타적입니다" + }, + { + "id": "api.getThreadsForUser.bad_only_params", + "translation": "getThreadsForUser의 OnlyThreads와 OnlyTotals 매개변수는 상호 배타적입니다" + }, + { + "id": "app.post.get_top_threads_for_team_since.app_error", + "translation": "팀의 상위 글타래를 가져올 수 없습니다." + }, + { + "id": "app.post.get_top_threads_for_user_since.app_error", + "translation": "사용자의 상위 글타래를 가져올 수 없습니다." + }, + { + "id": "app.user.get_thread_count_for_user.app_error", + "translation": "사용자의 글타래 개수를 가져올 수 없습니다." + }, + { + "id": "app.user.get_thread_membership_for_user.app_error", + "translation": "사용자 글타래 권한을 가져올 수 없습니다" + }, + { + "id": "app.user.get_thread_membership_for_user.not_found", + "translation": "사용자 글타래 권한이 없습니다" + }, + { + "id": "app.user.get_threads_for_user.app_error", + "translation": "사용자 글타래들을 가져올 수 없습니다" + }, + { + "id": "app.user.get_threads_for_user.not_found", + "translation": "사용자 글타래가 존재하지 않거나 지켜보고 있지 않습니다" + }, + { + "id": "app.user.update_thread_follow_for_user.app_error", + "translation": "글타래의 지켜보기 상태를 갱신할 수 없습니다" + }, + { + "id": "app.user.update_thread_read_for_user.app_error", + "translation": "글타래의 읽음 상태를 갱신할 수 없습니다" + }, + { + "id": "app.user.update_thread_read_for_user_by_post.app_error", + "translation": "유효하지 않은 post_id" + }, + { + "id": "model.config.is_valid.collapsed_threads.app_error", + "translation": "CollapsedThreads 설정은 disabled, default_on 혹은 default_off 중 하나여야만 합니다" + }, + { + "id": "model.config.is_valid.collapsed_threads.autofollow.app_error", + "translation": "CollapsedThreads 기능을 활성화하려면 ThreadAutoFollow 기능이 활성화되어야 합니다" + }, + { + "id": "app.user.update_threads_read_for_user.app_error", + "translation": "모든 사용자 글타래들을 읽음 상태로 변경할 수 없습니다" } ] diff --git a/i18n/nl.json b/i18n/nl.json index 683aa0f875..45c0f6813e 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -4769,7 +4769,7 @@ }, { "id": "web.error.unsupported_browser.min_os_version.mac", - "translation": "macOS 10.14+" + "translation": "macOS 11+" }, { "id": "web.error.unsupported_browser.min_browser_version.safari", @@ -6603,14 +6603,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Time-out voor synchronisatie van AD/LDAP werd bereikt." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Fout bij het verwerken van bericht export taak ExportFromTimeStamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Fout bij ontleden van bericht exporttaak BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Cluster API endpoint niet gevonden." @@ -9264,7 +9256,7 @@ }, { "id": "api.templates.delinquency_14.subject", - "translation": "Betaling is overtijd voor jouw Mattermost {{.Plan}}." + "translation": "Betaling is laattijdig voor jouw Mattermost {{.Plan}}" }, { "id": "api.templates.delinquency_14.button", @@ -9300,7 +9292,7 @@ }, { "id": "api.templates.delinquency_7.subtitle1", - "translation": "We konden jouw laatste betaling niet verwerken" + "translation": "We konden jouw laatste betaling niet verwerken." }, { "id": "api.templates.delinquency_7.button", @@ -9348,7 +9340,7 @@ }, { "id": "api.templates.delinquency_45.subtitle1", - "translation": "We hebben geen betaling kunnen innen voor openstaande facturen van {{.DelinquencyDate}}. Jouw werkruimte loopt het risico om gedowngraded te worden." + "translation": "We hebben geen betaling kunnen innen voor openstaande facturen vanaf {{.DelinquencyDate}}. Jouw werkruimte loopt het risico om gedowngraded te worden." }, { "id": "api.templates.delinquency_45.subject", @@ -9364,7 +9356,7 @@ }, { "id": "api.templates.delinquency_30.subtitle2", - "translation": "als geen actie wordt ondernomen, zal jouw werkruimte worden gedowngraded en kunnen de volgende gegevens worden gearchiveerd:" + "translation": "Als er geen actie wordt ondernomen, zal jouw werkruimte worden gedowngraded en kunnen de volgende gegevens worden gearchiveerd:" }, { "id": "api.templates.delinquency_30.subtitle1", @@ -9561,5 +9553,177 @@ { "id": "api.admin.syncables_error", "translation": "kon gebruiker niet toevoegen aan groep-teams en groep-kanalen" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "Je kan niet bevestigen in een gearchiveerd kanaal." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "Je kan een bevestiging niet wissen nadat 5min verstreken zijn." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "Je kan een bevestiging in een gearchiveerd kanaal niet verwijderen." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Creëer transparante workflows tussen ontwikkelingsteams om ervoor te zorgen dat jouw ontwikkelingsproces naadloos verloopt." + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "Verhoog de productiviteit in je kanaal door een Jira bot en Github bot te integreren. Deze worden voor jou gedownload." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chat met je team in een Feature Release-kanaal dat gemakkelijk verbinding maakt met je boards, playbooks en app bots." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Gebruik ons vergaderagenda-bord voor terugkerende vergaderingen zoals stand-up en ons Projecttakenbord om de voortgang van taken onderweg te beheren." + }, + { + "id": "worktemplate.category.product_teams", + "translation": "Productteams" + }, + { + "id": "model.draft.is_valid.user_id.app_error", + "translation": "Ongeldig gebruikers-id." + }, + { + "id": "model.draft.is_valid.update_at.app_error", + "translation": "Het veld 'update at' moet een geldige tijd zijn." + }, + { + "id": "model.draft.is_valid.root_id.app_error", + "translation": "Ongeldig root id." + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Ongeldige eigenschappen." + }, + { + "id": "model.draft.is_valid.priority.app_error", + "translation": "Ongeldige prioriteit" + }, + { + "id": "model.draft.is_valid.msg.app_error", + "translation": "Ongeldig bericht." + }, + { + "id": "model.draft.is_valid.file_ids.app_error", + "translation": "Ongeldige bestandids." + }, + { + "id": "model.draft.is_valid.create_at.app_error", + "translation": "Create at moet een geldige tijd zijn." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Ongeldig gebruikersid." + }, + { + "id": "model.draft.is_valid.channel_id.app_error", + "translation": "Ongeldig kanaalid." + }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Ongeldig bericht-id." + }, + { + "id": "app.worktemplates.get_templates.app_error", + "translation": "Kon geen werksjablonen ophalen" + }, + { + "id": "app.worktemplates.get_categories.app_error", + "translation": "Kan geen werksjablooncategorieën ophalen" + }, + { + "id": "app.post_prority.get_for_post.app_error", + "translation": "Kon geen berichtprioriteit ophalen voor bericht" + }, + { + "id": "app.draft.update.app_error", + "translation": "Kan het concept niet bijwerken." + }, + { + "id": "app.draft.save.app_error", + "translation": "Kan het concept niet opslaan." + }, + { + "id": "app.draft.get_for_draft.app_error", + "translation": "Kan geen bestanden ophalen voor concept." + }, + { + "id": "app.draft.get_drafts.app_error", + "translation": "Kon de Concepten van de gebruiker niet ophalen." + }, + { + "id": "app.draft.get.app_error", + "translation": "Kon het concept niet ophalen." + }, + { + "id": "app.draft.feature_disabled", + "translation": "De Conceptfunctie is uitgeschakeld." + }, + { + "id": "app.draft.delete.app_error", + "translation": "Kan het concept niet verwijderen." + }, + { + "id": "app.channel.get_priority_for_posts.app_error", + "translation": "Fout bij het ophalen van de prioriteiten voor berichten" + }, + { + "id": "app.channel.count_urgent_posts_since.app_error", + "translation": "Fout bij het tellen van de dringende berichten sinds de opgegeven datum." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Fout hij het bewaren van de bevestiging voor het bericht." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Fout hij het ophalen van de bevestiging voor bericht." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Fout hij het ophalen van de bevestiging." + }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Kan bevestiging niet verwijderen." + }, + { + "id": "api.user.get_users.validation.app_error", + "translation": "Fout bij het ophalen van rollen tijdens de validatie." + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "Kan bestand niet uploaden. Bestand is te groot." + }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "Bekijk jouw factuur" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Jouw {{.WorkspaceName}} werkruimte is nu opgewaardeerd." + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "Jouw {{.WorkspaceName}} werkruimte is nu geüpgraded. Dit zal gefactureerd worden vanaf {{.Date}}" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portaal niet beschikbaar voor self-hosted signup." + }, + { + "id": "api.drafts.disabled.app_error", + "translation": "De Conceptfunctie is uitgeschakeld." + }, + { + "id": "api.draft.create_draft.can_not_draft_to_deleted.error", + "translation": "Kan concept niet bewaren in een verwijderd kanaal" } ] diff --git a/i18n/pl.json b/i18n/pl.json index 2489a694c5..ae492d4db1 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -6351,14 +6351,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Osiągnięto limit czasu zadania synchronizacji AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Nie można przeanalizować zadania eksportowania komunikatu ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Nie można przeanalizować zadania eksportu komunikatu BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Nie znaleziono punktu końcowego interfejsu API klastra." @@ -9730,5 +9722,9 @@ { "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Twoja {{.WorkspaceName}} została zaktualizowana. Opłaty będą naliczane od {{.Date}}" + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portal niedostępny dla samodzielnej rejestracji." } ] diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 134b851682..81fed091ea 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -6571,14 +6571,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Tarefa de sincronização do AD/LDAP alcançou o limite de tempo." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Não foi possível analisar a tarefa de exportação de mensagens ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Não foi possível analisar a tarefa de exportação de mensagens BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Endpoint Cluster API não encontrado." diff --git a/i18n/ro.json b/i18n/ro.json index f2686f070c..a7ec824541 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -6619,14 +6619,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Am atins intervalul de timp pentru lucrarea de sincronizare AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Ar putea analiza mesaj export job ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Ar putea analiza mesaj export job BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Clustering API final nu a fost găsit." diff --git a/i18n/ru.json b/i18n/ru.json index 406b4220b0..62022682fd 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -6615,14 +6615,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Достигнут тайм-аут задания синхронизации AD/LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не удалось разобрать задание экспорта сообщения ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не удалось проанализировать задание экспорта сообщения BatchSize." - }, { "id": "ent.cluster.404.app_error", "translation": "Не найдена конечная точка API кластера." @@ -9730,5 +9722,9 @@ { "id": "api.user.get_users.validation.app_error", "translation": "Ошибка при получении ролей во время проверки." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Портал недоступен для самостоятельной регистрации." } ] diff --git a/i18n/sv.json b/i18n/sv.json index ec2aa98509..f5b180b542 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -3039,14 +3039,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Nådde timeout för AD/LDAP-synkronisering." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Kunde inte tolka värdet ExportFromTimestamp i export job." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Kunde inte tolka värdet BatchSize i export job." - }, { "id": "ent.id_loaded.license_disable.app_error", "translation": "Din licens tillåter inte pushnotifiering via meddelande-ID." @@ -9725,5 +9717,13 @@ { "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Din arbetsyta {{.WorkspaceName}} har nu uppgraderats. Du kommer att faktureras från och med {{.Date}}" + }, + { + "id": "model.draft.is_valid.props.app_error", + "translation": "Ogiltiga attribut." + }, + { + "id": "api.server.hosted_signup_unavailable.error", + "translation": "Portalen är inte tillgänglig för egen-hostad registrering." } ] diff --git a/i18n/tr.json b/i18n/tr.json index 39a1074bc0..2eadae012a 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -6603,14 +6603,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP eşitleme görevi zaman aşımına uğradı." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "İleti dışa aktarma görevinde ExportFromTimestamp değeri çözümlenemedi." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "İleti dışa aktarma görevinde BatchSize değeri çözümlenemedi." - }, { "id": "ent.cluster.404.app_error", "translation": "Küme API uç noktası bulunamadı." diff --git a/i18n/uk.json b/i18n/uk.json index cbba647d9d..7ffd8d2e6d 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -6335,14 +6335,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "Досягнута тайм-аут завдання синхронізації AD / LDAP." }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "Не вдалося проаналізувати завдання експортування повідомлень ExportFromTimestamp." - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "Не вдалося проаналізувати BatchSize завдання експортування повідомлення." - }, { "id": "ent.cluster.404.app_error", "translation": "Не знайдена кінцева точка API кластера." diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 2d91449bce..4fb33345ac 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -6479,14 +6479,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP 同步任务超时。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "无法解析 ExportFromTimestamp 导出任务消息。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "无法解析 BatchSize 导出任务消息。" - }, { "id": "ent.cluster.404.app_error", "translation": "未找到机群 API 接口。" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index e1a171691e..caedf2787b 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -6539,14 +6539,6 @@ "id": "ent.jobs.start_synchronize_job.timeout", "translation": "AD/LDAP 同步工作逾時。" }, - { - "id": "ent.jobs.do_job.batch_start_timestamp.parse_error", - "translation": "無法解析訊息匯出工作 ExportFromTimestamp。" - }, - { - "id": "ent.jobs.do_job.batch_size.parse_error", - "translation": "無法解析訊息匯出工作 BatchSize。" - }, { "id": "ent.cluster.404.app_error", "translation": "找不到叢集 API 端點。" diff --git a/jobs/export_process/worker.go b/jobs/export_process/worker.go index 2697b65980..7f80ac1950 100644 --- a/jobs/export_process/worker.go +++ b/jobs/export_process/worker.go @@ -44,26 +44,25 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { rd, wr := io.Pipe() - errCh := make(chan *model.AppError, 1) go func() { - defer close(errCh) - // Try to write without a timeout _, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename)) - errCh <- appErr + if appErr != nil { + // we close the reader here to prevent a deadlock when the bulk exporter tries to + // write into the pipe while app.WriteFile has already returned. The error will be + // returned by the writer part of the pipe when app.BulkExport tries to call + // wr.Write() on it. + rd.CloseWithError(appErr) // CloseWithError never returns an error + } }() - appErr := app.BulkExport(request.EmptyContext(app.Log()), wr, outPath, opts) - if err := wr.Close(); err != nil { - mlog.Warn("Worker: error closing writer") - } + logger := app.Log().With(mlog.String("job_id", job.Id)) + appErr := app.BulkExport(request.EmptyContext(logger), wr, outPath, opts) + wr.Close() // Close never returns an error if appErr != nil { return appErr } - if appErr := <-errCh; appErr != nil { - return appErr - } return nil } worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled) diff --git a/model/client4.go b/model/client4.go index 48d63e959c..298334d71a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8213,6 +8213,19 @@ func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { return cloudCustomer, BuildResponse(r), nil } +func (c *Client4) GetExpandStats(licenseId string) (*SubscriptionExpandStatus, *Response, error) { + r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/expand", licenseId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var subscriptionExpandable *SubscriptionExpandStatus + json.NewDecoder(r.Body).Decode(&subscriptionExpandable) + + return subscriptionExpandable, BuildResponse(r), nil +} + func (c *Client4) GetSubscription() (*Subscription, *Response, error) { r, err := c.DoAPIGet(c.cloudRoute()+"/subscription", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index 7feae13e3d..36c639d66c 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -124,6 +124,10 @@ type ValidateBusinessEmailResponse struct { IsValid bool `json:"is_valid"` } +type SubscriptionExpandStatus struct { + IsExpandable bool `json:"is_expandable"` +} + // CloudCustomerInfo represents editable info of a customer. type CloudCustomerInfo struct { Name string `json:"name"` diff --git a/model/feature_flags.go b/model/feature_flags.go index 27bab4830e..47dd10378d 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -16,9 +16,6 @@ type FeatureFlags struct { // all other values as false. TestBoolFeature bool - // Toggle on and off support for Collapsed Threads - CollapsedThreads bool - // Enable the remote cluster service for shared channels. EnableRemoteClusterService bool @@ -81,7 +78,6 @@ type FeatureFlags struct { func (f *FeatureFlags) SetDefaults() { f.TestFeature = "off" f.TestBoolFeature = false - f.CollapsedThreads = true f.EnableRemoteClusterService = false f.AppsEnabled = true f.PluginApps = "" diff --git a/model/hosted_customer.go b/model/hosted_customer.go index 0e1373a2bc..4f1917bdaf 100644 --- a/model/hosted_customer.go +++ b/model/hosted_customer.go @@ -10,6 +10,8 @@ type BootstrapSelfHostedSignupRequest struct { type BootstrapSelfHostedSignupResponse struct { Progress string `json:"progress"` + // email listed on the JWT claim + Email string `json:"email"` } type BootstrapSelfHostedSignupResponseInternal struct { diff --git a/model/migration.go b/model/migration.go index e0e9ae2267..766e51598a 100644 --- a/model/migration.go +++ b/model/migration.go @@ -39,4 +39,5 @@ const ( MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions" MigrationKeyAddPlayboosksManageRolesPermissions = "playbooks_manage_roles" MigrationKeyAddProductsBoardsPermissions = "products_boards" + MigrationKeyAddCustomUserGroupsPermissionRestore = "custom_groups_permission_restore" ) diff --git a/model/permission.go b/model/permission.go index 76cf07c872..a44a566964 100644 --- a/model/permission.go +++ b/model/permission.go @@ -366,6 +366,7 @@ var PermissionCreateCustomGroup *Permission var PermissionManageCustomGroupMembers *Permission var PermissionEditCustomGroup *Permission var PermissionDeleteCustomGroup *Permission +var PermissionRestoreCustomGroup *Permission var AllPermissions []*Permission var DeprecatedPermissions []*Permission @@ -1960,6 +1961,13 @@ func initializePermissions() { PermissionScopeGroup, } + PermissionRestoreCustomGroup = &Permission{ + "restore_custom_group", + "authentication.permissions.restore_custom_group.name", + "authentication.permissions.restore_custom_group.description", + PermissionScopeGroup, + } + // Playbooks PermissionPublicPlaybookCreate = &Permission{ "playbook_public_create", @@ -2340,6 +2348,7 @@ func initializePermissions() { PermissionManageCustomGroupMembers, PermissionEditCustomGroup, PermissionDeleteCustomGroup, + PermissionRestoreCustomGroup, } DeprecatedPermissions = []*Permission{ diff --git a/model/post.go b/model/post.go index 992188ac8e..e7c11edc44 100644 --- a/model/post.go +++ b/model/post.go @@ -119,7 +119,7 @@ type Post struct { } func (o *Post) Auditable() map[string]interface{} { - return map[string]interface{}{ // TODO check this + return map[string]interface{}{ "id": o.Id, "create_at": o.CreateAt, "update_at": o.UpdateAt, @@ -195,6 +195,15 @@ func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { return © } +func (o *PostPatch) Auditable() map[string]interface{} { + return map[string]interface{}{ + "is_pinned": o.IsPinned, + "props": o.Props, + "file_ids": o.FileIds, + "has_reactions": o.HasReactions, + } +} + type PostForExport struct { Post TeamName string @@ -324,13 +333,16 @@ type GetPostsOptions struct { type PostCountOptions struct { // Only include posts on a specific team. "" for any team. - TeamId string - MustHaveFile bool - MustHaveHashtag bool - ExcludeDeleted bool - UsersPostsOnly bool + TeamId string + MustHaveFile bool + MustHaveHashtag bool + ExcludeDeleted bool + ExcludeSystemPosts bool + UsersPostsOnly bool // AllowFromCache looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy. AllowFromCache bool + SincePostID string + SinceUpdateAt int64 } func (o *Post) Etag() string { diff --git a/model/role.go b/model/role.go index ac3fa3204e..b4a1825537 100644 --- a/model/role.go +++ b/model/role.go @@ -348,6 +348,7 @@ func init() { PermissionCreateCustomGroup.Id, PermissionEditCustomGroup.Id, PermissionDeleteCustomGroup.Id, + PermissionRestoreCustomGroup.Id, PermissionManageCustomGroupMembers.Id, } @@ -953,6 +954,7 @@ func MakeDefaultRoles() map[string]*Role { PermissionCreateCustomGroup.Id, PermissionEditCustomGroup.Id, PermissionDeleteCustomGroup.Id, + PermissionRestoreCustomGroup.Id, PermissionManageCustomGroupMembers.Id, }, SchemeManaged: true, diff --git a/model/utils.go b/model/utils.go index a8d82e3669..4ed85e2b32 100644 --- a/model/utils.go +++ b/model/utils.go @@ -256,11 +256,15 @@ func (er *AppError) Error() string { // render the error information sb.WriteString(er.Where) sb.WriteString(": ") - sb.WriteString(er.Message) + if er.Message != NoTranslation { + sb.WriteString(er.Message) + } // only render the detailed error when it's present if er.DetailedError != "" { - sb.WriteString(", ") + if er.Message != NoTranslation { + sb.WriteString(", ") + } sb.WriteString(er.DetailedError) } diff --git a/model/utils_test.go b/model/utils_test.go index 1b96f44516..606477d750 100644 --- a/model/utils_test.go +++ b/model/utils_test.go @@ -81,6 +81,11 @@ func TestAppError(t *testing.T) { t.Log(appErr.Error()) } +func TestAppErrorNoTranslation(t *testing.T) { + appErr := NewAppError("TestAppError", NoTranslation, nil, "test error", http.StatusBadRequest) + require.Equal(t, "TestAppError: test error", appErr.Error()) +} + func TestAppErrorJunk(t *testing.T) { rerr := AppErrorFromJSON(strings.NewReader("This is a broken test")) require.Equal(t, "body: This is a broken test", rerr.DetailedError) diff --git a/plugin/environment.go b/plugin/environment.go index 4ccbbdf023..cf7553579d 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -330,6 +330,8 @@ func (env *Environment) Activate(id string) (manifest *model.Manifest, activated return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component") } + mlog.Debug("Plugin activated", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version)) + return pluginInfo.Manifest, true, nil } diff --git a/product/api.go b/product/api.go index 5067c20ce9..54d473bb61 100644 --- a/product/api.go +++ b/product/api.go @@ -12,6 +12,8 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" + + fb_model "github.com/mattermost/focalboard/server/model" ) // RouterService enables registering the product router to the server. After registering the @@ -189,3 +191,23 @@ type PreferencesService interface { UpdatePreferencesForUser(userID string, preferences model.Preferences) *model.AppError DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError } + +// BoardsService is the API for accessing Boards service APIs. +// +// The service shall be registered via app.BoardsKey service key. +type BoardsService interface { + GetTemplates(teamID string, userID string) ([]*fb_model.Board, error) + GetBoard(boardID string) (*fb_model.Board, error) + CreateBoard(board *fb_model.Board, userID string, addmember bool) (*fb_model.Board, error) + PatchBoard(boardPatch *fb_model.BoardPatch, boardID string, userID string) (*fb_model.Board, error) + DeleteBoard(boardID string, userID string) error + SearchBoards(searchTerm string, searchField fb_model.BoardSearchField, userID string, includePublicBoards bool) ([]*fb_model.Board, error) + LinkBoardToChannel(boardID string, channelID string, userID string) (*fb_model.Board, error) + GetCards(boardID string) ([]*fb_model.Card, error) + GetCard(cardID string) (*fb_model.Card, error) + CreateCard(card *fb_model.Card, boardID string, userID string) (*fb_model.Card, error) + PatchCard(cardPatch *fb_model.CardPatch, cardID string, userID string) (*fb_model.Card, error) + DeleteCard(cardID string, userID string) error + HasPermissionToBoard(userID, boardID string, permission *model.Permission) bool + DuplicateBoard(boardID string, userID string, toTeam string, asTemplate bool) (*fb_model.BoardsAndBlocks, []*fb_model.BoardMember, error) +} diff --git a/product/service.go b/product/service.go index 221fa1c168..a0a92e6adc 100644 --- a/product/service.go +++ b/product/service.go @@ -25,4 +25,5 @@ const ( StoreKey ServiceKey = "storekey" SystemKey ServiceKey = "systemkey" PreferencesKey ServiceKey = "preferenceskey" + BoardsKey ServiceKey = "boards" ) diff --git a/services/upgrader/upgrader_linux.go b/services/upgrader/upgrader_linux.go index 1f895480ea..04ad1bcf82 100644 --- a/services/upgrader/upgrader_linux.go +++ b/services/upgrader/upgrader_linux.go @@ -333,7 +333,7 @@ func extractBinary(executablePath string, filename string) error { if err != nil { err2 := os.Rename(tmpFileName, executablePath) if err2 != nil { - mlog.Critical("Unable to restore the backup of the executable file. Restore the executable file manually.") + mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.") return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually") } return err @@ -342,13 +342,13 @@ func extractBinary(executablePath string, filename string) error { if _, err = io.Copy(outFile, tarReader); err != nil { err2 := os.Remove(executablePath) if err2 != nil { - mlog.Critical("Unable to restore the backup of the executable file. Restore the executable file manually.") + mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.") return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually") } err2 = os.Rename(tmpFileName, executablePath) if err2 != nil { - mlog.Critical("Unable to restore the backup of the executable file. Restore the executable file manually.") + mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.") return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually") } return err diff --git a/shared/mlog/global.go b/shared/mlog/global.go index de346123aa..71d7430a42 100644 --- a/shared/mlog/global.go +++ b/shared/mlog/global.go @@ -113,13 +113,10 @@ func Error(msg string, fields ...Field) { // Convenience method equivalent to calling `Log` with the `Critical` level. // DEPRECATED: Either use Error or Fatal. +// Critical level isn't added in mlog/levels.go:StdAll so calling this doesn't +// really work. For now we just call Fatal to atleast print something. func Critical(msg string, fields ...Field) { - logger := getGlobalLogger() - if logger == nil { - defaultLog(LvlCritical, msg, fields...) - return - } - logger.Critical(msg, fields...) + Fatal(msg, fields...) } func Fatal(msg string, fields ...Field) { diff --git a/shared/mlog/global_test.go b/shared/mlog/global_test.go index d97b3b15a3..486387e5f6 100644 --- a/shared/mlog/global_test.go +++ b/shared/mlog/global_test.go @@ -24,7 +24,6 @@ func TestLoggingBeforeInitialized(t *testing.T) { mlog.Debug("debug log") mlog.Warn("warning log") mlog.Error("error log") - mlog.Critical("critical log") }) } @@ -40,14 +39,13 @@ func TestLoggingAfterInitialized(t *testing.T) { Type: "file", Format: "json", FormatOptions: json.RawMessage(`{"enable_caller":true}`), - Levels: []mlog.Level{mlog.LvlCritical, mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug}, + Levels: []mlog.Level{mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug}, }, []string{ `{"timestamp":0,"level":"debug","msg":"real debug log","caller":"mlog/global_test.go:0"}`, `{"timestamp":0,"level":"info","msg":"real info log","caller":"mlog/global_test.go:0"}`, `{"timestamp":0,"level":"warn","msg":"real warning log","caller":"mlog/global_test.go:0"}`, `{"timestamp":0,"level":"error","msg":"real error log","caller":"mlog/global_test.go:0"}`, - `{"timestamp":0,"level":"critical","msg":"real critical log","caller":"mlog/global_test.go:0"}`, }, }, { @@ -56,11 +54,10 @@ func TestLoggingAfterInitialized(t *testing.T) { Type: "file", Format: "json", FormatOptions: json.RawMessage(`{"enable_caller":true}`), - Levels: []mlog.Level{mlog.LvlCritical, mlog.LvlError}, + Levels: []mlog.Level{mlog.LvlError}, }, []string{ `{"timestamp":0,"level":"error","msg":"real error log","caller":"mlog/global_test.go:0"}`, - `{"timestamp":0,"level":"critical","msg":"real critical log","caller":"mlog/global_test.go:0"}`, }, }, { @@ -69,14 +66,13 @@ func TestLoggingAfterInitialized(t *testing.T) { Type: "file", Format: "plain", FormatOptions: json.RawMessage(`{"delim":" | ", "enable_caller":true}`), - Levels: []mlog.Level{mlog.LvlCritical, mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug}, + Levels: []mlog.Level{mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug}, }, []string{ `debug | TIME | real debug log | caller="mlog/global_test.go:0"`, `info | TIME | real info log | caller="mlog/global_test.go:0"`, `warn | TIME | real warning log | caller="mlog/global_test.go:0"`, `error | TIME | real error log | caller="mlog/global_test.go:0"`, - `critical | TIME | real critical log | caller="mlog/global_test.go:0"`, }, }, { @@ -85,11 +81,10 @@ func TestLoggingAfterInitialized(t *testing.T) { Type: "file", Format: "plain", FormatOptions: json.RawMessage(`{"delim":" | ", "enable_caller":true}`), - Levels: []mlog.Level{mlog.LvlCritical, mlog.LvlError}, + Levels: []mlog.Level{mlog.LvlError}, }, []string{ `error | TIME | real error log | caller="mlog/global_test.go:0"`, - `critical | TIME | real critical log | caller="mlog/global_test.go:0"`, }, }, } @@ -116,7 +111,6 @@ func TestLoggingAfterInitialized(t *testing.T) { mlog.Info("real info log") mlog.Warn("real warning log") mlog.Error("real error log") - mlog.Critical("real critical log") logger.Shutdown() diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 87b443640a..129ef5254b 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -3197,7 +3197,7 @@ func (s *OpenTracingLayerComplianceStore) GetAll(offset int, limit int) (model.C return result, err } -func (s *OpenTracingLayerComplianceStore) MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { +func (s *OpenTracingLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ComplianceStore.MessageExport") s.Root.Store.SetContext(newCtx) @@ -3206,7 +3206,7 @@ func (s *OpenTracingLayerComplianceStore) MessageExport(cursor model.MessageExpo }() defer span.Finish() - result, resultVar1, err := s.ComplianceStore.MessageExport(cursor, limit) + result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 38a3f52dbf..065a1400a9 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -3567,11 +3567,11 @@ func (s *RetryLayerComplianceStore) GetAll(offset int, limit int) (model.Complia } -func (s *RetryLayerComplianceStore) MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { +func (s *RetryLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { tries := 0 for { - result, resultVar1, err := s.ComplianceStore.MessageExport(cursor, limit) + result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit) if err == nil { return result, resultVar1, nil } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 85864f3784..ca7575924a 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -3850,7 +3850,31 @@ func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId defer finalizeTransactionX(transaction, &err) channelMembers := []channelMember{} - if err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (?, ?) ORDER BY ChannelId, UserId LIMIT 100", fromChannelId, fromUserId); err != nil { + query := ` + SELECT + ChannelId, + UserId, + Roles, + LastViewedAt, + MsgCount, + MentionCount, + MentionCountRoot, + COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount, + MsgCountRoot, + NotifyProps, + LastUpdateAt, + SchemeUser, + SchemeAdmin, + SchemeGuest + FROM + ChannelMembers + WHERE + (ChannelId, UserId) > (?, ?) + ORDER BY ChannelId, UserId + LIMIT 100 + ` + + if err := transaction.Select(&channelMembers, query, fromChannelId, fromUserId); err != nil { return nil, errors.Wrap(err, "failed to find ChannelMembers") } @@ -3954,7 +3978,31 @@ func (s SqlChannelStore) ClearAllCustomRoleAssignments() (err error) { } channelMembers := []*channelMember{} - if err = transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (?, ?) ORDER BY ChannelId, UserId LIMIT 1000", lastChannelId, lastUserId); err != nil { + query := ` + SELECT + ChannelId, + UserId, + Roles, + LastViewedAt, + MsgCount, + MentionCount, + MentionCountRoot, + COALESCE(UrgentMentionCount, 0) AS UrgentMentionCount, + MsgCountRoot, + NotifyProps, + LastUpdateAt, + SchemeUser, + SchemeAdmin, + SchemeGuest + FROM + ChannelMembers + WHERE + (ChannelId, UserId) > (?, ?) + ORDER BY ChannelId, UserId + LIMIT 1000 + ` + + if err = transaction.Select(&channelMembers, query, lastChannelId, lastUserId); err != nil { finalizeTransactionX(transaction, &err) return errors.Wrap(err, "failed to find ChannelMembers") } diff --git a/store/sqlstore/compliance_store.go b/store/sqlstore/compliance_store.go index 6e2ae38ac1..9c76cea3d6 100644 --- a/store/sqlstore/compliance_store.go +++ b/store/sqlstore/compliance_store.go @@ -4,6 +4,7 @@ package sqlstore import ( + "context" "database/sql" "fmt" "strings" @@ -270,7 +271,7 @@ func (s SqlComplianceStore) ComplianceExport(job *model.Compliance, cursor model return append(channelPosts, directMessagePosts...), cursor, nil } -func (s SqlComplianceStore) MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { +func (s SqlComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { var args []any args = append(args, model.ChannelTypeDirect, model.ChannelTypeGroup, cursor.LastPostUpdateAt, cursor.LastPostUpdateAt, cursor.LastPostId, limit) query := @@ -317,7 +318,7 @@ func (s SqlComplianceStore) MessageExport(cursor model.MessageExportCursor, limi LIMIT ?` cposts := []*model.MessageExport{} - if err := s.GetReplicaX().Select(&cposts, query, args...); err != nil { + if err := s.GetReplicaX().SelectCtx(ctx, &cposts, query, args...); err != nil { return nil, cursor, errors.Wrap(err, "unable to export messages") } if len(cposts) > 0 { diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index b7f5979cd9..8bb4de38cd 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -2272,6 +2272,20 @@ func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int6 query = query.Where(sq.Eq{"p.DeleteAt": 0}) } + if options.ExcludeSystemPosts { + query = query.Where("p.Type NOT LIKE 'system_%'") + } + + if options.SinceUpdateAt > 0 { + query = query.Where(sq.Or{ + sq.Gt{"p.UpdateAt": options.SinceUpdateAt}, + sq.And{ + sq.Eq{"p.UpdateAt": options.SinceUpdateAt}, + sq.Gt{"p.Id": options.SincePostID}, + }, + }) + } + queryString, args, err := query.ToSql() if err != nil { return 0, errors.Wrap(err, "post_tosql") diff --git a/store/sqlstore/sqlx_wrapper.go b/store/sqlstore/sqlx_wrapper.go index 95b274aae9..3d215ff8a5 100644 --- a/store/sqlstore/sqlx_wrapper.go +++ b/store/sqlstore/sqlx_wrapper.go @@ -224,8 +224,12 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) { } func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error { + return w.SelectCtx(context.Background(), dest, query, args...) +} + +func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, args ...any) error { query = w.DB.Rebind(query) - ctx, cancel := context.WithTimeout(context.Background(), w.queryTimeout) + ctx, cancel := context.WithTimeout(ctx, w.queryTimeout) defer cancel() if w.trace { diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 4c1bf1f05b..28a86886cf 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -9,7 +9,7 @@ import ( dbsql "database/sql" "fmt" "log" - "path/filepath" + "path" "strconv" "strings" "sync" @@ -226,7 +226,9 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS return store } -func setupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB { +// SetupConnection sets up the connection to the database and pings it to make sure it's alive. +// It also applies any database configuration settings that are required. +func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB { db, err := dbsql.Open(*settings.DriverName, dataSource) if err != nil { mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err)) @@ -292,7 +294,7 @@ func (ss *SqlStore) initConnection() { } } - handle := setupConnection("master", dataSource, ss.settings) + handle := SetupConnection("master", dataSource, ss.settings) ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), time.Duration(*ss.settings.QueryTimeout)*time.Second, *ss.settings.Trace) @@ -303,7 +305,7 @@ func (ss *SqlStore) initConnection() { if len(ss.settings.DataSourceReplicas) > 0 { ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas)) for i, replica := range ss.settings.DataSourceReplicas { - handle := setupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings) + handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings) ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), time.Duration(*ss.settings.QueryTimeout)*time.Second, *ss.settings.Trace) @@ -316,7 +318,7 @@ func (ss *SqlStore) initConnection() { if len(ss.settings.DataSourceSearchReplicas) > 0 { ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas)) for i, replica := range ss.settings.DataSourceSearchReplicas { - handle := setupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings) + handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings) ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()), time.Duration(*ss.settings.QueryTimeout)*time.Second, *ss.settings.Trace) @@ -332,7 +334,7 @@ func (ss *SqlStore) initConnection() { if src.DataSource == nil { continue } - ss.replicaLagHandles[i] = setupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings) + ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings) } } } @@ -1041,7 +1043,7 @@ func (ss *SqlStore) hasLicense() bool { func (ss *SqlStore) migrate(direction migrationDirection) error { assets := db.Assets() - assetsList, err := assets.ReadDir(filepath.Join("migrations", ss.DriverName())) + assetsList, err := assets.ReadDir(path.Join("migrations", ss.DriverName())) if err != nil { return err } @@ -1054,7 +1056,7 @@ func (ss *SqlStore) migrate(direction migrationDirection) error { src, err := mbindata.WithInstance(&mbindata.AssetSource{ Names: assetNamesForDriver, AssetFunc: func(name string) ([]byte, error) { - return assets.ReadFile(filepath.Join("migrations", ss.DriverName(), name)) + return assets.ReadFile(path.Join("migrations", ss.DriverName(), name)) }, }) if err != nil { @@ -1073,7 +1075,7 @@ func (ss *SqlStore) migrate(direction migrationDirection) error { if err != nil { return err } - db := setupConnection("master", dataSource, ss.settings) + db := SetupConnection("master", dataSource, ss.settings) driver, err = ms.WithInstance(db) defer db.Close() case model.DatabaseDriverPostgres: diff --git a/store/store.go b/store/store.go index 49490afdbb..8a1c69d91a 100644 --- a/store/store.go +++ b/store/store.go @@ -549,7 +549,7 @@ type ComplianceStore interface { Get(id string) (*model.Compliance, error) GetAll(offset, limit int) (model.Compliances, error) ComplianceExport(compliance *model.Compliance, cursor model.ComplianceExportCursor, limit int) ([]*model.CompliancePost, model.ComplianceExportCursor, error) - MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) + MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) } type OAuthStore interface { diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go index f39b1c43ef..7c2d106325 100644 --- a/store/storetest/compliance_store.go +++ b/store/storetest/compliance_store.go @@ -4,6 +4,7 @@ package storetest import ( + "context" "encoding/json" "testing" "time" @@ -399,7 +400,7 @@ func testMessageExportPublicChannel(t *testing.T, ss store.Store) { // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -469,7 +470,7 @@ func testMessageExportPublicChannel(t *testing.T, ss store.Store) { // fetch the message exports for both posts that user1 sent messageExportMap := map[string]model.MessageExport{} - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 2, len(messages)) @@ -503,7 +504,7 @@ func testMessageExportPrivateChannel(t *testing.T, ss store.Store) { // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -573,7 +574,7 @@ func testMessageExportPrivateChannel(t *testing.T, ss store.Store) { // fetch the message exports for both posts that user1 sent messageExportMap := map[string]model.MessageExport{} - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 2, len(messages)) @@ -609,7 +610,7 @@ func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) { // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -664,7 +665,7 @@ func testMessageExportDirectMessageChannel(t *testing.T, ss store.Store) { // fetch the message export for the post that user1 sent messageExportMap := map[string]model.MessageExport{} - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) @@ -690,7 +691,7 @@ func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) { // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -762,7 +763,7 @@ func testMessageExportGroupMessageChannel(t *testing.T, ss store.Store) { // fetch the message export for the post that user1 sent messageExportMap := map[string]model.MessageExport{} - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 10}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) @@ -787,7 +788,7 @@ func testEditExportMessage(t *testing.T, ss store.Store) { defer cleanupStoreState(t, ss) // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -842,7 +843,7 @@ func testEditExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports from the start - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 2, len(messages)) @@ -879,7 +880,7 @@ func testEditAfterExportMessage(t *testing.T, ss store.Store) { defer cleanupStoreState(t, ss) // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -927,7 +928,7 @@ func testEditAfterExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports from the start - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) @@ -953,7 +954,7 @@ func testEditAfterExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports after edit - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: postEditTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postEditTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 2, len(messages)) @@ -990,7 +991,7 @@ func testDeleteExportMessage(t *testing.T, ss store.Store) { defer cleanupStoreState(t, ss) // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -1043,7 +1044,7 @@ func testDeleteExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports from the start - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) @@ -1075,7 +1076,7 @@ func testDeleteAfterExportMessage(t *testing.T, ss store.Store) { defer cleanupStoreState(t, ss) // get the starting number of message export entries startTime := model.GetMillis() - messages, _, err := ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err := ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 0, len(messages)) @@ -1123,7 +1124,7 @@ func testDeleteAfterExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports from the start - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: startTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) @@ -1146,7 +1147,7 @@ func testDeleteAfterExportMessage(t *testing.T, ss store.Store) { require.NoError(t, err) // fetch the message exports after delete - messages, _, err = ss.Compliance().MessageExport(model.MessageExportCursor{LastPostUpdateAt: postDeleteTime - 1}, 10) + messages, _, err = ss.Compliance().MessageExport(context.Background(), model.MessageExportCursor{LastPostUpdateAt: postDeleteTime - 1}, 10) require.NoError(t, err) assert.Equal(t, 1, len(messages)) diff --git a/store/storetest/mocks/ComplianceStore.go b/store/storetest/mocks/ComplianceStore.go index 657670b023..a1041e3bae 100644 --- a/store/storetest/mocks/ComplianceStore.go +++ b/store/storetest/mocks/ComplianceStore.go @@ -5,6 +5,8 @@ package mocks import ( + context "context" + model "github.com/mattermost/mattermost-server/v6/model" mock "github.com/stretchr/testify/mock" ) @@ -90,13 +92,13 @@ func (_m *ComplianceStore) GetAll(offset int, limit int) (model.Compliances, err return r0, r1 } -// MessageExport provides a mock function with given fields: cursor, limit -func (_m *ComplianceStore) MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { - ret := _m.Called(cursor, limit) +// MessageExport provides a mock function with given fields: ctx, cursor, limit +func (_m *ComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { + ret := _m.Called(ctx, cursor, limit) var r0 []*model.MessageExport - if rf, ok := ret.Get(0).(func(model.MessageExportCursor, int) []*model.MessageExport); ok { - r0 = rf(cursor, limit) + if rf, ok := ret.Get(0).(func(context.Context, model.MessageExportCursor, int) []*model.MessageExport); ok { + r0 = rf(ctx, cursor, limit) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.MessageExport) @@ -104,15 +106,15 @@ func (_m *ComplianceStore) MessageExport(cursor model.MessageExportCursor, limit } var r1 model.MessageExportCursor - if rf, ok := ret.Get(1).(func(model.MessageExportCursor, int) model.MessageExportCursor); ok { - r1 = rf(cursor, limit) + if rf, ok := ret.Get(1).(func(context.Context, model.MessageExportCursor, int) model.MessageExportCursor); ok { + r1 = rf(ctx, cursor, limit) } else { r1 = ret.Get(1).(model.MessageExportCursor) } var r2 error - if rf, ok := ret.Get(2).(func(model.MessageExportCursor, int) error); ok { - r2 = rf(cursor, limit) + if rf, ok := ret.Get(2).(func(context.Context, model.MessageExportCursor, int) error); ok { + r2 = rf(ctx, cursor, limit) } else { r2 = ret.Error(2) } diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index f3c7d91630..c6e9ecd7ef 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -38,6 +38,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDuration", func(t *testing.T) { testPostCountsByDay(t, ss) }) + t.Run("PostCounts", func(t *testing.T) { testPostCounts(t, ss) }) t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) }) t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, ss) }) t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, ss) }) @@ -2685,45 +2686,169 @@ func testPostCountsByDay(t *testing.T, ss store.Store) { r1, err = ss.Post().AnalyticsPostCountsByDay(postCountsOptions) require.NoError(t, err) assert.Equal(t, float64(1), r1[0].Value) +} + +func testPostCounts(t *testing.T, ss store.Store) { + now := time.Now() + twentyMinAgo := now.Add(-20 * time.Minute).UnixMilli() + fifteenMinAgo := now.Add(-15 * time.Minute).UnixMilli() + tenMinAgo := now.Add(-10 * time.Minute).UnixMilli() + + t1 := &model.Team{} + t1.DisplayName = "DisplayName" + t1.Name = NewTestId() + t1.Email = MakeEmail() + t1.Type = model.TeamOpen + t1, err := ss.Team().Save(t1) + require.NoError(t, err) + + c1 := &model.Channel{} + c1.TeamId = t1.Id + c1.DisplayName = "Channel2" + c1.Name = NewTestId() + c1.Type = model.ChannelTypeOpen + c1, nErr := ss.Channel().Save(c1, -1) + require.NoError(t, nErr) + + // system post + p1 := &model.Post{} + p1.Type = "system_add_to_channel" + p1.ChannelId = c1.Id + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.CreateAt = twentyMinAgo + p1.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p1) + require.NoError(t, nErr) + + p2 := &model.Post{} + p2.ChannelId = c1.Id + p2.UserId = model.NewId() + p2.Message = NewTestId() + p2.Hashtags = "hashtag" + p2.CreateAt = twentyMinAgo + p2.UpdateAt = twentyMinAgo + p2, nErr = ss.Post().Save(p2) + require.NoError(t, nErr) + + p3 := &model.Post{} + p3.ChannelId = c1.Id + p3.UserId = model.NewId() + p3.Message = NewTestId() + p3.FileIds = []string{"fileId1"} + p3.CreateAt = twentyMinAgo + p3.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p3) + require.NoError(t, nErr) + + p4 := &model.Post{} + p4.ChannelId = c1.Id + p4.UserId = model.NewId() + p4.Message = NewTestId() + p4.Filenames = []string{"filename1"} + p4.CreateAt = tenMinAgo + p4.UpdateAt = tenMinAgo + p4, nErr = ss.Post().Save(p4) + require.NoError(t, nErr) + + p5 := &model.Post{} + p5.ChannelId = c1.Id + p5.UserId = p4.UserId + p5.Message = NewTestId() + p5.Hashtags = "hashtag" + p5.FileIds = []string{"fileId2"} + p5.CreateAt = tenMinAgo + p5.UpdateAt = tenMinAgo + _, nErr = ss.Post().Save(p5) + require.NoError(t, nErr) + + bot1 := &model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: model.NewId(), + UserId: model.NewId(), + } + _, nErr = ss.Bot().Save(bot1) + require.NoError(t, nErr) + + p6 := &model.Post{} + p6.Message = "bot message one" + p6.ChannelId = c1.Id + p6.UserId = bot1.UserId + p6.CreateAt = twentyMinAgo + p6.UpdateAt = twentyMinAgo + _, nErr = ss.Post().Save(p6) + require.NoError(t, nErr) + + p7 := &model.Post{} + p7.Message = "bot message two" + p7.ChannelId = c1.Id + p7.UserId = bot1.UserId + p7.CreateAt = tenMinAgo + p7.UpdateAt = tenMinAgo + _, nErr = ss.Post().Save(p7) + require.NoError(t, nErr) + + // total across all teams + c, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{}) + require.NoError(t, err) + assert.GreaterOrEqual(t, c, int64(7)) // total for single team - r2, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id}) require.NoError(t, err) - assert.Equal(t, int64(6), r2) + assert.Equal(t, int64(7), c) - // total across teams - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{}) + // with files + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(6)) + assert.Equal(t, int64(3), c) - // total across teams with files - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true}) + // with hashtags + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveHashtag: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(3)) + assert.Equal(t, int64(2), c) - // total across teams with hashtags - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveHashtag: true}) + // with hashtags and files + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, MustHaveFile: true, MustHaveHashtag: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(2)) + assert.Equal(t, int64(1), c) - // total across teams with hashtags and files - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true, MustHaveHashtag: true}) + // excluding system posts + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeSystemPosts: true}) require.NoError(t, err) - assert.GreaterOrEqual(t, r2, int64(1)) + assert.Equal(t, int64(6), c) + + // before update_at time + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: fifteenMinAgo}) + require.NoError(t, err) + assert.Equal(t, int64(3), c) + + // equal to update_at time + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo}) + require.NoError(t, err) + assert.Equal(t, int64(3), c) + + // since update_at and since post id + tenMinAgoIDs := []string{p4.Id, p5.Id, p7.Id} + sort.Strings(tenMinAgoIDs) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, SinceUpdateAt: tenMinAgo, SincePostID: tenMinAgoIDs[0]}) + require.NoError(t, err) + assert.Equal(t, int64(2), c) // delete 1 post - err = ss.Post().Delete(o1.Id, 1, o1.UserId) + err = ss.Post().Delete(p2.Id, 1, p2.UserId) require.NoError(t, err) // total for single team with the deleted post excluded - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true}) require.NoError(t, err) - assert.Equal(t, int64(5), r2) + assert.Equal(t, int64(6), c) // total users only posts for single team with the deleted post excluded - r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true}) + c, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true, UsersPostsOnly: true}) require.NoError(t, err) - assert.Equal(t, int64(3), r2) + assert.Equal(t, int64(3), c) } func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStore) { diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 02be7b110d..19ccaf656d 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -2932,10 +2932,10 @@ func (s *TimerLayerComplianceStore) GetAll(offset int, limit int) (model.Complia return result, err } -func (s *TimerLayerComplianceStore) MessageExport(cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { +func (s *TimerLayerComplianceStore) MessageExport(ctx context.Context, cursor model.MessageExportCursor, limit int) ([]*model.MessageExport, model.MessageExportCursor, error) { start := time.Now() - result, resultVar1, err := s.ComplianceStore.MessageExport(cursor, limit) + result, resultVar1, err := s.ComplianceStore.MessageExport(ctx, cursor, limit) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/templates/payment_failed_no_card_body.html b/templates/payment_failed_no_card_body.html index 6966bf296b..ec7d4d5de6 100644 --- a/templates/payment_failed_no_card_body.html +++ b/templates/payment_failed_no_card_body.html @@ -1,7 +1,7 @@ {{define "payment_failed_no_card_body"}} -
@@ -18,23 +18,16 @@
- + style="border-collapse: collapse"> +
+ style="padding: 20px 0 0; text-align: center; margin: 0 auto; max-width: 443px"> -
- - - - -
-

- {{ .Props.Title }}

-
+
+

+ {{ .Props.Title }}

{{ .Props.Info1 }}

@@ -50,7 +43,7 @@
- +
@@ -65,7 +58,7 @@
- +
diff --git a/testlib/store.go b/testlib/store.go index 800764da07..9f70dfa8cd 100644 --- a/testlib/store.go +++ b/testlib/store.go @@ -68,6 +68,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", model.MigrationKeyAddPlaybooksPermissions).Return(&model.System{Name: model.MigrationKeyAddPlaybooksPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) + systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissionRestore).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissionRestore, Value: "true"}, nil) systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil) systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil) systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()