fix(messages): sync preserve private dialogs after clearing history
This commit is contained in:
parent
3926db7762
commit
e22fac1c3f
19 changed files with 923 additions and 231 deletions
|
|
@ -33,7 +33,7 @@ func (s *MessageStore) DeleteMessages(_ context.Context, req domain.DeleteMessag
|
|||
if req.Revoke && len(revokeUIDs) > 0 {
|
||||
deleted = append(deleted, s.deleteMemoryMessagesByUIDLocked(revokeUIDs, req.OwnerUserID)...)
|
||||
}
|
||||
return s.finishMemoryDeleteLocked(res, deleted, req.Date, false), nil
|
||||
return s.finishMemoryDeleteLocked(res, deleted, req.Date, nil), nil
|
||||
}
|
||||
|
||||
type deletedMemoryMessage struct {
|
||||
|
|
@ -45,8 +45,13 @@ type deletedMemoryMessage struct {
|
|||
randomID int64
|
||||
}
|
||||
|
||||
func (s *MessageStore) finishMemoryDeleteLocked(res domain.DeleteMessagesResult, deleted []deletedMemoryMessage, date int, preserveEmptyDialogs bool) domain.DeleteMessagesResult {
|
||||
if len(deleted) == 0 {
|
||||
type memoryHistoryClearAnchor struct {
|
||||
message domain.Message
|
||||
materialized bool
|
||||
}
|
||||
|
||||
func (s *MessageStore) finishMemoryDeleteLocked(res domain.DeleteMessagesResult, deleted []deletedMemoryMessage, date int, anchors map[int64]memoryHistoryClearAnchor) domain.DeleteMessagesResult {
|
||||
if len(deleted) == 0 && len(anchors) == 0 {
|
||||
return res
|
||||
}
|
||||
idsByOwner := make(map[int64][]int)
|
||||
|
|
@ -64,57 +69,153 @@ func (s *MessageStore) finishMemoryDeleteLocked(res domain.DeleteMessagesResult,
|
|||
}
|
||||
peersByOwner[row.userID][row.peer] = struct{}{}
|
||||
}
|
||||
if s.dialogs != nil {
|
||||
s.dialogs.mu.Lock()
|
||||
for userID, peers := range peersByOwner {
|
||||
for peer := range peers {
|
||||
s.rebuildMemoryDialogLocked(userID, peer, preserveEmptyDialogs)
|
||||
}
|
||||
for userID, anchor := range anchors {
|
||||
if peersByOwner[userID] == nil {
|
||||
peersByOwner[userID] = make(map[domain.Peer]struct{})
|
||||
}
|
||||
s.dialogs.mu.Unlock()
|
||||
peersByOwner[userID][anchor.message.Peer] = struct{}{}
|
||||
}
|
||||
ownerIDs := make([]int64, 0, len(idsByOwner))
|
||||
ownerSet := make(map[int64]struct{}, len(idsByOwner)+len(anchors))
|
||||
for userID := range idsByOwner {
|
||||
ownerSet[userID] = struct{}{}
|
||||
}
|
||||
for userID, anchor := range anchors {
|
||||
if !anchor.materialized {
|
||||
ownerSet[userID] = struct{}{}
|
||||
}
|
||||
}
|
||||
ownerIDs := make([]int64, 0, len(ownerSet))
|
||||
for userID := range ownerSet {
|
||||
ownerIDs = append(ownerIDs, userID)
|
||||
}
|
||||
sort.Slice(ownerIDs, func(i, j int) bool { return ownerIDs[i] < ownerIDs[j] })
|
||||
for _, userID := range ownerIDs {
|
||||
ids := normalizeMemoryMessageIDs(idsByOwner[userID])
|
||||
if len(ids) == 0 {
|
||||
anchor, hasAnchor := anchors[userID]
|
||||
materializeAnchor := hasAnchor && !anchor.materialized
|
||||
totalPtsCount := len(ids)
|
||||
if materializeAnchor {
|
||||
totalPtsCount += 2
|
||||
}
|
||||
if totalPtsCount == 0 {
|
||||
continue
|
||||
}
|
||||
pts := s.nextPtsNLocked(userID, len(ids))
|
||||
event := domain.UpdateEvent{
|
||||
pts := s.nextPtsNLocked(userID, totalPtsCount)
|
||||
cursor := pts - totalPtsCount
|
||||
item := domain.DeletedMessagesForUser{
|
||||
UserID: userID,
|
||||
Type: domain.UpdateEventDeleteMessages,
|
||||
MessageIDs: ids,
|
||||
Pts: pts,
|
||||
PtsCount: len(ids),
|
||||
Date: date,
|
||||
MessageIDs: ids,
|
||||
PtsCount: totalPtsCount,
|
||||
Events: make([]domain.UpdateEvent, 0, 3),
|
||||
}
|
||||
for _, row := range deleted {
|
||||
if row.userID != userID || row.messageSenderID != userID || row.randomID == 0 || row.privateMessageID == 0 {
|
||||
continue
|
||||
if len(ids) > 0 {
|
||||
cursor += len(ids)
|
||||
event := domain.UpdateEvent{
|
||||
UserID: userID,
|
||||
Type: domain.UpdateEventDeleteMessages,
|
||||
Pts: cursor,
|
||||
PtsCount: len(ids),
|
||||
Date: date,
|
||||
MessageIDs: ids,
|
||||
}
|
||||
key := privateSendDedupKey{senderUserID: userID, randomID: row.randomID}
|
||||
record, ok := s.privateSendDedup[key]
|
||||
if !ok {
|
||||
continue
|
||||
for _, row := range deleted {
|
||||
if row.userID != userID || row.messageSenderID != userID || row.randomID == 0 || row.privateMessageID == 0 {
|
||||
continue
|
||||
}
|
||||
key := privateSendDedupKey{senderUserID: userID, randomID: row.randomID}
|
||||
record, ok := s.privateSendDedup[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cloned := cloneUpdateEvent(event)
|
||||
record.senderDeleteEvent = &cloned
|
||||
s.privateSendDedup[key] = record
|
||||
}
|
||||
cloned := cloneUpdateEvent(event)
|
||||
record.senderDeleteEvent = &cloned
|
||||
s.privateSendDedup[key] = record
|
||||
item.Event = event
|
||||
item.Events = append(item.Events, event)
|
||||
}
|
||||
res.Deleted = append(res.Deleted, domain.DeletedMessagesForUser{
|
||||
UserID: userID,
|
||||
MessageIDs: ids,
|
||||
Event: event,
|
||||
})
|
||||
if materializeAnchor {
|
||||
readPts := cursor + 1
|
||||
editPts := readPts + 1
|
||||
msg := domain.NewHistoryClearMessage(
|
||||
userID,
|
||||
anchor.message.Peer,
|
||||
anchor.message.ID,
|
||||
anchor.message.UID,
|
||||
anchor.message.Date,
|
||||
editPts,
|
||||
)
|
||||
for i := range s.m[userID] {
|
||||
if s.m[userID][i].ID == anchor.message.ID && s.m[userID][i].Peer == anchor.message.Peer {
|
||||
s.m[userID][i] = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
if byMessage := s.savedMessageTags[userID]; byMessage != nil {
|
||||
delete(byMessage, anchor.message.ID)
|
||||
if len(byMessage) == 0 {
|
||||
delete(s.savedMessageTags, userID)
|
||||
}
|
||||
}
|
||||
readEvent := domain.UpdateEvent{
|
||||
UserID: userID,
|
||||
Type: domain.UpdateEventReadHistoryInbox,
|
||||
Pts: readPts,
|
||||
PtsCount: 1,
|
||||
Date: date,
|
||||
Peer: anchor.message.Peer,
|
||||
MaxID: anchor.message.ID,
|
||||
StillUnreadCount: 0,
|
||||
}
|
||||
editEvent := domain.UpdateEvent{
|
||||
UserID: userID,
|
||||
Type: domain.UpdateEventEditMessage,
|
||||
Pts: editPts,
|
||||
PtsCount: 1,
|
||||
Date: date,
|
||||
Message: cloneMessage(msg),
|
||||
}
|
||||
item.Events = append(item.Events, readEvent, editEvent)
|
||||
cursor = editPts
|
||||
}
|
||||
if s.dialogs != nil {
|
||||
s.dialogs.mu.Lock()
|
||||
for peer := range peersByOwner[userID] {
|
||||
s.rebuildMemoryDialogLocked(userID, peer)
|
||||
}
|
||||
if materializeAnchor {
|
||||
s.advanceMemoryHistoryClearDialogLocked(userID, anchor.message.Peer, anchor.message.ID)
|
||||
}
|
||||
s.dialogs.mu.Unlock()
|
||||
}
|
||||
if cursor != pts {
|
||||
panic(fmt.Sprintf("memory delete history pts cursor %d does not reach reserved pts %d", cursor, pts))
|
||||
}
|
||||
res.Deleted = append(res.Deleted, item)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *MessageStore) rebuildMemoryDialogLocked(userID int64, peer domain.Peer, preserveEmpty bool) {
|
||||
func (s *MessageStore) advanceMemoryHistoryClearDialogLocked(userID int64, peer domain.Peer, maxID int) {
|
||||
list := s.dialogs.m[userID]
|
||||
for i := range list.Dialogs {
|
||||
if list.Dialogs[i].Peer != peer {
|
||||
continue
|
||||
}
|
||||
if list.Dialogs[i].ReadInboxMaxID < maxID {
|
||||
list.Dialogs[i].ReadInboxMaxID = maxID
|
||||
}
|
||||
list.Dialogs[i].UnreadCount = 0
|
||||
list.Dialogs[i].UnreadMark = false
|
||||
list.Dialogs[i].UnreadMentions = 0
|
||||
list.Dialogs[i].UnreadReactions = 0
|
||||
break
|
||||
}
|
||||
s.dialogs.m[userID] = list
|
||||
}
|
||||
|
||||
func (s *MessageStore) rebuildMemoryDialogLocked(userID int64, peer domain.Peer) {
|
||||
list := s.dialogs.m[userID]
|
||||
topID := 0
|
||||
topDate := 0
|
||||
|
|
@ -135,22 +236,6 @@ func (s *MessageStore) rebuildMemoryDialogLocked(userID int64, peer domain.Peer,
|
|||
continue
|
||||
}
|
||||
if topID == 0 {
|
||||
if preserveEmpty {
|
||||
oldTop := dialog.TopMessage
|
||||
dialog.TopMessage = 0
|
||||
dialog.TopMessageDate = 0
|
||||
if dialog.ReadInboxMaxID < oldTop {
|
||||
dialog.ReadInboxMaxID = oldTop
|
||||
}
|
||||
if dialog.ReadOutboxMaxID < oldTop {
|
||||
dialog.ReadOutboxMaxID = oldTop
|
||||
}
|
||||
dialog.UnreadCount = 0
|
||||
dialog.UnreadMark = false
|
||||
dialog.UnreadMentions = 0
|
||||
dialog.UnreadReactions = 0
|
||||
dialogs = append(dialogs, dialog)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, msg := range s.m[userID] {
|
||||
|
|
|
|||
|
|
@ -232,30 +232,66 @@ func (s *MessageStore) DeleteHistory(_ context.Context, req domain.DeleteHistory
|
|||
}
|
||||
return true
|
||||
}
|
||||
var anchors map[int64]memoryHistoryClearAnchor
|
||||
fullJustClear := req.JustClear && req.MaxID <= 0 && req.MinDate <= 0 && req.MaxDate <= 0
|
||||
if fullJustClear {
|
||||
anchors = make(map[int64]memoryHistoryClearAnchor, 2)
|
||||
if anchor, found := s.memoryHistoryClearAnchorLocked(req.OwnerUserID, req.Peer); found {
|
||||
anchors[req.OwnerUserID] = anchor
|
||||
}
|
||||
if req.Revoke && req.Peer.ID != req.OwnerUserID {
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}
|
||||
if anchor, found := s.memoryHistoryClearAnchorLocked(req.Peer.ID, peer); found {
|
||||
anchors[req.Peer.ID] = anchor
|
||||
}
|
||||
}
|
||||
}
|
||||
deleted, revokeUIDs, more := s.deleteMemoryMessagesLocked(req.OwnerUserID, domain.MaxDeleteHistoryBatch, func(msg domain.Message) bool {
|
||||
if anchor, ok := anchors[req.OwnerUserID]; ok && msg.ID == anchor.message.ID {
|
||||
return false
|
||||
}
|
||||
return msg.Peer == req.Peer && (req.MaxID <= 0 || msg.ID <= req.MaxID) && inDateRange(msg)
|
||||
})
|
||||
if req.Revoke {
|
||||
if len(revokeUIDs) > 0 {
|
||||
if req.MaxID > 0 && len(revokeUIDs) > 0 {
|
||||
deleted = append(deleted, s.deleteMemoryMessagesByUIDLocked(revokeUIDs, req.OwnerUserID)...)
|
||||
}
|
||||
// 与 PG 同语义:全量/按日期的双向清史直扫对端残余,我方早已
|
||||
// 单向删除的消息不能在对端残留。
|
||||
if req.MaxID <= 0 && req.Peer.ID != req.OwnerUserID {
|
||||
peerDeleted, _, peerMore := s.deleteMemoryMessagesLocked(req.Peer.ID, domain.MaxDeleteHistoryBatch, func(msg domain.Message) bool {
|
||||
if anchor, ok := anchors[req.Peer.ID]; ok && msg.ID == anchor.message.ID {
|
||||
return false
|
||||
}
|
||||
return msg.Peer == (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) && inDateRange(msg)
|
||||
})
|
||||
deleted = append(deleted, peerDeleted...)
|
||||
more = more || peerMore
|
||||
}
|
||||
}
|
||||
res = s.finishMemoryDeleteLocked(res, deleted, req.Date, req.JustClear)
|
||||
res = s.finishMemoryDeleteLocked(res, deleted, req.Date, anchors)
|
||||
if more {
|
||||
res.Offset = 1
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *MessageStore) memoryHistoryClearAnchorLocked(userID int64, peer domain.Peer) (memoryHistoryClearAnchor, bool) {
|
||||
var top domain.Message
|
||||
for _, msg := range s.m[userID] {
|
||||
if msg.Peer == peer && msg.ID > top.ID {
|
||||
top = msg
|
||||
}
|
||||
}
|
||||
if top.ID == 0 {
|
||||
return memoryHistoryClearAnchor{}, false
|
||||
}
|
||||
return memoryHistoryClearAnchor{
|
||||
message: cloneMessage(top),
|
||||
materialized: domain.IsHistoryClearServiceMessage(top),
|
||||
}, true
|
||||
}
|
||||
|
||||
func filterMessageList(messages []domain.Message, filter domain.MessageFilter) domain.MessageList {
|
||||
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
|
||||
sort.SliceStable(messages, func(i, j int) bool {
|
||||
|
|
|
|||
|
|
@ -1218,29 +1218,191 @@ func TestMessageStoreDeleteHistoryDeletesOrPreservesDialogAndRebuilds(t *testing
|
|||
preservedOwner := int64(1000000003)
|
||||
preservedPeerID := int64(1000000004)
|
||||
preservedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: preservedPeerID}
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: preservedOwner,
|
||||
RecipientUserID: preservedPeerID,
|
||||
RandomID: 300,
|
||||
Message: "clear but keep dialog",
|
||||
Date: 1700000500,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed preserved send: %v", err)
|
||||
var preservedTop domain.Message
|
||||
for i := 0; i < 2; i++ {
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: preservedOwner,
|
||||
RecipientUserID: preservedPeerID,
|
||||
RandomID: int64(300 + i),
|
||||
Message: "clear but keep dialog",
|
||||
Date: 1700000500 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("seed preserved send %d: %v", i, err)
|
||||
}
|
||||
preservedTop = sent.SenderMessage
|
||||
}
|
||||
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
clearResult, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: preservedOwner,
|
||||
Peer: preservedPeer,
|
||||
JustClear: true,
|
||||
Date: 1700000600,
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteHistory just_clear: %v", err)
|
||||
}
|
||||
clearSelf := clearResult.Self()
|
||||
if clearSelf.Pts != 5 || clearSelf.PtsCount != 3 || len(clearSelf.MessageIDs) != 1 || len(clearSelf.Events) != 3 {
|
||||
t.Fatalf("just_clear result = %+v, want delete+read+edit ending pts=5 count=3", clearSelf)
|
||||
}
|
||||
if clearSelf.Events[0].Type != domain.UpdateEventDeleteMessages ||
|
||||
clearSelf.Events[1].Type != domain.UpdateEventReadHistoryInbox ||
|
||||
clearSelf.Events[2].Type != domain.UpdateEventEditMessage {
|
||||
t.Fatalf("just_clear events = %+v, want delete/read/edit order", clearSelf.Events)
|
||||
}
|
||||
preservedDialogs, err := dialogs.ListByUser(ctx, preservedOwner, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("preserved dialogs: %v", err)
|
||||
}
|
||||
if len(preservedDialogs.Dialogs) != 1 || preservedDialogs.Dialogs[0].Peer != preservedPeer || preservedDialogs.Dialogs[0].TopMessage != 0 || len(preservedDialogs.Messages) != 0 {
|
||||
t.Fatalf("preserved dialogs = %+v messages=%+v, want empty dialog kept after just_clear", preservedDialogs.Dialogs, preservedDialogs.Messages)
|
||||
if len(preservedDialogs.Dialogs) != 1 || preservedDialogs.Dialogs[0].Peer != preservedPeer ||
|
||||
preservedDialogs.Dialogs[0].TopMessage != preservedTop.ID || len(preservedDialogs.Messages) != 1 {
|
||||
t.Fatalf("preserved dialogs = %+v messages=%+v, want history-clear top %d", preservedDialogs.Dialogs, preservedDialogs.Messages, preservedTop.ID)
|
||||
}
|
||||
clearMessage := preservedDialogs.Messages[0]
|
||||
if !domain.IsHistoryClearServiceMessage(clearMessage) || clearMessage.ID != preservedTop.ID ||
|
||||
!clearMessage.Out || clearMessage.From.ID != preservedOwner || clearMessage.Body != "" ||
|
||||
clearMessage.ReplyTo != nil || clearMessage.Forward != nil || clearMessage.MediaUnread ||
|
||||
clearMessage.ReactionUnread || clearMessage.Pinned {
|
||||
t.Fatalf("history clear anchor = %+v, want clean owner-local service message", clearMessage)
|
||||
}
|
||||
repeated, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: preservedOwner,
|
||||
Peer: preservedPeer,
|
||||
JustClear: true,
|
||||
Date: 1700000601,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("repeat DeleteHistory just_clear: %v", err)
|
||||
}
|
||||
if repeated.Changed() || len(repeated.Deleted) != 0 || messages.nextPts[preservedOwner] != 5 {
|
||||
t.Fatalf("repeat just_clear = %+v pts=%d, want idempotent no-op", repeated, messages.nextPts[preservedOwner])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreDeleteHistoryJustClearRevokeKeepsPerOwnerAnchors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
const alice, bob = int64(1101), int64(1102)
|
||||
var sent domain.SendPrivateTextResult
|
||||
for i := 0; i < 2; i++ {
|
||||
var err error
|
||||
sent, err = messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: alice, RecipientUserID: bob, RandomID: int64(800 + i),
|
||||
Message: "revoke clear", Date: 1700000700 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
res, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: alice,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob},
|
||||
JustClear: true,
|
||||
Revoke: true,
|
||||
Date: 1700000800,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("revoke just_clear: %v", err)
|
||||
}
|
||||
if len(res.Deleted) != 2 {
|
||||
t.Fatalf("deleted owners = %+v, want alice and bob", res.Deleted)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
userID int64
|
||||
peerID int64
|
||||
topID int
|
||||
}{
|
||||
{alice, bob, sent.SenderMessage.ID},
|
||||
{bob, alice, sent.RecipientMessage.ID},
|
||||
} {
|
||||
history, err := messages.ListByUser(ctx, tc.userID, domain.MessageFilter{
|
||||
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: tc.peerID}, Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("history user %d: %v", tc.userID, err)
|
||||
}
|
||||
if len(history.Messages) != 1 || history.Messages[0].ID != tc.topID ||
|
||||
!domain.IsHistoryClearServiceMessage(history.Messages[0]) ||
|
||||
history.Messages[0].From.ID != tc.userID || !history.Messages[0].Out {
|
||||
t.Fatalf("history user %d = %+v, want owner-local anchor %d", tc.userID, history.Messages, tc.topID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreDeleteHistoryDateRangeDoesNotCreateHistoryClearAnchor(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
const owner, peerID = int64(1201), int64(1202)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerID}
|
||||
for i, date := range []int{100, 200} {
|
||||
if _, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: owner, RecipientUserID: peerID, RandomID: int64(900 + i),
|
||||
Message: "dated", Date: date,
|
||||
}); err != nil {
|
||||
t.Fatalf("send %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if _, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: owner, Peer: peer, JustClear: true, MinDate: 150, MaxDate: 250, Date: 300,
|
||||
}); err != nil {
|
||||
t.Fatalf("date delete: %v", err)
|
||||
}
|
||||
history, err := messages.ListByUser(ctx, owner, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("history: %v", err)
|
||||
}
|
||||
if len(history.Messages) != 1 || history.Messages[0].Date != 100 || domain.IsHistoryClearServiceMessage(history.Messages[0]) {
|
||||
t.Fatalf("date history = %+v, want surviving ordinary message only", history.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreDeleteHistoryJustClearKeepsAnchorAcrossBatches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
messages := NewMessageStore(dialogs)
|
||||
const owner, peerID = int64(1301), int64(1302)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerID}
|
||||
total := domain.MaxDeleteHistoryBatch + 2
|
||||
var topID int
|
||||
for i := 0; i < total; i++ {
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: owner, RecipientUserID: peerID, RandomID: int64(10000 + i),
|
||||
Message: "batch clear", Date: 1700010000 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send %d: %v", i, err)
|
||||
}
|
||||
topID = sent.SenderMessage.ID
|
||||
}
|
||||
first, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: owner, Peer: peer, JustClear: true, Date: 1700020000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("first clear: %v", err)
|
||||
}
|
||||
if first.Offset == 0 || len(first.Self().MessageIDs) != domain.MaxDeleteHistoryBatch ||
|
||||
first.Self().PtsCount != domain.MaxDeleteHistoryBatch+2 {
|
||||
t.Fatalf("first clear = %+v, want full batch plus one read/edit", first.Self())
|
||||
}
|
||||
second, err := messages.DeleteHistory(ctx, domain.DeleteHistoryRequest{
|
||||
OwnerUserID: owner, Peer: peer, JustClear: true, Date: 1700020001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("second clear: %v", err)
|
||||
}
|
||||
if second.Offset != 0 || len(second.Self().MessageIDs) != 1 || second.Self().PtsCount != 1 ||
|
||||
len(second.Self().Events) != 1 || second.Self().Events[0].Type != domain.UpdateEventDeleteMessages {
|
||||
t.Fatalf("second clear = %+v, want remaining delete only", second.Self())
|
||||
}
|
||||
history, err := messages.ListByUser(ctx, owner, domain.MessageFilter{HasPeer: true, Peer: peer, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("history: %v", err)
|
||||
}
|
||||
if len(history.Messages) != 1 || history.Messages[0].ID != topID ||
|
||||
!domain.IsHistoryClearServiceMessage(history.Messages[0]) {
|
||||
t.Fatalf("history = %+v, want stable top anchor %d", history.Messages, topID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ func (s *MessageStore) DeleteSavedHistory(_ context.Context, req domain.DeleteSa
|
|||
return true
|
||||
}
|
||||
deleted, _, more := s.deleteMemoryMessagesLocked(req.OwnerUserID, domain.MaxDeleteHistoryBatch, match)
|
||||
delRes := s.finishMemoryDeleteLocked(domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}, deleted, req.Date, false)
|
||||
delRes := s.finishMemoryDeleteLocked(domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}, deleted, req.Date, nil)
|
||||
res.More = more
|
||||
for _, d := range delRes.Deleted {
|
||||
if d.UserID == req.OwnerUserID {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue