diff --git a/api4/post.go b/api4/post.go index b4ad80d653..6aa7049563 100644 --- a/api4/post.go +++ b/api4/post.go @@ -38,6 +38,9 @@ func (api *API) InitPost() { api.BaseRoutes.Post.Handle("/pin", api.APISessionRequired(pinPost)).Methods("POST") api.BaseRoutes.Post.Handle("/unpin", api.APISessionRequired(unpinPost)).Methods("POST") + + api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods("POST") + api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods("DELETE") } func createPost(c *Context, w http.ResponseWriter, r *http.Request) { @@ -941,6 +944,80 @@ func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) { saveIsPinnedPost(c, w, false) } +func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) { + // license check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequirePostId().RequireUserId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { + c.SetPermissionError(model.PermissionEditOtherUsers) + return + } + + if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) { + c.SetPermissionError(model.PermissionReadChannel) + return + } + + acknowledgement, appErr := c.App.SaveAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId) + if appErr != nil { + c.Err = appErr + return + } + + js, err := json.Marshal(acknowledgement) + if err != nil { + c.Err = model.NewAppError("acknowledgePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + return + } + + w.Write(js) +} + +func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) { + // license check + permissionErr := minimumProfessionalLicense(c) + if permissionErr != nil { + c.Err = permissionErr + return + } + c.RequirePostId().RequireUserId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { + c.SetPermissionError(model.PermissionEditOtherUsers) + return + } + + if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) { + c.SetPermissionError(model.PermissionReadChannel) + return + } + + _, err := c.App.GetSinglePost(c.Params.PostId, false) + if err != nil { + c.Err = err + return + } + + appErr := c.App.DeleteAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId) + if appErr != nil { + c.Err = appErr + return + } + + ReturnStatusOK(w) +} + func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/post_test.go b/api4/post_test.go index 86b064191e..3240347e1e 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -3342,3 +3342,87 @@ func TestPostReminder(t *testing.T) { require.Truef(t, caught, "User should have received %s event", model.WebsocketEventEphemeralMessage) } + +func TestAcknowledgePost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + client := th.Client + + post := th.BasicPost + ack, _, err := client.AcknowledgePost(post.Id, th.BasicUser.Id) + require.NoError(t, err) + + acks, appErr := th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, appErr) + require.Len(t, acks, 1) + require.Equal(t, acks[0], ack) + + _, resp, err := client.AcknowledgePost("junk", th.BasicUser.Id) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + _, resp, err = client.AcknowledgePost(GenerateTestId(), th.BasicUser.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + _, resp, err = client.AcknowledgePost(post.Id, "junk") + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + _, resp, err = client.AcknowledgePost(post.Id, th.BasicUser2.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + client.Logout() + _, resp, err = client.AcknowledgePost(post.Id, th.BasicUser.Id) + require.Error(t, err) + CheckUnauthorizedStatus(t, resp) + + _, _, err = th.SystemAdminClient.AcknowledgePost(post.Id, th.SystemAdminUser.Id) + require.NoError(t, err) +} + +func TestUnacknowledgePost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + client := th.Client + + post := th.BasicPost + ack, _, err := client.AcknowledgePost(post.Id, th.BasicUser.Id) + require.NoError(t, err) + + acks, appErr := th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, appErr) + require.Len(t, acks, 1) + require.Equal(t, acks[0], ack) + + resp, err := client.UnacknowledgePost("junk", th.BasicUser.Id) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + resp, err = client.UnacknowledgePost(GenerateTestId(), th.BasicUser.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + resp, err = client.UnacknowledgePost(post.Id, "junk") + require.Error(t, err) + CheckBadRequestStatus(t, resp) + + resp, err = client.UnacknowledgePost(post.Id, th.BasicUser2.Id) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + _, err = client.UnacknowledgePost(post.Id, th.BasicUser.Id) + require.NoError(t, err) + + acks, appErr = th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, appErr) + require.Len(t, acks, 0) + + client.Logout() + resp, err = client.UnacknowledgePost(post.Id, th.BasicUser.Id) + require.Error(t, err) + CheckUnauthorizedStatus(t, resp) +} diff --git a/api4/user_test.go b/api4/user_test.go index d269593fce..17195f7d6d 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -6569,6 +6569,7 @@ func TestSingleThreadGet(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ThreadAutoFollow = true + *cfg.ServiceSettings.PostPriority = false *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn *cfg.ServiceSettings.PostPriority = true cfg.FeatureFlags.PostPriority = true @@ -6615,6 +6616,7 @@ func TestSingleThreadGet(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.PostPriority = true + cfg.FeatureFlags.PostPriority = true }) tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true) diff --git a/app/app_iface.go b/app/app_iface.go index 093d97a035..352ece5283 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -509,6 +509,7 @@ type AppIface interface { DeactivateGuests(c *request.Context) *model.AppError DeactivateMfa(userID string) *model.AppError DeauthorizeOAuthAppForUser(userID, appID string) *model.AppError + DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError DeleteAllExpiredPluginKeys() *model.AppError DeleteAllKeysForPlugin(pluginID string) *model.AppError DeleteBrandImage() *model.AppError @@ -567,6 +568,8 @@ type AppIface interface { GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) GeneratePublicLink(siteURL string, info *model.FileInfo) string GenerateSupportPacket() []model.FileData + GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) + GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) GetAllChannels(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) @@ -984,6 +987,7 @@ type AppIface interface { SanitizeProfile(user *model.User, asAdmin bool) SanitizeTeam(session model.Session, team *model.Team) *model.Team SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team + SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) SaveBrandImage(imageData *multipart.FileHeader) *model.AppError diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index d58f160c4b..f4ac4fbb51 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -2838,6 +2838,28 @@ func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string { return resultVar0 } +func (a *OpenTracingAppLayer) DeleteAcknowledgementForPost(c *request.Context, postID string, userID string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAcknowledgementForPost") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.DeleteAcknowledgementForPost(c, postID, userID) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) DeleteAllExpiredPluginKeys() *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllExpiredPluginKeys") @@ -4411,6 +4433,50 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData { return resultVar0 } +func (a *OpenTracingAppLayer) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPost") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetAcknowledgementsForPost(postID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (a *OpenTracingAppLayer) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPostList") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetAcknowledgementsForPostList(postList) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests") @@ -14312,6 +14378,28 @@ func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*mode return resultVar0 } +func (a *OpenTracingAppLayer) SaveAcknowledgementForPost(c *request.Context, postID string, userID string) (*model.PostAcknowledgement, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAcknowledgementForPost") + + a.ctx = newCtx + a.app.Srv().Store().SetContext(newCtx) + defer func() { + a.app.Srv().Store().SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.SaveAcknowledgementForPost(c, postID, userID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification") diff --git a/app/post_acknowledgements.go b/app/post_acknowledgements.go new file mode 100644 index 0000000000..a91f76d02b --- /dev/null +++ b/app/post_acknowledgements.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/mattermost/mattermost-server/v6/app/request" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/store" +) + +func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) { + post, err := a.GetSinglePost(postID, false) + if err != nil { + return nil, err + } + + channel, err := a.GetChannel(c, post.ChannelId) + if err != nil { + return nil, err + } + + if channel.DeleteAt > 0 { + return nil, model.NewAppError("SaveAcknowledgementForPost", "api.acknowledgement.save.archived_channel.app_error", nil, "", http.StatusForbidden) + } + + acknowledgedAt := model.GetMillis() + acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt) + + if nErr != nil { + var appErr *model.AppError + switch { + case errors.As(nErr, &appErr): + return nil, appErr + default: + return nil, model.NewAppError("SaveAcknowledgementForPost", "app.acknowledgement.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + } + + a.Srv().Go(func() { + a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementAdded, acknowledgement, post) + }) + + return acknowledgement, nil +} + +func (a *App) DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError { + post, err := a.GetSinglePost(postID, false) + if err != nil { + return err + } + + channel, err := a.GetChannel(c, post.ChannelId) + if err != nil { + return err + } + + if channel.DeleteAt > 0 { + return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.archived_channel.app_error", nil, "", http.StatusForbidden) + } + + oldAck, nErr := a.Srv().Store().PostAcknowledgement().Get(postID, userID) + + if nErr != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(nErr, &nfErr): + return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr) + default: + return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + } + + if model.GetMillis()-oldAck.AcknowledgedAt > 5*60*1000 { + return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.deadline.app_error", nil, "", http.StatusForbidden) + } + + nErr = a.Srv().Store().PostAcknowledgement().Delete(oldAck) + if nErr != nil { + return model.NewAppError("DeleteAcknowledgementForPost", "app.acknowledgement.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + + a.Srv().Go(func() { + a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementRemoved, oldAck, post) + }) + + return nil +} + +func (a *App) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) { + acknowledgements, nErr := a.Srv().Store().PostAcknowledgement().GetForPost(postID) + if nErr != nil { + return nil, model.NewAppError("GetAcknowledgementsForPost", "app.acknowledgement.getforpost.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) + } + + return acknowledgements, nil +} + +func (a *App) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) { + acknowledgements, err := a.Srv().Store().PostAcknowledgement().GetForPosts(postList.Order) + + if err != nil { + return nil, model.NewAppError("GetPostAcknowledgementsForPostList", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + acknowledgementsMap := make(map[string][]*model.PostAcknowledgement) + + for _, ack := range acknowledgements { + acknowledgementsMap[ack.PostId] = append(acknowledgementsMap[ack.PostId], ack) + } + + return acknowledgementsMap, nil +} + +func (a *App) sendAcknowledgementEvent(event string, acknowledgement *model.PostAcknowledgement, post *model.Post) { + // send out that a acknowledgement has been added/removed + message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "") + + acknowledgementJSON, err := json.Marshal(acknowledgement) + if err != nil { + a.Log().Warn("Failed to encode acknowledgement to JSON", mlog.Err(err)) + } + message.Add("acknowledgement", string(acknowledgementJSON)) + a.Publish(message) +} diff --git a/app/post_acknowledgements_test.go b/app/post_acknowledgements_test.go new file mode 100644 index 0000000000..f1306c29fd --- /dev/null +++ b/app/post_acknowledgements_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/require" +) + +func TestPostAcknowledgementsApp(t *testing.T) { + t.Run("SaveAcknowledgementForPost", func(t *testing.T) { testSaveAcknowledgementForPost(t) }) + t.Run("DeleteAcknowledgementForPost", func(t *testing.T) { testDeleteAcknowledgementForPost(t) }) + t.Run("GetAcknowledgementsForPostList", func(t *testing.T) { testGetAcknowledgementsForPostList(t) }) +} + +func testSaveAcknowledgementForPost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("save acknowledgment for post should save acknowledgement", func(t *testing.T) { + post, err := th.App.CreatePostAsUser(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Message: "message", + }, "", true) + + require.Nil(t, err) + + acknowledgment, err := th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id) + require.Nil(t, err) + + require.Greater(t, acknowledgment.AcknowledgedAt, int64(0)) + require.Equal(t, post.Id, acknowledgment.PostId) + require.Equal(t, th.BasicUser.Id, acknowledgment.UserId) + }) +} + +func testDeleteAcknowledgementForPost(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + post, err := th.App.CreatePostAsUser(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + CreateAt: model.GetMillis(), + Message: "message", + }, "", true) + require.Nil(t, err) + + t.Run("delete acknowledgment for post should delete acknowledgement", func(t *testing.T) { + _, err = th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id) + require.Nil(t, err) + + acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, err) + require.Len(t, acknowledgments, 1) + require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0)) + + err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id) + require.Nil(t, err) + + acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, err) + require.Empty(t, acknowledgments) + }) + + t.Run("delete acknowledgment for post after 5 min after acknowledged should not delete", func(t *testing.T) { + _, nErr := th.App.Srv().Store().PostAcknowledgement().Save(post.Id, th.BasicUser.Id, model.GetMillis()-int64(6*60*1000)) + require.NoError(t, nErr) + + acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, err) + require.Len(t, acknowledgments, 1) + require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0)) + + err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id) + require.NotNil(t, err) + require.Equal(t, 403, err.StatusCode) + + acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id) + require.Nil(t, err) + require.Len(t, acknowledgments, 1) + require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0)) + }) +} + +func testGetAcknowledgementsForPostList(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + p1, err := th.App.CreatePostAsUser(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + CreateAt: model.GetMillis(), + Message: "message", + }, "", true) + require.Nil(t, err) + + p2, err := th.App.CreatePostAsUser(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + CreateAt: model.GetMillis(), + Message: "message", + }, "", true) + require.Nil(t, err) + + p3, err := th.App.CreatePostAsUser(th.Context, &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + CreateAt: model.GetMillis(), + Message: "message", + }, "", true) + require.Nil(t, err) + + t.Run("get acknowledgments for post list should return a map", func(t *testing.T) { + _, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.SaveAcknowledgementForPost(th.Context, p2.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser2.Id) + require.Nil(t, err) + + postList := model.NewPostList() + postList.AddPost(p1) + postList.AddOrder(p1.Id) + postList.AddPost(p2) + postList.AddOrder(p2.Id) + postList.AddPost(p3) + postList.AddOrder(p3.Id) + + acks1, err := th.App.GetAcknowledgementsForPost(p1.Id) + require.Nil(t, err) + acks2, err := th.App.GetAcknowledgementsForPost(p2.Id) + require.Nil(t, err) + + acknowledgementsMap, err := th.App.GetAcknowledgementsForPostList(postList) + require.Nil(t, err) + + expected := map[string][]*model.PostAcknowledgement{ + p1.Id: acks1, + p2.Id: acks2, + } + require.Equal(t, expected, acknowledgementsMap) + require.Len(t, acknowledgementsMap[p1.Id], 2) + require.Len(t, acknowledgementsMap[p2.Id], 1) + require.Nil(t, acknowledgementsMap[p3.Id]) + }) +} diff --git a/app/post_metadata.go b/app/post_metadata.go index 368d0803f0..347dfe8708 100644 --- a/app/post_metadata.go +++ b/app/post_metadata.go @@ -63,10 +63,15 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi if a.isPostPriorityEnabled() { priority, _ := a.GetPriorityForPostList(list) + acknowledgements, _ := a.GetAcknowledgementsForPostList(list) + for _, id := range list.Order { if _, ok := priority[id]; ok { list.Posts[id].Metadata.Priority = priority[id] } + if _, ok := acknowledgements[id]; ok { + list.Posts[id].Metadata.Acknowledgements = acknowledgements[id] + } } } @@ -139,6 +144,13 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe } else { post.Metadata.Priority = priority } + + // Post's acknowledgements if any + if acknowledgements, err := a.GetAcknowledgementsForPost(post.Id); err != nil { + mlog.Warn("Failed to get post acknowledgements for a post", mlog.String("post_id", post.Id), mlog.Err(err)) + } else { + post.Metadata.Acknowledgements = acknowledgements + } } return post diff --git a/config/client.go b/config/client.go index c6a8587865..c6e650578b 100644 --- a/config/client.go +++ b/config/client.go @@ -206,6 +206,10 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise { props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups) } + + if (license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise) && c.FeatureFlags.PostPriority { + props["PostAcknowledgements"] = "true" + } } return props diff --git a/db/migrations/migrations.list b/db/migrations/migrations.list index 1acb91f075..ba9b1103ba 100644 --- a/db/migrations/migrations.list +++ b/db/migrations/migrations.list @@ -194,6 +194,8 @@ db/migrations/mysql/000096_threads_threadteamid.down.sql db/migrations/mysql/000096_threads_threadteamid.up.sql db/migrations/mysql/000097_create_posts_priority.down.sql db/migrations/mysql/000097_create_posts_priority.up.sql +db/migrations/mysql/000098_create_post_acknowledgements.down.sql +db/migrations/mysql/000098_create_post_acknowledgements.up.sql db/migrations/postgres/000001_create_teams.down.sql db/migrations/postgres/000001_create_teams.up.sql db/migrations/postgres/000002_create_team_members.down.sql @@ -388,3 +390,5 @@ db/migrations/postgres/000096_threads_threadteamid.down.sql db/migrations/postgres/000096_threads_threadteamid.up.sql db/migrations/postgres/000097_create_posts_priority.down.sql db/migrations/postgres/000097_create_posts_priority.up.sql +db/migrations/postgres/000098_create_post_acknowledgements.down.sql +db/migrations/postgres/000098_create_post_acknowledgements.up.sql diff --git a/db/migrations/mysql/000098_create_post_acknowledgements.down.sql b/db/migrations/mysql/000098_create_post_acknowledgements.down.sql new file mode 100644 index 0000000000..2360ca5a6e --- /dev/null +++ b/db/migrations/mysql/000098_create_post_acknowledgements.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS PostAcknowledgements; diff --git a/db/migrations/mysql/000098_create_post_acknowledgements.up.sql b/db/migrations/mysql/000098_create_post_acknowledgements.up.sql new file mode 100644 index 0000000000..9eb7567a85 --- /dev/null +++ b/db/migrations/mysql/000098_create_post_acknowledgements.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS PostAcknowledgements ( + PostId varchar(26) NOT NULL, + UserId varchar(26) NOT NULL, + AcknowledgedAt bigint(20) DEFAULT NULL, + PRIMARY KEY (PostId, UserId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/postgres/000098_create_post_acknowledgements.down.sql b/db/migrations/postgres/000098_create_post_acknowledgements.down.sql new file mode 100644 index 0000000000..dc5e96624d --- /dev/null +++ b/db/migrations/postgres/000098_create_post_acknowledgements.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS postacknowledgements; diff --git a/db/migrations/postgres/000098_create_post_acknowledgements.up.sql b/db/migrations/postgres/000098_create_post_acknowledgements.up.sql new file mode 100644 index 0000000000..8f35e7b685 --- /dev/null +++ b/db/migrations/postgres/000098_create_post_acknowledgements.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS postacknowledgements( + postid VARCHAR(26) NOT NULL, + userid VARCHAR(26) NOT NULL, + acknowledgedat bigint, + PRIMARY KEY (postid, userid) +); diff --git a/i18n/en.json b/i18n/en.json index 589c90973a..d7b4627505 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -59,6 +59,18 @@ "id": "September", "translation": "September" }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "You cannot remove an acknowledgment in an archived channel." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "You cannot delete an acknowledgment after 5min have passed." + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "You cannot acknowledgment in an archived channel." + }, { "id": "api.admin.add_certificate.array.app_error", "translation": "No file under 'certificate' in request." @@ -4459,6 +4471,22 @@ "id": "api.websocket_handler.server_busy.app_error", "translation": "Server is busy, non-critical services are temporarily unavailable." }, + { + "id": "app.acknowledgement.delete.app_error", + "translation": "Unable to delete acknowledgement." + }, + { + "id": "app.acknowledgement.get.app_error", + "translation": "Unable to get acknowledgement." + }, + { + "id": "app.acknowledgement.getforpost.get.app_error", + "translation": "Unable to get acknowledgement for post." + }, + { + "id": "app.acknowledgement.save.save.app_error", + "translation": "Unable to save acknowledgement for post." + }, { "id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", "translation": "Could not decode the XML metadata information received from the Identity Provider." @@ -7927,6 +7955,14 @@ "id": "model.access.is_valid.user_id.app_error", "translation": "Invalid user id." }, + { + "id": "model.acknowledgement.is_valid.post_id.app_error", + "translation": "Invalid post id." + }, + { + "id": "model.acknowledgement.is_valid.user_id.app_error", + "translation": "Invalid user id." + }, { "id": "model.authorize.is_valid.auth_code.app_error", "translation": "Invalid authorization code." diff --git a/model/client4.go b/model/client4.go index 47a1ce366b..b05895859e 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8448,6 +8448,28 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i return newTeamMembersList, BuildResponse(r), nil } +func (c *Client4) AcknowledgePost(postId, userId string) (*PostAcknowledgement, *Response, error) { + r, err := c.DoAPIPost(c.userRoute(userId)+c.postRoute(postId)+"/ack", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ack *PostAcknowledgement + if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil { + return nil, nil, NewAppError("AcknowledgePost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return ack, BuildResponse(r), nil +} + +func (c *Client4) UnacknowledgePost(postId, userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.userRoute(userId) + c.postRoute(postId) + "/ack") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) { r, err := c.DoAPIPost(c.ldapRoute()+"/users/"+userID+"/group_sync_memberships", "") if err != nil { diff --git a/model/config.go b/model/config.go index be7a88b61d..a538f7032e 100644 --- a/model/config.go +++ b/model/config.go @@ -845,7 +845,7 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { } if s.PostPriority == nil { - s.PostPriority = NewBool(false) + s.PostPriority = NewBool(true) } } diff --git a/model/post_acknowledgement.go b/model/post_acknowledgement.go new file mode 100644 index 0000000000..227a678e6b --- /dev/null +++ b/model/post_acknowledgement.go @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "net/http" + +type PostAcknowledgement struct { + UserId string `json:"user_id"` + PostId string `json:"post_id"` + AcknowledgedAt int64 `json:"acknowledged_at"` +} + +func (o *PostAcknowledgement) IsValid() *AppError { + if !IsValidId(o.UserId) { + return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.user_id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest) + } + + if !IsValidId(o.PostId) { + return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.post_id.app_error", nil, "post_id="+o.PostId, http.StatusBadRequest) + } + + return nil +} diff --git a/model/post_metadata.go b/model/post_metadata.go index 3730d06f55..ee49eb4f48 100644 --- a/model/post_metadata.go +++ b/model/post_metadata.go @@ -23,8 +23,11 @@ type PostMetadata struct { // Reactions holds reactions made to the post. Reactions []*Reaction `json:"reactions,omitempty"` - // Reactions holds reactions made to the post. + // Priority holds info about priority settings for the post. Priority *PostPriority `json:"priority,omitempty"` + + // Acknowledgements holds acknowledgements made by users to the post + Acknowledgements []*PostAcknowledgement `json:"acknowledgements,omitempty"` } type PostImage struct { @@ -57,6 +60,9 @@ func (p *PostMetadata) Copy() *PostMetadata { reactionsCopy := make([]*Reaction, len(p.Reactions)) copy(reactionsCopy, p.Reactions) + acknowledgementsCopy := make([]*PostAcknowledgement, len(p.Acknowledgements)) + copy(acknowledgementsCopy, p.Acknowledgements) + var postPriorityCopy *PostPriority if p.Priority != nil { postPriorityCopy = &PostPriority{ @@ -69,11 +75,12 @@ func (p *PostMetadata) Copy() *PostMetadata { } return &PostMetadata{ - Embeds: embedsCopy, - Emojis: emojisCopy, - Files: filesCopy, - Images: imagesCopy, - Reactions: reactionsCopy, - Priority: postPriorityCopy, + Embeds: embedsCopy, + Emojis: emojisCopy, + Files: filesCopy, + Images: imagesCopy, + Reactions: reactionsCopy, + Priority: postPriorityCopy, + Acknowledgements: acknowledgementsCopy, } } diff --git a/model/websocket_message.go b/model/websocket_message.go index 9cd3892453..8cd2ad1961 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -76,6 +76,8 @@ const ( WebsocketEventThreadFollowChanged = "thread_follow_changed" WebsocketEventThreadReadChanged = "thread_read_changed" WebsocketFirstAdminVisitMarketplaceStatusReceived = "first_admin_visit_marketplace_status_received" + WebsocketEventAcknowledgementAdded = "post_acknowledgement_added" + WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed" ) type WebSocketMessage interface { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 3ab5c4715d..38e136d493 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -37,6 +37,7 @@ type OpenTracingLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostAcknowledgementStore store.PostAcknowledgementStore PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore @@ -132,6 +133,10 @@ func (s *OpenTracingLayer) Post() store.PostStore { return s.PostStore } +func (s *OpenTracingLayer) PostAcknowledgement() store.PostAcknowledgementStore { + return s.PostAcknowledgementStore +} + func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore { return s.PostPriorityStore } @@ -306,6 +311,11 @@ type OpenTracingLayerPostStore struct { Root *OpenTracingLayer } +type OpenTracingLayerPostAcknowledgementStore struct { + store.PostAcknowledgementStore + Root *OpenTracingLayer +} + type OpenTracingLayerPostPriorityStore struct { store.PostPriorityStore Root *OpenTracingLayer @@ -6544,6 +6554,96 @@ func (s *OpenTracingLayerPostStore) Update(newPost *model.Post, oldPost *model.P return result, err } +func (s *OpenTracingLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Delete") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.PostAcknowledgementStore.Delete(acknowledgement) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + +func (s *OpenTracingLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Get") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostAcknowledgementStore.Get(postID, userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPost") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostAcknowledgementStore.GetForPost(postID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPosts") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostAcknowledgementStore.GetForPosts(postIds) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Save") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost") @@ -12591,6 +12691,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer { newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore} newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore} diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 8dc782fce7..1da922dec1 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -40,6 +40,7 @@ type RetryLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostAcknowledgementStore store.PostAcknowledgementStore PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore @@ -135,6 +136,10 @@ func (s *RetryLayer) Post() store.PostStore { return s.PostStore } +func (s *RetryLayer) PostAcknowledgement() store.PostAcknowledgementStore { + return s.PostAcknowledgementStore +} + func (s *RetryLayer) PostPriority() store.PostPriorityStore { return s.PostPriorityStore } @@ -309,6 +314,11 @@ type RetryLayerPostStore struct { Root *RetryLayer } +type RetryLayerPostAcknowledgementStore struct { + store.PostAcknowledgementStore + Root *RetryLayer +} + type RetryLayerPostPriorityStore struct { store.PostPriorityStore Root *RetryLayer @@ -7420,6 +7430,111 @@ func (s *RetryLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) ( } +func (s *RetryLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error { + + tries := 0 + for { + err := s.PostAcknowledgementStore.Delete(acknowledgement) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) { + + tries := 0 + for { + result, err := s.PostAcknowledgementStore.Get(postID, userID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) { + + tries := 0 + for { + result, err := s.PostAcknowledgementStore.GetForPost(postID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) { + + tries := 0 + for { + result, err := s.PostAcknowledgementStore.GetForPosts(postIds) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + +func (s *RetryLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) { + + tries := 0 + for { + result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { tries := 0 @@ -14355,6 +14470,7 @@ func New(childStore store.Store) *RetryLayer { newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore} newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore} diff --git a/store/retrylayer/retrylayer_test.go b/store/retrylayer/retrylayer_test.go index b45bc7c561..7efbfa899a 100644 --- a/store/retrylayer/retrylayer_test.go +++ b/store/retrylayer/retrylayer_test.go @@ -55,6 +55,7 @@ func genStore() *mocks.Store { mock.On("Webhook").Return(&mocks.WebhookStore{}) mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{}) mock.On("PostPriority").Return(&mocks.PostPriorityStore{}) + mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{}) return mock } diff --git a/store/sqlstore/post_acknowledgements_store.go b/store/sqlstore/post_acknowledgements_store.go new file mode 100644 index 0000000000..d3de3addea --- /dev/null +++ b/store/sqlstore/post_acknowledgements_store.go @@ -0,0 +1,144 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "database/sql" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/store" + sq "github.com/mattermost/squirrel" + "github.com/pkg/errors" +) + +type SqlPostAcknowledgementStore struct { + *SqlStore +} + +func newSqlPostAcknowledgementStore(sqlStore *SqlStore) store.PostAcknowledgementStore { + return &SqlPostAcknowledgementStore{sqlStore} +} + +func (s *SqlPostAcknowledgementStore) Get(postID, userID string) (*model.PostAcknowledgement, error) { + query := s.getQueryBuilder(). + Select("PostId", "UserId", "AcknowledgedAt"). + From("PostAcknowledgements"). + Where(sq.And{ + sq.Eq{"PostId": postID}, + sq.Eq{"UserId": userID}, + sq.NotEq{"AcknowledgedAt": 0}, + }) + + var acknowledgement model.PostAcknowledgement + err := s.GetReplicaX().GetBuilder(&acknowledgement, query) + if err != nil { + if err == sql.ErrNoRows { + return nil, store.NewErrNotFound("PostAcknowledgement", postID) + } + + return nil, err + } + + return &acknowledgement, nil +} + +func (s *SqlPostAcknowledgementStore) Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) { + if acknowledgedAt == 0 { + acknowledgedAt = model.GetMillis() + } + + acknowledgement := &model.PostAcknowledgement{ + UserId: userID, + PostId: postID, + AcknowledgedAt: acknowledgedAt, + } + + if err := acknowledgement.IsValid(); err != nil { + return nil, err + } + + query := s.getQueryBuilder(). + Insert("PostAcknowledgements"). + Columns("PostId", "UserId", "AcknowledgedAt"). + Values(acknowledgement.PostId, acknowledgement.UserId, acknowledgement.AcknowledgedAt) + + if s.DriverName() == model.DatabaseDriverMysql { + query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE AcknowledgedAt = ?", acknowledgement.AcknowledgedAt)) + } else { + query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET AcknowledgedAt = ?", acknowledgement.AcknowledgedAt)) + } + + _, err := s.GetMasterX().ExecBuilder(query) + if err != nil { + return nil, err + } + + return acknowledgement, nil +} + +func (s *SqlPostAcknowledgementStore) Delete(ack *model.PostAcknowledgement) error { + query := s.getQueryBuilder(). + Update("PostAcknowledgements"). + Set("AcknowledgedAt", 0). + Where(sq.And{ + sq.Eq{"PostId": ack.PostId}, + sq.Eq{"UserId": ack.UserId}, + }) + + _, err := s.GetMasterX().ExecBuilder(query) + if err != nil { + return err + } + + return nil +} + +func (s *SqlPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) { + var acknowledgements []*model.PostAcknowledgement + + query := s.getQueryBuilder(). + Select("PostId", "UserId", "AcknowledgedAt"). + From("PostAcknowledgements"). + Where(sq.And{ + sq.NotEq{"AcknowledgedAt": 0}, + sq.Eq{"PostId": postID}, + }) + + err := s.GetReplicaX().SelectBuilder(&acknowledgements, query) + if err != nil { + return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for postID=%s", postID) + } + + return acknowledgements, nil +} + +func (s *SqlPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) { + var acknowledgements []*model.PostAcknowledgement + + perPage := 200 + for i := 0; i < len(postIds); i += perPage { + j := i + perPage + if len(postIds) < j { + j = len(postIds) + } + + query := s.getQueryBuilder(). + Select("PostId", "UserId", "AcknowledgedAt"). + From("PostAcknowledgements"). + Where(sq.And{ + sq.Eq{"PostId": postIds[i:j]}, + sq.NotEq{"AcknowledgedAt": 0}, + }) + + var acknowledgementsBatch []*model.PostAcknowledgement + err := s.GetReplicaX().SelectBuilder(&acknowledgementsBatch, query) + if err != nil { + return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for post list") + } + + acknowledgements = append(acknowledgements, acknowledgementsBatch...) + } + + return acknowledgements, nil +} diff --git a/store/sqlstore/post_acknowledgements_store_test.go b/store/sqlstore/post_acknowledgements_store_test.go new file mode 100644 index 0000000000..7de92cfef4 --- /dev/null +++ b/store/sqlstore/post_acknowledgements_store_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v6/store/storetest" +) + +func TestPostAcknowledgementsStore(t *testing.T) { + StoreTestWithSqlStore(t, storetest.TestPostAcknowledgementsStore) +} diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 0c5aad077f..6a0cdbc51a 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -110,6 +110,7 @@ type SqlStoreStores struct { sharedchannel store.SharedChannelStore notifyAdmin store.NotifyAdminStore postPriority store.PostPriorityStore + postAcknowledgement store.PostAcknowledgementStore } type SqlStore struct { @@ -216,6 +217,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS store.stores.productNotices = newSqlProductNoticesStore(store) store.stores.notifyAdmin = newSqlNotifyAdminStore(store) store.stores.postPriority = newSqlPostPriorityStore(store) + store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store) store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures() @@ -961,6 +963,10 @@ func (ss *SqlStore) PostPriority() store.PostPriorityStore { return ss.stores.postPriority } +func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore { + return ss.stores.postAcknowledgement +} + func (ss *SqlStore) DropAllTables() { if ss.DriverName() == model.DatabaseDriverPostgres { ss.masterX.Exec(`DO diff --git a/store/store.go b/store/store.go index c99b1fa7c2..4b3f4a94ea 100644 --- a/store/store.go +++ b/store/store.go @@ -85,6 +85,7 @@ type Store interface { Context() context.Context NotifyAdmin() NotifyAdminStore PostPriority() PostPriorityStore + PostAcknowledgement() PostAcknowledgementStore } type RetentionPolicyStore interface { @@ -978,6 +979,14 @@ type PostPriorityStore interface { GetForPosts(ids []string) ([]*model.PostPriority, error) } +type PostAcknowledgementStore interface { + Get(postID, userID string) (*model.PostAcknowledgement, error) + GetForPost(postID string) ([]*model.PostAcknowledgement, error) + GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) + Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) + Delete(acknowledgement *model.PostAcknowledgement) error +} + // ChannelSearchOpts contains options for searching channels. // // NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. diff --git a/store/storetest/mocks/PostAcknowledgementStore.go b/store/storetest/mocks/PostAcknowledgementStore.go new file mode 100644 index 0000000000..ad130137da --- /dev/null +++ b/store/storetest/mocks/PostAcknowledgementStore.go @@ -0,0 +1,121 @@ +// Code generated by mockery v2.10.4. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import ( + model "github.com/mattermost/mattermost-server/v6/model" + mock "github.com/stretchr/testify/mock" +) + +// PostAcknowledgementStore is an autogenerated mock type for the PostAcknowledgementStore type +type PostAcknowledgementStore struct { + mock.Mock +} + +// Delete provides a mock function with given fields: acknowledgement +func (_m *PostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error { + ret := _m.Called(acknowledgement) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.PostAcknowledgement) error); ok { + r0 = rf(acknowledgement) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: postID, userID +func (_m *PostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) { + ret := _m.Called(postID, userID) + + var r0 *model.PostAcknowledgement + if rf, ok := ret.Get(0).(func(string, string) *model.PostAcknowledgement); ok { + r0 = rf(postID, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.PostAcknowledgement) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(postID, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetForPost provides a mock function with given fields: postID +func (_m *PostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) { + ret := _m.Called(postID) + + var r0 []*model.PostAcknowledgement + if rf, ok := ret.Get(0).(func(string) []*model.PostAcknowledgement); ok { + r0 = rf(postID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.PostAcknowledgement) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(postID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetForPosts provides a mock function with given fields: postIds +func (_m *PostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) { + ret := _m.Called(postIds) + + var r0 []*model.PostAcknowledgement + if rf, ok := ret.Get(0).(func([]string) []*model.PostAcknowledgement); ok { + r0 = rf(postIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.PostAcknowledgement) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(postIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: postID, userID, acknowledgedAt +func (_m *PostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) { + ret := _m.Called(postID, userID, acknowledgedAt) + + var r0 *model.PostAcknowledgement + if rf, ok := ret.Get(0).(func(string, string, int64) *model.PostAcknowledgement); ok { + r0 = rf(postID, userID, acknowledgedAt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.PostAcknowledgement) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, int64) error); ok { + r1 = rf(postID, userID, acknowledgedAt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index 1d8e8ac326..cfa20ca980 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -475,6 +475,22 @@ func (_m *Store) Post() store.PostStore { return r0 } +// PostAcknowledgement provides a mock function with given fields: +func (_m *Store) PostAcknowledgement() store.PostAcknowledgementStore { + ret := _m.Called() + + var r0 store.PostAcknowledgementStore + if rf, ok := ret.Get(0).(func() store.PostAcknowledgementStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.PostAcknowledgementStore) + } + } + + return r0 +} + // PostPriority provides a mock function with given fields: func (_m *Store) PostPriority() store.PostPriorityStore { ret := _m.Called() diff --git a/store/storetest/post_acknowledgements_store.go b/store/storetest/post_acknowledgements_store.go new file mode 100644 index 0000000000..ac751119bf --- /dev/null +++ b/store/storetest/post_acknowledgements_store.go @@ -0,0 +1,181 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package storetest + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/store" + "github.com/stretchr/testify/require" +) + +func TestPostAcknowledgementsStore(t *testing.T, ss store.Store, s SqlStore) { + t.Run("Save", func(t *testing.T) { testPostAcknowledgementsStoreSave(t, ss) }) + t.Run("GetForPost", func(t *testing.T) { testPostAcknowledgementsStoreGetForPost(t, ss) }) + t.Run("GetForPosts", func(t *testing.T) { testPostAcknowledgementsStoreGetForPosts(t, ss) }) +} + +func testPostAcknowledgementsStoreSave(t *testing.T, ss store.Store) { + userId1 := model.NewId() + + p1 := model.Post{} + p1.ChannelId = model.NewId() + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + _, err := ss.Post().Save(&p1) + require.NoError(t, err) + + t.Run("consecutive saves should just update the acknowledged at", func(t *testing.T) { + _, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0) + require.NoError(t, err) + + _, err = ss.PostAcknowledgement().Save(p1.Id, userId1, 0) + require.NoError(t, err) + + ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0) + require.NoError(t, err) + + acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1}) + }) +} + +func testPostAcknowledgementsStoreGetForPost(t *testing.T, ss store.Store) { + userId1 := model.NewId() + userId2 := model.NewId() + userId3 := model.NewId() + + p1 := model.Post{} + p1.ChannelId = model.NewId() + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + _, err := ss.Post().Save(&p1) + require.NoError(t, err) + + t.Run("get acknowledgements for post", func(t *testing.T) { + ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0) + require.NoError(t, err) + ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0) + require.NoError(t, err) + ack3, err := ss.PostAcknowledgement().Save(p1.Id, userId3, 0) + require.NoError(t, err) + + acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3}) + + err = ss.PostAcknowledgement().Delete(ack1) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3}) + + err = ss.PostAcknowledgement().Delete(ack2) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3}) + + err = ss.PostAcknowledgement().Delete(ack3) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id) + require.NoError(t, err) + require.Empty(t, acknowledgements) + }) +} + +func testPostAcknowledgementsStoreGetForPosts(t *testing.T, ss store.Store) { + userId1 := model.NewId() + userId2 := model.NewId() + userId3 := model.NewId() + + p1 := model.Post{} + p1.ChannelId = model.NewId() + p1.UserId = model.NewId() + p1.Message = NewTestId() + p1.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString("important"), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + p2 := model.Post{} + p2.ChannelId = model.NewId() + p2.UserId = model.NewId() + p2.Message = NewTestId() + p2.Metadata = &model.PostMetadata{ + Priority: &model.PostPriority{ + Priority: model.NewString(""), + RequestedAck: model.NewBool(true), + PersistentNotifications: model.NewBool(false), + }, + } + _, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2}) + require.NoError(t, err) + require.Equal(t, -1, errIdx) + + t.Run("get acknowledgements for post", func(t *testing.T) { + ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0) + require.NoError(t, err) + ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0) + require.NoError(t, err) + ack3, err := ss.PostAcknowledgement().Save(p2.Id, userId2, 0) + require.NoError(t, err) + ack4, err := ss.PostAcknowledgement().Save(p2.Id, userId3, 0) + require.NoError(t, err) + + acknowledgements, err := ss.PostAcknowledgement().GetForPosts([]string{p1.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2}) + + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p2.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4}) + + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3, ack4}) + + err = ss.PostAcknowledgement().Delete(ack1) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3, ack4}) + + err = ss.PostAcknowledgement().Delete(ack2) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4}) + + err = ss.PostAcknowledgement().Delete(ack3) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id}) + require.NoError(t, err) + require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack4}) + + err = ss.PostAcknowledgement().Delete(ack4) + require.NoError(t, err) + acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id}) + require.NoError(t, err) + require.Empty(t, acknowledgements) + }) +} diff --git a/store/storetest/store.go b/store/storetest/store.go index 4f785c0426..93a85d3ab1 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -57,6 +57,7 @@ type Store struct { context context.Context NotifyAdminStore mocks.NotifyAdminStore PostPriorityStore mocks.PostPriorityStore + PostAcknowledgementStore mocks.PostAcknowledgementStore } func (s *Store) SetContext(context context.Context) { s.context = context } @@ -102,17 +103,20 @@ func (s *Store) Group() store.GroupStore { return &s.GroupStore func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore } func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore } func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore } -func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ } -func (s *Store) Close() { /* do nothing */ } -func (s *Store) LockToMaster() { /* do nothing */ } -func (s *Store) UnlockFromMaster() { /* do nothing */ } -func (s *Store) DropAllTables() { /* do nothing */ } -func (s *Store) GetDbVersion(bool) (string, error) { return "", nil } -func (s *Store) GetInternalMasterDB() *sql.DB { return nil } -func (s *Store) GetInternalReplicaDB() *sql.DB { return nil } -func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil } -func (s *Store) RecycleDBConnections(time.Duration) {} -func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil } +func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore { + return &s.PostAcknowledgementStore +} +func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ } +func (s *Store) Close() { /* do nothing */ } +func (s *Store) LockToMaster() { /* do nothing */ } +func (s *Store) UnlockFromMaster() { /* do nothing */ } +func (s *Store) DropAllTables() { /* do nothing */ } +func (s *Store) GetDbVersion(bool) (string, error) { return "", nil } +func (s *Store) GetInternalMasterDB() *sql.DB { return nil } +func (s *Store) GetInternalReplicaDB() *sql.DB { return nil } +func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil } +func (s *Store) RecycleDBConnections(time.Duration) {} +func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil } func (s *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) { return []model.AppliedMigration{}, nil } @@ -161,5 +165,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.SharedChannelStore, &s.NotifyAdminStore, &s.PostPriorityStore, + &s.PostAcknowledgementStore, ) } diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index ba5d2436e7..3395ab8b45 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -36,6 +36,7 @@ type TimerLayer struct { OAuthStore store.OAuthStore PluginStore store.PluginStore PostStore store.PostStore + PostAcknowledgementStore store.PostAcknowledgementStore PostPriorityStore store.PostPriorityStore PreferenceStore store.PreferenceStore ProductNoticesStore store.ProductNoticesStore @@ -131,6 +132,10 @@ func (s *TimerLayer) Post() store.PostStore { return s.PostStore } +func (s *TimerLayer) PostAcknowledgement() store.PostAcknowledgementStore { + return s.PostAcknowledgementStore +} + func (s *TimerLayer) PostPriority() store.PostPriorityStore { return s.PostPriorityStore } @@ -305,6 +310,11 @@ type TimerLayerPostStore struct { Root *TimerLayer } +type TimerLayerPostAcknowledgementStore struct { + store.PostAcknowledgementStore + Root *TimerLayer +} + type TimerLayerPostPriorityStore struct { store.PostPriorityStore Root *TimerLayer @@ -5917,6 +5927,86 @@ func (s *TimerLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) ( return result, err } +func (s *TimerLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error { + start := time.Now() + + err := s.PostAcknowledgementStore.Delete(acknowledgement) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Delete", success, elapsed) + } + return err +} + +func (s *TimerLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) { + start := time.Now() + + result, err := s.PostAcknowledgementStore.Get(postID, userID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Get", success, elapsed) + } + return result, err +} + +func (s *TimerLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) { + start := time.Now() + + result, err := s.PostAcknowledgementStore.GetForPost(postID) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPost", success, elapsed) + } + return result, err +} + +func (s *TimerLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) { + start := time.Now() + + result, err := s.PostAcknowledgementStore.GetForPosts(postIds) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPosts", success, elapsed) + } + return result, err +} + +func (s *TimerLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) { + start := time.Now() + + result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Save", success, elapsed) + } + return result, err +} + func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) { start := time.Now() @@ -11344,6 +11434,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore} newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore} newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore} + newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore} newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore} newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore} newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}