From aac34f6db4c5b0100ba27686b0f7d792d1af5ca8 Mon Sep 17 00:00:00 2001 From: Ben Cooke Date: Mon, 9 Jun 2025 15:41:07 -0400 Subject: [PATCH] [MM-64360] Data retention: optionally preserve pinned posts (#31165) * add new config to preserve pinned posts during data retention * more graceful error if the pinned post was a reply in a deleted thread --- server/channels/app/syncables_test.go | 16 +- .../channels/store/retrylayer/retrylayer.go | 16 +- .../sqlstore/channel_member_history_store.go | 8 +- server/channels/store/sqlstore/post_store.go | 19 +- .../channels/store/sqlstore/thread_store.go | 16 +- server/channels/store/store.go | 8 +- .../storetest/channel_member_history_store.go | 21 +- .../channels/store/storetest/group_store.go | 7 +- .../mocks/ChannelMemberHistoryStore.go | 22 +- .../store/storetest/mocks/PostStore.go | 22 +- .../store/storetest/mocks/ThreadStore.go | 44 ++-- server/channels/store/storetest/post_store.go | 193 +++++++++++++++++- .../store/storetest/preference_store.go | 6 +- .../store/storetest/reaction_store.go | 6 +- .../channels/store/storetest/thread_store.go | 36 +++- .../channels/store/timerlayer/timerlayer.go | 16 +- server/public/model/config.go | 5 + server/public/model/data_retention_policy.go | 7 + .../components/error_page/error_message.tsx | 10 + .../src/components/error_page/error_title.tsx | 8 + .../src/components/permalink_view/actions.ts | 5 + webapp/channels/src/i18n/en.json | 2 + webapp/channels/src/utils/constants.tsx | 1 + 23 files changed, 388 insertions(+), 106 deletions(-) diff --git a/server/channels/app/syncables_test.go b/server/channels/app/syncables_test.go index 3fbf97ded5..c1266cb2e6 100644 --- a/server/channels/app/syncables_test.go +++ b/server/channels/app/syncables_test.go @@ -303,9 +303,15 @@ func TestCreateDefaultMemberships(t *testing.T) { timeAfterLeaving := model.GetMillis() + 1 + retentionPolicyBatchConfigs := model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: timeBeforeLeaving, + Limit: 1000, + } + // Purging channelmemberhistory doesn't re-add user to channel deletedCount, _, nErr := th.App.Srv().Store().ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - 0, timeBeforeLeaving, 1000, model.RetentionPolicyCursor{}) + retentionPolicyBatchConfigs, model.RetentionPolicyCursor{}) if nErr != nil { t.Errorf("error permanently deleting channelmemberhistory: %s", nErr.Error()) } @@ -321,9 +327,15 @@ func TestCreateDefaultMemberships(t *testing.T) { t.Error("Expected channel member to remain deleted") } + retentionPolicyBatchConfigs = model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: timeAfterLeaving, + Limit: 1000, + } + // Purging channelmemberhistory doesn't re-add user to channel deletedCount, _, nErr = th.App.Srv().Store().ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - 0, timeAfterLeaving, 1000, model.RetentionPolicyCursor{}) + retentionPolicyBatchConfigs, model.RetentionPolicyCursor{}) if nErr != nil { t.Errorf("error permanently deleting channelmemberhistory: %s", nErr.Error()) } diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index 2c67daa4b3..2fc01b7632 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -3656,11 +3656,11 @@ func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64 } -func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *RetryLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { tries := 0 for { - result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) if err == nil { return result, resultVar1, nil } @@ -8360,11 +8360,11 @@ func (s *RetryLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) ( } -func (s *RetryLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *RetryLayerPostStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { tries := 0 for { - result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) if err == nil { return result, resultVar1, nil } @@ -13796,11 +13796,11 @@ func (s *RetryLayerThreadStore) MarkAsRead(userID string, threadID string, times } -func (s *RetryLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *RetryLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { tries := 0 for { - result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) if err == nil { return result, resultVar1, nil } @@ -13817,11 +13817,11 @@ func (s *RetryLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int } -func (s *RetryLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *RetryLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { tries := 0 for { - result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs, cursor) if err == nil { return result, resultVar1, nil } diff --git a/server/channels/store/sqlstore/channel_member_history_store.go b/server/channels/store/sqlstore/channel_member_history_store.go index 109eeb1d03..5d1d09fc2c 100644 --- a/server/channels/store/sqlstore/channel_member_history_store.go +++ b/server/channels/store/sqlstore/channel_member_history_store.go @@ -229,7 +229,7 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64 // PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by // the global or a granular retention policy. // See `genericPermanentDeleteBatchForRetentionPolicies` for details. -func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { builder := s.getQueryBuilder(). Select("ChannelMemberHistory.ChannelId, ChannelMemberHistory.UserId, ChannelMemberHistory.JoinTime"). From("ChannelMemberHistory") @@ -239,9 +239,9 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(n TimeColumn: "LeaveTime", PrimaryKeys: []string{"ChannelId", "UserId", "JoinTime"}, ChannelIDTable: "ChannelMemberHistory", - NowMillis: now, - GlobalPolicyEndTime: globalPolicyEndTime, - Limit: limit, + NowMillis: retentionPolicyBatchConfigs.Now, + GlobalPolicyEndTime: retentionPolicyBatchConfigs.GlobalPolicyEndTime, + Limit: retentionPolicyBatchConfigs.Limit, StoreDeletedIds: false, }, s.SqlStore, cursor) } diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index c4bf9d34e3..b1b823c86c 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -2681,19 +2681,30 @@ func (s *SqlPostStore) GetPostsBatchForIndexing(startTime int64, startPostID str // PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by // the global or a granular retention policy. // See `genericPermanentDeleteBatchForRetentionPolicies` for details. -func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *SqlPostStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { builder := s.getQueryBuilder(). Select("Posts.Id"). From("Posts") + + if retentionPolicyBatchConfigs.PreservePinnedPosts { + builder = builder.Where(sq.Or{ + sq.Eq{"Posts.IsPinned": false}, + sq.And{ + sq.Eq{"Posts.IsPinned": true}, + sq.Gt{"Posts.DeleteAt": 0}, + }, + }) + } + return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{ BaseBuilder: builder, Table: "Posts", TimeColumn: "CreateAt", PrimaryKeys: []string{"Id"}, ChannelIDTable: "Posts", - NowMillis: now, - GlobalPolicyEndTime: globalPolicyEndTime, - Limit: limit, + NowMillis: retentionPolicyBatchConfigs.Now, + GlobalPolicyEndTime: retentionPolicyBatchConfigs.GlobalPolicyEndTime, + Limit: retentionPolicyBatchConfigs.Limit, StoreDeletedIds: true, }, s.SqlStore, cursor) } diff --git a/server/channels/store/sqlstore/thread_store.go b/server/channels/store/sqlstore/thread_store.go index d46a2e73fc..de3c421450 100644 --- a/server/channels/store/sqlstore/thread_store.go +++ b/server/channels/store/sqlstore/thread_store.go @@ -987,7 +987,7 @@ func (s *SqlThreadStore) maintainMembershipTx(trx *sqlxTxWrapper, userID, postID // PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by // the global or a granular retention policy. // See `genericPermanentDeleteBatchForRetentionPolicies` for details. -func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { builder := s.getQueryBuilder(). Select("Threads.PostId"). From("Threads") @@ -997,9 +997,9 @@ func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPol TimeColumn: "LastReplyAt", PrimaryKeys: []string{"PostId"}, ChannelIDTable: "Threads", - NowMillis: now, - GlobalPolicyEndTime: globalPolicyEndTime, - Limit: limit, + NowMillis: retentionPolicyBatchConfigs.Now, + GlobalPolicyEndTime: retentionPolicyBatchConfigs.GlobalPolicyEndTime, + Limit: retentionPolicyBatchConfigs.Limit, StoreDeletedIds: false, }, s.SqlStore, cursor) } @@ -1007,7 +1007,7 @@ func (s *SqlThreadStore) PermanentDeleteBatchForRetentionPolicies(now, globalPol // PermanentDeleteBatchThreadMembershipsForRetentionPolicies deletes a batch of records // which are affected by the global or a granular retention policy. // See `genericPermanentDeleteBatchForRetentionPolicies` for details. -func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { builder := s.getQueryBuilder(). Select("ThreadMemberships.PostId"). From("ThreadMemberships"). @@ -1018,9 +1018,9 @@ func (s *SqlThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolici TimeColumn: "LastUpdated", PrimaryKeys: []string{"PostId"}, ChannelIDTable: "Threads", - NowMillis: now, - GlobalPolicyEndTime: globalPolicyEndTime, - Limit: limit, + NowMillis: retentionPolicyBatchConfigs.Now, + GlobalPolicyEndTime: retentionPolicyBatchConfigs.GlobalPolicyEndTime, + Limit: retentionPolicyBatchConfigs.Limit, StoreDeletedIds: false, }, s.SqlStore, cursor) } diff --git a/server/channels/store/store.go b/server/channels/store/store.go index 321972d675..cbb31bc82d 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -327,7 +327,7 @@ type ChannelMemberHistoryStore interface { LogLeaveEvent(userID string, channelID string, leaveTime int64) error GetUsersInChannelDuring(startTime int64, endTime int64, channelID []string) ([]*model.ChannelMemberHistoryResult, error) GetChannelsWithActivityDuring(startTime int64, endTime int64) ([]string, error) - PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) + PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) DeleteOrphanedRows(limit int) (deleted int64, err error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) GetChannelsLeftSince(userID string, since int64) ([]string, error) @@ -355,8 +355,8 @@ type ThreadStore interface { GetMembershipForUser(userID, postID string) (*model.ThreadMembership, error) DeleteMembershipForUser(userID, postID string) error MaintainMembership(userID, postID string, opts ThreadMembershipOpts) (*model.ThreadMembership, error) - PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) - PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) + PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) + PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) DeleteOrphanedRows(limit int) (deleted int64, err error) GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error) DeleteMembershipsForChannel(userID, channelID string) error @@ -401,7 +401,7 @@ type PostStore interface { GetPostsByIds(postIds []string) ([]*model.Post, error) GetEditHistoryForPost(postID string) ([]*model.Post, error) GetPostsBatchForIndexing(startTime int64, startPostID string, limit int) ([]*model.PostForIndexing, error) - PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) + PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) GetOldest() (*model.Post, error) GetMaxPostSize() int diff --git a/server/channels/store/storetest/channel_member_history_store.go b/server/channels/store/storetest/channel_member_history_store.go index e6bbae7635..aaae0e34ab 100644 --- a/server/channels/store/storetest/channel_member_history_store.go +++ b/server/channels/store/storetest/channel_member_history_store.go @@ -358,8 +358,11 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, rctx request.CTX, ss st tableDataTruncated := false for !tableDataTruncated { var count int64 - count, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - 0, model.GetMillis(), 1000, model.RetentionPolicyCursor{}) + count, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: model.GetMillis(), + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err, "Failed to truncate ChannelMemberHistory contents") tableDataTruncated = count == int64(0) } @@ -493,8 +496,11 @@ func testPermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store.Store) { assert.Len(t, channelMembers, 2) // the permanent delete should delete at least one record - rowsDeleted, _, err := ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - 0, leaveTime+1, math.MaxInt64, model.RetentionPolicyCursor{}) + rowsDeleted, _, err := ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: leaveTime + 1, + Limit: math.MaxInt64, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) assert.NotEqual(t, int64(0), rowsDeleted) @@ -540,8 +546,11 @@ func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx request.CTX require.NoError(t, err) nowMillis := leaveTime + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) result, err := ss.ChannelMemberHistory().GetUsersInChannelDuring(joinTime, leaveTime, []string{channel.Id}) require.NoError(t, err) diff --git a/server/channels/store/storetest/group_store.go b/server/channels/store/storetest/group_store.go index 0f3d7a3c20..1a7c527e5b 100644 --- a/server/channels/store/storetest/group_store.go +++ b/server/channels/store/storetest/group_store.go @@ -2345,8 +2345,11 @@ func testChannelMembersToAdd(t *testing.T, rctx request.CTX, ss store.Store) { require.Empty(t, channelMembers) // Purging ChannelMemberHistory re-returns the result - _, _, nErr = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies( - 0, model.GetMillis()+1, 100, model.RetentionPolicyCursor{}) + _, _, nErr = ss.ChannelMemberHistory().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: model.GetMillis() + 1, + Limit: 100, + }, model.RetentionPolicyCursor{}) require.NoError(t, nErr) channelMembers, err = ss.Group().ChannelMembersToAdd(0, nil, false) require.NoError(t, err) diff --git a/server/channels/store/storetest/mocks/ChannelMemberHistoryStore.go b/server/channels/store/storetest/mocks/ChannelMemberHistoryStore.go index c1327afaa3..90988b5309 100644 --- a/server/channels/store/storetest/mocks/ChannelMemberHistoryStore.go +++ b/server/channels/store/storetest/mocks/ChannelMemberHistoryStore.go @@ -196,9 +196,9 @@ func (_m *ChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit i return r0, r1 } -// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor -func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { - ret := _m.Called(now, globalPolicyEndTime, limit, cursor) +// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: retentionPolicyBatchConfigs, cursor +func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { + ret := _m.Called(retentionPolicyBatchConfigs, cursor) if len(ret) == 0 { panic("no return value specified for PermanentDeleteBatchForRetentionPolicies") @@ -207,23 +207,23 @@ func (_m *ChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(no var r0 int64 var r1 model.RetentionPolicyCursor var r2 error - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { - return rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { + return rf(retentionPolicyBatchConfigs, cursor) } - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok { - r0 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) int64); ok { + r0 = rf(retentionPolicyBatchConfigs, cursor) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { - r1 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(1).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { + r1 = rf(retentionPolicyBatchConfigs, cursor) } else { r1 = ret.Get(1).(model.RetentionPolicyCursor) } - if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok { - r2 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(2).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) error); ok { + r2 = rf(retentionPolicyBatchConfigs, cursor) } else { r2 = ret.Error(2) } diff --git a/server/channels/store/storetest/mocks/PostStore.go b/server/channels/store/storetest/mocks/PostStore.go index ce8a439108..d1ecf4dcc6 100644 --- a/server/channels/store/storetest/mocks/PostStore.go +++ b/server/channels/store/storetest/mocks/PostStore.go @@ -1120,9 +1120,9 @@ func (_m *PostStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, er return r0, r1 } -// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor -func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { - ret := _m.Called(now, globalPolicyEndTime, limit, cursor) +// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: retentionPolicyBatchConfigs, cursor +func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { + ret := _m.Called(retentionPolicyBatchConfigs, cursor) if len(ret) == 0 { panic("no return value specified for PermanentDeleteBatchForRetentionPolicies") @@ -1131,23 +1131,23 @@ func (_m *PostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalP var r0 int64 var r1 model.RetentionPolicyCursor var r2 error - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { - return rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { + return rf(retentionPolicyBatchConfigs, cursor) } - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok { - r0 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) int64); ok { + r0 = rf(retentionPolicyBatchConfigs, cursor) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { - r1 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(1).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { + r1 = rf(retentionPolicyBatchConfigs, cursor) } else { r1 = ret.Get(1).(model.RetentionPolicyCursor) } - if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok { - r2 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(2).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) error); ok { + r2 = rf(retentionPolicyBatchConfigs, cursor) } else { r2 = ret.Error(2) } diff --git a/server/channels/store/storetest/mocks/ThreadStore.go b/server/channels/store/storetest/mocks/ThreadStore.go index 85e23756da..9a93de42f2 100644 --- a/server/channels/store/storetest/mocks/ThreadStore.go +++ b/server/channels/store/storetest/mocks/ThreadStore.go @@ -591,9 +591,9 @@ func (_m *ThreadStore) MarkAsRead(userID string, threadID string, timestamp int6 return r0 } -// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor -func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { - ret := _m.Called(now, globalPolicyEndTime, limit, cursor) +// PermanentDeleteBatchForRetentionPolicies provides a mock function with given fields: retentionPolicyBatchConfigs, cursor +func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { + ret := _m.Called(retentionPolicyBatchConfigs, cursor) if len(ret) == 0 { panic("no return value specified for PermanentDeleteBatchForRetentionPolicies") @@ -602,23 +602,23 @@ func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globa var r0 int64 var r1 model.RetentionPolicyCursor var r2 error - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { - return rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { + return rf(retentionPolicyBatchConfigs, cursor) } - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok { - r0 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) int64); ok { + r0 = rf(retentionPolicyBatchConfigs, cursor) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { - r1 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(1).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { + r1 = rf(retentionPolicyBatchConfigs, cursor) } else { r1 = ret.Get(1).(model.RetentionPolicyCursor) } - if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok { - r2 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(2).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) error); ok { + r2 = rf(retentionPolicyBatchConfigs, cursor) } else { r2 = ret.Error(2) } @@ -626,9 +626,9 @@ func (_m *ThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globa return r0, r1, r2 } -// PermanentDeleteBatchThreadMembershipsForRetentionPolicies provides a mock function with given fields: now, globalPolicyEndTime, limit, cursor -func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { - ret := _m.Called(now, globalPolicyEndTime, limit, cursor) +// PermanentDeleteBatchThreadMembershipsForRetentionPolicies provides a mock function with given fields: retentionPolicyBatchConfigs, cursor +func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { + ret := _m.Called(retentionPolicyBatchConfigs, cursor) if len(ret) == 0 { panic("no return value specified for PermanentDeleteBatchThreadMembershipsForRetentionPolicies") @@ -637,23 +637,23 @@ func (_m *ThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies var r0 int64 var r1 model.RetentionPolicyCursor var r2 error - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { - return rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)); ok { + return rf(retentionPolicyBatchConfigs, cursor) } - if rf, ok := ret.Get(0).(func(int64, int64, int64, model.RetentionPolicyCursor) int64); ok { - r0 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(0).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) int64); ok { + r0 = rf(retentionPolicyBatchConfigs, cursor) } else { r0 = ret.Get(0).(int64) } - if rf, ok := ret.Get(1).(func(int64, int64, int64, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { - r1 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(1).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) model.RetentionPolicyCursor); ok { + r1 = rf(retentionPolicyBatchConfigs, cursor) } else { r1 = ret.Get(1).(model.RetentionPolicyCursor) } - if rf, ok := ret.Get(2).(func(int64, int64, int64, model.RetentionPolicyCursor) error); ok { - r2 = rf(now, globalPolicyEndTime, limit, cursor) + if rf, ok := ret.Get(2).(func(model.RetentionPolicyBatchConfigs, model.RetentionPolicyCursor) error); ok { + r2 = rf(retentionPolicyBatchConfigs, cursor) } else { r2 = ret.Error(2) } diff --git a/server/channels/store/storetest/post_store.go b/server/channels/store/storetest/post_store.go index 5e89f35bd2..4455dc7aaf 100644 --- a/server/channels/store/storetest/post_store.go +++ b/server/channels/store/storetest/post_store.go @@ -4220,7 +4220,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. o3, err = ss.Post().Save(rctx, o3) require.NoError(t, err) - deleted, _, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{}) + deleted, _, err := ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) require.Equal(t, int64(2), deleted) @@ -4254,7 +4258,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. } cursor := model.RetentionPolicyCursor{} - deleted, cursor, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) + deleted, cursor, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2, + Limit: 2, + }, cursor) require.NoError(t, err) require.Equal(t, int64(2), deleted) @@ -4268,7 +4276,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. require.NoError(t, err) require.Equal(t, int64(0), deleted) - deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2, 2, cursor) + deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2, + Limit: 2, + }, cursor) require.NoError(t, err) require.Equal(t, int64(1), deleted) @@ -4301,13 +4313,21 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. post, err2 = ss.Post().Save(rctx, post) require.NoError(t, err2) - _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, 1000, model.RetentionPolicyCursor{}) + _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err2) _, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.NoError(t, err2, "global policy should have been ignored due to granular policy") nowMillis := post.CreateAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{}) + _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err2) _, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.Error(t, err2, "post should have been deleted by channel policy") @@ -4326,7 +4346,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. require.NoError(t, err2) nowMillis = post.CreateAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{}) + _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err2) _, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.NoError(t, err2, "channel policy should have overridden team policy") @@ -4338,7 +4362,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. err2 = ss.RetentionPolicy().Delete(channelPolicy.ID) require.NoError(t, err2) - _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, 1000, model.RetentionPolicyCursor{}) + _, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err2) _, err2 = ss.Post().Get(context.Background(), post.Id, model.GetPostsOptions{}, "", map[string]bool{}) require.Error(t, err2, "post should have been deleted by team policy") @@ -4419,7 +4447,11 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. require.NoError(t, err2) nowMillis := int64(1 + 30*model.DayInMilliseconds + 1) - deleted, _, err2 := ss.Post().PermanentDeleteBatchForRetentionPolicies(nowMillis, 2, 1000, model.RetentionPolicyCursor{}) + deleted, _, err2 = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 2, + Limit: 1000, + }, model.RetentionPolicyCursor{}) require.NoError(t, err2) require.Equal(t, int64(3), deleted) @@ -4435,6 +4467,151 @@ func testPostStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss store. require.Equal(t, int64(0), deleted) } }) + + t.Run("with preserve pinned posts true", func(t *testing.T) { + p1 := &model.Post{} + p1.ChannelId = channel.Id + p1.UserId = model.NewId() + p1.IsPinned = false + p1.Message = NewTestID() + p1.CreateAt = 1000 + p1, err = ss.Post().Save(rctx, p1) + require.NoError(t, err) + + p2 := &model.Post{} + p2.ChannelId = channel.Id + p2.UserId = model.NewId() + p2.IsPinned = false + p2.Message = NewTestID() + p2.CreateAt = 1000 + p2, err = ss.Post().Save(rctx, p2) + require.NoError(t, err) + + p3 := &model.Post{} + p3.ChannelId = channel.Id + p3.UserId = model.NewId() + p3.IsPinned = false + p3.Message = NewTestID() + p3.CreateAt = 1000 + p3, err = ss.Post().Save(rctx, p3) + require.NoError(t, err) + + np3 := p3.Clone() + np3.IsPinned = true + + np3, err = ss.Post().Update(rctx, np3, p3) + + deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: 1000, + PreservePinnedPosts: true, + }, model.RetentionPolicyCursor{}) + require.NoError(t, err) + require.Equal(t, int64(3), deleted) + + _, err = ss.Post().Get(context.Background(), p1.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 1 after purge") + + _, err = ss.Post().Get(context.Background(), p2.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 2 after purge") + + _, err = ss.Post().Get(context.Background(), p3.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 3 before update after purge") + + _, err = ss.Post().Get(context.Background(), np3.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.NoError(t, err, "Should have found updated post 3 after purge") + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 3, len(rows[0].Ids)) + // Clean up retention ids table + deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + require.Equal(t, int64(0), deleted) + + // Clean up pinned post + _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: 1000, + PreservePinnedPosts: false, + }, model.RetentionPolicyCursor{}) + require.NoError(t, err) + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + + // Clean up retention ids table + _, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + }) + + t.Run("with preserve pinned posts false", func(t *testing.T) { + p1 := &model.Post{} + p1.ChannelId = channel.Id + p1.UserId = model.NewId() + p1.IsPinned = false + p1.Message = NewTestID() + p1.CreateAt = 1000 + p1, err = ss.Post().Save(rctx, p1) + require.NoError(t, err) + + p2 := &model.Post{} + p2.ChannelId = channel.Id + p2.UserId = model.NewId() + p2.IsPinned = false + p2.Message = NewTestID() + p2.CreateAt = 1000 + p2, err = ss.Post().Save(rctx, p2) + require.NoError(t, err) + + p3 := &model.Post{} + p3.ChannelId = channel.Id + p3.UserId = model.NewId() + p3.IsPinned = false + p3.Message = NewTestID() + p3.CreateAt = 1000 + p3, err = ss.Post().Save(rctx, p3) + require.NoError(t, err) + + np3 := p3.Clone() + np3.IsPinned = true + + np3, err = ss.Post().Update(rctx, np3, p3) + + deleted, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: 1000, + PreservePinnedPosts: false, + }, model.RetentionPolicyCursor{}) + require.NoError(t, err) + require.Equal(t, int64(4), deleted) + + _, err = ss.Post().Get(context.Background(), p1.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 1 after purge") + + _, err = ss.Post().Get(context.Background(), p2.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 2 after purge") + + _, err = ss.Post().Get(context.Background(), p3.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found post 3 before update after purge") + + _, err = ss.Post().Get(context.Background(), np3.Id, model.GetPostsOptions{}, "", map[string]bool{}) + require.Error(t, err, "Should have not found updated post 3 after purge") + + rows, err = ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) + require.NoError(t, err) + require.Equal(t, 1, len(rows)) + require.Equal(t, 4, len(rows[0].Ids)) + // Clean up retention ids table + deleted, err = ss.Reaction().DeleteOrphanedRowsByIds(rows[0]) + require.NoError(t, err) + require.Equal(t, int64(0), deleted) + }) } func testPostStoreGetOldest(t *testing.T, rctx request.CTX, ss store.Store) { diff --git a/server/channels/store/storetest/preference_store.go b/server/channels/store/storetest/preference_store.go index a860f995a7..257e071c09 100644 --- a/server/channels/store/storetest/preference_store.go +++ b/server/channels/store/storetest/preference_store.go @@ -448,7 +448,11 @@ func testPreferenceDeleteOrphanedRows(t *testing.T, rctx request.CTX, ss store.S nErr := ss.Preference().Save(model.Preferences{preference1, preference2}) require.NoError(t, nErr) - _, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) + _, _, nErr = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: limit, + }, model.RetentionPolicyCursor{}) assert.NoError(t, nErr) rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) diff --git a/server/channels/store/storetest/reaction_store.go b/server/channels/store/storetest/reaction_store.go index 9bacc4644f..34ce984822 100644 --- a/server/channels/store/storetest/reaction_store.go +++ b/server/channels/store/storetest/reaction_store.go @@ -710,7 +710,11 @@ func testReactionStorePermanentDeleteBatch(t *testing.T, rctx request.CTX, ss st require.NoError(t, err) } - _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(0, 2000, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Post().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: 0, + GlobalPolicyEndTime: 2000, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) rows, err := ss.RetentionPolicy().GetIdsForDeletionByTableName("Posts", 1000) diff --git a/server/channels/store/storetest/thread_store.go b/server/channels/store/storetest/thread_store.go index eef636cc6d..3a682f04d6 100644 --- a/server/channels/store/storetest/thread_store.go +++ b/server/channels/store/storetest/thread_store.go @@ -530,7 +530,11 @@ func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx require.NoError(t, err) nowMillis := thread.LastReplyAt + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) thread, err = ss.Thread().Get(post.Id) assert.NoError(t, err) @@ -552,7 +556,11 @@ func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx require.NoError(t, err) nowMillis = thread.LastReplyAt + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) _, err = ss.Thread().Get(post.Id) require.NoError(t, err, "channel policy should have overridden team policy") @@ -560,7 +568,11 @@ func testThreadStorePermanentDeleteBatchForRetentionPolicies(t *testing.T, rctx // Delete channel policy and re-run team policy err = ss.RetentionPolicy().Delete(channelPolicy.ID) require.NoError(t, err) - _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) thread, err = ss.Thread().Get(post.Id) assert.NoError(t, err) @@ -617,7 +629,11 @@ func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t require.NoError(t, err) nowMillis := threadMembership.LastUpdated + *channelPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) _, err = ss.Thread().GetMembershipForUser(userID, post.Id) require.Error(t, err, "thread membership should have been deleted by channel policy") @@ -636,7 +652,11 @@ func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t require.NoError(t, err) nowMillis = threadMembership.LastUpdated + *teamPolicy.PostDurationDays*model.DayInMilliseconds + 1 - _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) _, err = ss.Thread().GetMembershipForUser(userID, post.Id) require.NoError(t, err, "channel policy should have overridden team policy") @@ -644,7 +664,11 @@ func testThreadStorePermanentDeleteBatchThreadMembershipsForRetentionPolicies(t // Delete channel policy and re-run team policy err = ss.RetentionPolicy().Delete(channelPolicy.ID) require.NoError(t, err) - _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(nowMillis, 0, limit, model.RetentionPolicyCursor{}) + _, _, err = ss.Thread().PermanentDeleteBatchThreadMembershipsForRetentionPolicies(model.RetentionPolicyBatchConfigs{ + Now: nowMillis, + GlobalPolicyEndTime: 0, + Limit: limit, + }, model.RetentionPolicyCursor{}) require.NoError(t, err) _, err = ss.Thread().GetMembershipForUser(userID, post.Id) require.Error(t, err, "thread membership should have been deleted by team policy") diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 098aa67dfc..42cad90e3a 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -3002,10 +3002,10 @@ func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64 return result, err } -func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *TimerLayerChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { start := time.Now() - result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ChannelMemberHistoryStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -6661,10 +6661,10 @@ func (s *TimerLayerPostStore) PermanentDeleteBatch(endTime int64, limit int64) ( return result, err } -func (s *TimerLayerPostStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *TimerLayerPostStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { start := time.Now() - result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.PostStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -10835,10 +10835,10 @@ func (s *TimerLayerThreadStore) MarkAsRead(userID string, threadID string, times return err } -func (s *TimerLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *TimerLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { start := time.Now() - result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchForRetentionPolicies(retentionPolicyBatchConfigs, cursor) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { @@ -10851,10 +10851,10 @@ func (s *TimerLayerThreadStore) PermanentDeleteBatchForRetentionPolicies(now int return result, resultVar1, err } -func (s *TimerLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now int64, globalPolicyEndTime int64, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { +func (s *TimerLayerThreadStore) PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs model.RetentionPolicyBatchConfigs, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) { start := time.Now() - result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit, cursor) + result, resultVar1, err := s.ThreadStore.PermanentDeleteBatchThreadMembershipsForRetentionPolicies(retentionPolicyBatchConfigs, cursor) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/server/public/model/config.go b/server/public/model/config.go index 5083a5ebdf..7c622bafdd 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -3119,6 +3119,7 @@ type DataRetentionSettings struct { BatchSize *int `access:"compliance_data_retention_policy"` TimeBetweenBatchesMilliseconds *int `access:"compliance_data_retention_policy"` RetentionIdsBatchSize *int `access:"compliance_data_retention_policy"` + PreservePinnedPosts *bool `access:"compliance_data_retention_policy"` } func (s *DataRetentionSettings) SetDefaults() { @@ -3168,6 +3169,10 @@ func (s *DataRetentionSettings) SetDefaults() { if s.RetentionIdsBatchSize == nil { s.RetentionIdsBatchSize = NewPointer(DataRetentionSettingsDefaultRetentionIdsBatchSize) } + + if s.PreservePinnedPosts == nil { + s.PreservePinnedPosts = NewPointer(false) + } } // GetMessageRetentionHours returns the message retention time as an int. diff --git a/server/public/model/data_retention_policy.go b/server/public/model/data_retention_policy.go index 8da6606b02..7d9c8e5c17 100644 --- a/server/public/model/data_retention_policy.go +++ b/server/public/model/data_retention_policy.go @@ -91,6 +91,13 @@ type RetentionIdsForDeletion struct { Ids []string } +type RetentionPolicyBatchConfigs struct { + Now int64 + GlobalPolicyEndTime int64 + Limit int64 + PreservePinnedPosts bool +} + func (r *RetentionIdsForDeletion) PreSave() { if r.Id == "" { r.Id = NewId() diff --git a/webapp/channels/src/components/error_page/error_message.tsx b/webapp/channels/src/components/error_page/error_message.tsx index deb412651e..9530a17b52 100644 --- a/webapp/channels/src/components/error_page/error_message.tsx +++ b/webapp/channels/src/components/error_page/error_message.tsx @@ -60,6 +60,16 @@ const ErrorMessage: React.FC = ({type, message, service, isGuest}: Props)

); break; + case ErrorPageTypes.POST_NOT_FOUND: + errorMessage = ( +

+ +

+ ); + break; case ErrorPageTypes.CLOUD_ARCHIVED: errorMessage = (

diff --git a/webapp/channels/src/components/error_page/error_title.tsx b/webapp/channels/src/components/error_page/error_title.tsx index c7edc048fa..5ace975c9f 100644 --- a/webapp/channels/src/components/error_page/error_title.tsx +++ b/webapp/channels/src/components/error_page/error_title.tsx @@ -31,6 +31,14 @@ const ErrorTitle: React.FC = ({type, title}: Props) => { /> ); break; + case ErrorPageTypes.POST_NOT_FOUND: + errorTitle = ( + + ); + break; case ErrorPageTypes.CLOUD_ARCHIVED: errorTitle = ( { const {data} = await dispatch(getPostThread(post.root_id)); + if (!data) { + getHistory().replace(`/error?type=${ErrorPageTypes.POST_NOT_FOUND}&returnTo=${returnTo}`); + return {data: false}; + } + if (data!.first_inaccessible_post_time) { getHistory().replace(`/error?type=${ErrorPageTypes.CLOUD_ARCHIVED}&returnTo=${returnTo}`); return {data: false}; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 8160758a1f..ab7d2fd0cf 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5008,6 +5008,8 @@ "post.ariaLabel.reaction": ", 1 reaction", "post.ariaLabel.reactionMultiple": ", {reactionCount} reactions", "post.ariaLabel.replyMessage": "At {time} {date}, {authorName} replied, {message}", + "post.error.access": "The post you're requesting is private or does not exist.", + "post.error.title": "Post Not Found", "post.reminder.acknowledgement": "You will be reminded at {reminderTime}, {reminderDate} about this message from {username}: {permaLink}", "post.reminder.systemBot": "Hi there, here's your reminder about this message from {username}: {permaLink}", "post.renderError.message": "An error occurred while rendering this post.", diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index ad33c85134..d3dd52f4d3 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -927,6 +927,7 @@ export const ErrorPageTypes = { PERMALINK_NOT_FOUND: 'permalink_not_found', TEAM_NOT_FOUND: 'team_not_found', CHANNEL_NOT_FOUND: 'channel_not_found', + POST_NOT_FOUND: 'post_not_found', CLOUD_ARCHIVED: 'cloud_archived', };