fix(channels): sync preserve locally cleared dialogs
This commit is contained in:
parent
e22fac1c3f
commit
73cf6a8184
50 changed files with 1525 additions and 327 deletions
|
|
@ -394,10 +394,14 @@ func (r *Router) onChannelsDeleteHistory(ctx context.Context, req *tg.ChannelsDe
|
|||
return nil, channelDeleteErr(err)
|
||||
}
|
||||
if res.Event.Pts == 0 {
|
||||
event := r.recordChannelAvailableMessages(ctx, userID, res.Channel.ID, res.AvailableMinID)
|
||||
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, event.MaxID)
|
||||
updates.Updates = appendAuxPtsBookkeeping(updates.Updates, event)
|
||||
r.pushUserUpdates(ctx, userID, updates)
|
||||
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, res.AvailableMinID)
|
||||
if res.AvailableMinChanged {
|
||||
// updateChannelAvailableMessages is an absolute owner-local boundary
|
||||
// with no account/channel pts. Other online sessions consume it
|
||||
// immediately; future cold/offline sessions discover the same
|
||||
// boundary through account/channel difference recovery.
|
||||
r.pushUserUpdates(ctx, userID, updates)
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
pushBatch := func(batch domain.DeleteChannelHistoryResult) *tg.Updates {
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ func TestChannelsDeleteHistoryLocalClearEmitsAvailableMessagesUpdate(t *testing.
|
|||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
|
||||
Updates: updateSvc,
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
|
@ -193,7 +194,14 @@ func TestChannelsDeleteHistoryLocalClearEmitsAvailableMessagesUpdate(t *testing.
|
|||
if err != nil {
|
||||
t.Fatalf("send channel message: %v", err)
|
||||
}
|
||||
msg := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage).Message.(*tg.Message)
|
||||
newChannelUpdate := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage)
|
||||
msg := newChannelUpdate.Message.(*tg.Message)
|
||||
clearSince := int(time.Now().Unix()) - 1
|
||||
stateBefore, err := updateSvc.CurrentState(ctx, owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("account state before clear: %v", err)
|
||||
}
|
||||
pushesBefore := len(sessions.pushedUserIDs())
|
||||
cleared, err := r.onChannelsDeleteHistory(WithUserID(ctx, owner.ID), &tg.ChannelsDeleteHistoryRequest{
|
||||
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
MaxID: msg.ID,
|
||||
|
|
@ -202,42 +210,144 @@ func TestChannelsDeleteHistoryLocalClearEmitsAvailableMessagesUpdate(t *testing.
|
|||
t.Fatalf("delete channel history local: %v", err)
|
||||
}
|
||||
updates, ok := cleared.(*tg.Updates)
|
||||
if !ok || len(updates.Updates) != 2 {
|
||||
t.Fatalf("clear response = %T %+v, want available update plus pts bookkeeping", cleared, cleared)
|
||||
if !ok || len(updates.Updates) != 1 {
|
||||
t.Fatalf("clear response = %T %+v, want one no-pts available update", cleared, cleared)
|
||||
}
|
||||
available, ok := updates.Updates[0].(*tg.UpdateChannelAvailableMessages)
|
||||
if !ok || available.ChannelID != channel.ID || available.AvailableMinID != msg.ID {
|
||||
t.Fatalf("clear update = %#v, want updateChannelAvailableMessages channel=%d min=%d", updates.Updates[0], channel.ID, msg.ID)
|
||||
}
|
||||
// updateChannelAvailableMessages 不带账号 pts,事件占用的 pts 槽位
|
||||
// 必须用空 updateDeleteMessages 显式同步给客户端。
|
||||
bookkeeping, ok := updates.Updates[1].(*tg.UpdateDeleteMessages)
|
||||
if !ok || len(bookkeeping.Messages) != 0 || bookkeeping.Pts <= 0 || bookkeeping.PtsCount != 1 {
|
||||
t.Fatalf("clear bookkeeping = %#v, want empty updateDeleteMessages carrying the account pts step", updates.Updates[1])
|
||||
stateAfter, err := updateSvc.CurrentState(ctx, owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("account state after clear: %v", err)
|
||||
}
|
||||
if stateAfter.Pts != stateBefore.Pts {
|
||||
t.Fatalf("account pts advanced on local channel clear: before=%d after=%d", stateBefore.Pts, stateAfter.Pts)
|
||||
}
|
||||
pushed := sessions.snapshot()
|
||||
if pushed.userID != owner.ID {
|
||||
t.Fatalf("pushed user = %d, want owner %d", pushed.userID, owner.ID)
|
||||
}
|
||||
pushedUpdates, ok := pushed.message.(*tg.Updates)
|
||||
if !ok || len(pushedUpdates.Updates) != 2 {
|
||||
t.Fatalf("pushed clear update = %T %+v, want available update plus pts bookkeeping", pushed.message, pushed.message)
|
||||
if !ok || len(pushedUpdates.Updates) != 1 {
|
||||
t.Fatalf("pushed clear update = %T %+v, want one no-pts available update", pushed.message, pushed.message)
|
||||
}
|
||||
if _, ok := pushedUpdates.Updates[0].(*tg.UpdateChannelAvailableMessages); !ok {
|
||||
t.Fatalf("pushed update[0] = %T, want updateChannelAvailableMessages", pushedUpdates.Updates[0])
|
||||
}
|
||||
diff, err := r.onUpdatesGetDifference(WithUserID(ctx, owner.ID), &tg.UpdatesGetDifferenceRequest{Pts: 0})
|
||||
if got := len(sessions.pushedUserIDs()) - pushesBefore; got != 1 {
|
||||
t.Fatalf("clear online pushes = %d, want exactly one owner-session fanout", got)
|
||||
}
|
||||
diff, err := updateSvc.GetDifference(ctx, [8]byte{}, owner.ID, stateBefore)
|
||||
if err != nil {
|
||||
t.Fatalf("get difference: %v", err)
|
||||
}
|
||||
full, ok := diff.(*tg.UpdatesDifference)
|
||||
if !ok || len(full.OtherUpdates) != 1 {
|
||||
t.Fatalf("difference = %T %+v, want one other update", diff, diff)
|
||||
if len(diff.Events) != 0 || diff.State.Pts != stateBefore.Pts {
|
||||
t.Fatalf("difference after no-pts clear = %+v, want no durable account event", diff)
|
||||
}
|
||||
if diffUpdate, ok := full.OtherUpdates[0].(*tg.UpdateChannelAvailableMessages); !ok || diffUpdate.ChannelID != channel.ID || diffUpdate.AvailableMinID != msg.ID {
|
||||
t.Fatalf("difference update = %#v, want updateChannelAvailableMessages", full.OtherUpdates[0])
|
||||
offline, err := r.onUpdatesGetDifference(WithUserID(ctx, owner.ID), &tg.UpdatesGetDifferenceRequest{
|
||||
Pts: stateBefore.Pts,
|
||||
Date: clearSince,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("account difference after offline local clear: %v", err)
|
||||
}
|
||||
offlineDiff, ok := offline.(*tg.UpdatesDifference)
|
||||
if !ok {
|
||||
t.Fatalf("offline account difference = %T %+v, want updates.difference", offline, offline)
|
||||
}
|
||||
var accountAvailable *tg.UpdateChannelAvailableMessages
|
||||
for _, update := range offlineDiff.OtherUpdates {
|
||||
if value, ok := update.(*tg.UpdateChannelAvailableMessages); ok {
|
||||
accountAvailable = value
|
||||
break
|
||||
}
|
||||
}
|
||||
if accountAvailable == nil ||
|
||||
accountAvailable.ChannelID != channel.ID ||
|
||||
accountAvailable.AvailableMinID != msg.ID {
|
||||
t.Fatalf("offline account updates = %+v, want updateChannelAvailableMessages channel=%d min=%d",
|
||||
offlineDiff.OtherUpdates, channel.ID, msg.ID)
|
||||
}
|
||||
if offlineDiff.State.Pts != stateBefore.Pts || offlineDiff.State.Date < clearSince {
|
||||
t.Fatalf("offline account state = %+v, want unchanged pts=%d and non-regressing date", offlineDiff.State, stateBefore.Pts)
|
||||
}
|
||||
|
||||
channelOffline, err := r.onUpdatesGetChannelDifference(WithUserID(ctx, owner.ID), &tg.UpdatesGetChannelDifferenceRequest{
|
||||
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
Filter: &tg.ChannelMessagesFilterEmpty{},
|
||||
Pts: newChannelUpdate.Pts,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("channel difference after offline local clear: %v", err)
|
||||
}
|
||||
channelDiff, ok := channelOffline.(*tg.UpdatesChannelDifference)
|
||||
if !ok {
|
||||
t.Fatalf("offline channel difference = %T %+v, want non-empty difference with absolute update", channelOffline, channelOffline)
|
||||
}
|
||||
var channelAvailable *tg.UpdateChannelAvailableMessages
|
||||
for _, update := range channelDiff.OtherUpdates {
|
||||
if value, ok := update.(*tg.UpdateChannelAvailableMessages); ok {
|
||||
channelAvailable = value
|
||||
break
|
||||
}
|
||||
}
|
||||
if channelAvailable == nil ||
|
||||
channelAvailable.ChannelID != channel.ID ||
|
||||
channelAvailable.AvailableMinID != msg.ID ||
|
||||
channelDiff.Pts != newChannelUpdate.Pts {
|
||||
t.Fatalf("offline channel updates = %+v pts=%d, want available boundary %d with unchanged channel pts=%d",
|
||||
channelDiff.OtherUpdates, channelDiff.Pts, msg.ID, newChannelUpdate.Pts)
|
||||
}
|
||||
|
||||
req := &tg.MessagesGetDialogsRequest{OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}
|
||||
var b bin.Buffer
|
||||
if err := req.Encode(&b); err != nil {
|
||||
t.Fatalf("encode get dialogs after clear: %v", err)
|
||||
}
|
||||
enc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &b)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch get dialogs after clear: %v", err)
|
||||
}
|
||||
dialogs, ok := enc.(*tg.MessagesDialogs)
|
||||
if !ok || len(dialogs.Dialogs) != 1 || len(dialogs.Messages) != 1 {
|
||||
t.Fatalf("dialogs after cold projection = %T %+v, want one anchored dialog/message", enc, enc)
|
||||
}
|
||||
dialog := dialogs.Dialogs[0].(*tg.Dialog)
|
||||
service, ok := dialogs.Messages[0].(*tg.MessageService)
|
||||
if !ok || dialog.TopMessage != msg.ID || service.ID != msg.ID {
|
||||
t.Fatalf("cold dialog projection = dialog=%+v message=%T %+v, want history-clear top %d", dialog, dialogs.Messages[0], dialogs.Messages[0], msg.ID)
|
||||
}
|
||||
if _, ok := service.Action.(*tg.MessageActionHistoryClear); !ok {
|
||||
t.Fatalf("cold dialog service action = %T, want messageActionHistoryClear", service.Action)
|
||||
}
|
||||
|
||||
historyReq := &tg.MessagesGetHistoryRequest{
|
||||
Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
Limit: 20,
|
||||
}
|
||||
b.Reset()
|
||||
if err := historyReq.Encode(&b); err != nil {
|
||||
t.Fatalf("encode get history after clear: %v", err)
|
||||
}
|
||||
historyEnc, err := r.Dispatch(WithUserID(ctx, owner.ID), [8]byte{}, 0, &b)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch get history after clear: %v", err)
|
||||
}
|
||||
history, ok := historyEnc.(*tg.MessagesChannelMessages)
|
||||
if !ok || len(history.Messages) != 1 {
|
||||
t.Fatalf("history after cold projection = %T %+v, want one history-clear marker", historyEnc, historyEnc)
|
||||
}
|
||||
historyService, ok := history.Messages[0].(*tg.MessageService)
|
||||
if !ok || historyService.ID != msg.ID {
|
||||
t.Fatalf("cold history projection = %T %+v, want service marker %d", history.Messages[0], history.Messages[0], msg.ID)
|
||||
}
|
||||
if _, ok := historyService.Action.(*tg.MessageActionHistoryClear); !ok {
|
||||
t.Fatalf("cold history service action = %T, want messageActionHistoryClear", historyService.Action)
|
||||
}
|
||||
|
||||
stalePushesBefore := len(sessions.pushedUserIDs())
|
||||
stale, err := r.onChannelsDeleteHistory(WithUserID(ctx, owner.ID), &tg.ChannelsDeleteHistoryRequest{
|
||||
Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash},
|
||||
MaxID: msg.ID - 1,
|
||||
|
|
@ -246,13 +356,16 @@ func TestChannelsDeleteHistoryLocalClearEmitsAvailableMessagesUpdate(t *testing.
|
|||
t.Fatalf("stale delete channel history local: %v", err)
|
||||
}
|
||||
staleUpdates, ok := stale.(*tg.Updates)
|
||||
if !ok || len(staleUpdates.Updates) != 2 {
|
||||
t.Fatalf("stale clear response = %T %+v, want monotonic update plus pts bookkeeping", stale, stale)
|
||||
if !ok || len(staleUpdates.Updates) != 1 {
|
||||
t.Fatalf("stale clear response = %T %+v, want monotonic absolute update", stale, stale)
|
||||
}
|
||||
staleAvailable, ok := staleUpdates.Updates[0].(*tg.UpdateChannelAvailableMessages)
|
||||
if !ok || staleAvailable.ChannelID != channel.ID || staleAvailable.AvailableMinID != msg.ID {
|
||||
t.Fatalf("stale clear update = %#v, want monotonic updateChannelAvailableMessages channel=%d min=%d", staleUpdates.Updates[0], channel.ID, msg.ID)
|
||||
}
|
||||
if got := len(sessions.pushedUserIDs()); got != stalePushesBefore {
|
||||
t.Fatalf("stale clear pushed another online update: before=%d after=%d", stalePushesBefore, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDeleteRejectsInvalidMessageIDsRPC(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -290,27 +290,6 @@ func peerIDsExcept(ids []int64, skipIDs ...int64) []int64 {
|
|||
|
||||
type channelFanoutScope int
|
||||
|
||||
func (r *Router) recordChannelAvailableMessages(ctx context.Context, userID, channelID int64, availableMinID int) domain.UpdateEvent {
|
||||
event := domain.UpdateEvent{
|
||||
UserID: userID,
|
||||
Type: domain.UpdateEventChannelAvailable,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
||||
MaxID: availableMinID,
|
||||
PtsCount: 1,
|
||||
}
|
||||
if r.deps.Updates == nil || userID == 0 || channelID == 0 || availableMinID <= 0 {
|
||||
return event
|
||||
}
|
||||
authKeyID, _ := AuthKeyIDFrom(ctx)
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
recorded, _, err := r.deps.Updates.RecordChannelAvailableMessages(ctx, authKeyID, userID, channelID, availableMinID, rawAuthKeyIDForOrigin(ctx), sessionID)
|
||||
if err != nil {
|
||||
return event
|
||||
}
|
||||
return recorded
|
||||
}
|
||||
|
||||
func (r *Router) recordChannelReadInbox(ctx context.Context, userID int64, read domain.ReadChannelHistoryResult) (domain.UpdateEvent, error) {
|
||||
if !read.Changed || read.ChannelID == 0 {
|
||||
return domain.UpdateEvent{}, nil
|
||||
|
|
|
|||
|
|
@ -222,6 +222,8 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction
|
|||
switch action.Type {
|
||||
case domain.ChannelActionCreate:
|
||||
return &tg.MessageActionChannelCreate{Title: action.Title}
|
||||
case domain.ChannelActionHistoryClear:
|
||||
return &tg.MessageActionHistoryClear{}
|
||||
case domain.ChannelActionChatAddUser, domain.ChannelActionChatJoined:
|
||||
return &tg.MessageActionChatAddUser{Users: append([]int64(nil), action.UserIDs...)}
|
||||
case domain.ChannelActionChatJoinedByLink:
|
||||
|
|
|
|||
|
|
@ -55,11 +55,23 @@ func tgUpdatesDifference(viewerUserID int64, diff domain.UpdateDifference) tg.Up
|
|||
if nudge.ChannelID == 0 {
|
||||
continue
|
||||
}
|
||||
update := &tg.UpdateChannelTooLong{ChannelID: nudge.ChannelID}
|
||||
if nudge.Pts > 0 {
|
||||
update.SetPts(nudge.Pts)
|
||||
if nudge.AvailableMinID > 0 {
|
||||
out.OtherUpdates = append(out.OtherUpdates, &tg.UpdateChannelAvailableMessages{
|
||||
ChannelID: nudge.ChannelID,
|
||||
AvailableMinID: nudge.AvailableMinID,
|
||||
})
|
||||
}
|
||||
// Preserve compatibility for older domain callers that only supplied
|
||||
// Pts: a nudge without an owner-local boundary is a shared channel
|
||||
// update nudge. New store results set ChannelUpdatesDirty explicitly so
|
||||
// a channel can carry both absolute clear and too-long updates.
|
||||
if nudge.ChannelUpdatesDirty || nudge.AvailableMinID == 0 {
|
||||
update := &tg.UpdateChannelTooLong{ChannelID: nudge.ChannelID}
|
||||
if nudge.Pts > 0 {
|
||||
update.SetPts(nudge.Pts)
|
||||
}
|
||||
out.OtherUpdates = append(out.OtherUpdates, update)
|
||||
}
|
||||
out.OtherUpdates = append(out.OtherUpdates, update)
|
||||
if nudge.Channel != nil && nudge.Channel.Channel.ID != 0 {
|
||||
addChannelNudgeChat(out, seenChats, tgChannelChatForView(viewerUserID, *nudge.Channel))
|
||||
}
|
||||
|
|
@ -110,7 +122,7 @@ func tgChannelDifference(viewerUserID int64, diff domain.ChannelDifference) tg.U
|
|||
Users: tgUsersForViewer(viewerUserID, diff.Users),
|
||||
}
|
||||
}
|
||||
if len(diff.Events) == 0 && len(diff.NewMessages) == 0 && len(diff.OtherUpdates) == 0 {
|
||||
if len(diff.Events) == 0 && len(diff.NewMessages) == 0 && len(diff.OtherUpdates) == 0 && diff.AvailableMinID == 0 {
|
||||
return &tg.UpdatesChannelDifferenceEmpty{
|
||||
Final: diff.Final,
|
||||
Pts: diff.Pts,
|
||||
|
|
@ -136,6 +148,12 @@ func tgChannelDifference(viewerUserID int64, diff domain.ChannelDifference) tg.U
|
|||
updates = append(updates, update)
|
||||
}
|
||||
}
|
||||
if diff.AvailableMinID > 0 {
|
||||
updates = append(updates, &tg.UpdateChannelAvailableMessages{
|
||||
ChannelID: diff.Channel.ID,
|
||||
AvailableMinID: diff.AvailableMinID,
|
||||
})
|
||||
}
|
||||
chats := tgChannelDifferenceChats(viewerUserID, diff)
|
||||
users := tgUsersForViewer(viewerUserID, diff.Users)
|
||||
return &tg.UpdatesChannelDifference{
|
||||
|
|
@ -483,14 +501,6 @@ func tgOtherUpdateFromEvent(event domain.UpdateEvent) tg.UpdateClass {
|
|||
Pts: event.Pts,
|
||||
PtsCount: event.PtsCount,
|
||||
}
|
||||
case domain.UpdateEventChannelAvailable:
|
||||
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 || event.MaxID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return &tg.UpdateChannelAvailableMessages{
|
||||
ChannelID: event.Peer.ID,
|
||||
AvailableMinID: event.MaxID,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -496,7 +496,6 @@ type UpdatesService interface {
|
|||
RecordDialogFilterOrder(ctx context.Context, stateAuthKeyID [8]byte, userID int64, order []int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordDialogFiltersReload(ctx context.Context, stateAuthKeyID [8]byte, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordFolderPeers(ctx context.Context, stateAuthKeyID [8]byte, userID int64, peers []domain.FolderPeerUpdate, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordChannelAvailableMessages(ctx context.Context, stateAuthKeyID [8]byte, userID, channelID int64, availableMinID int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordChannelViewForumAsMessages(ctx context.Context, stateAuthKeyID [8]byte, userID, channelID int64, enabled bool, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordChannelDiscussionInbox(ctx context.Context, stateAuthKeyID [8]byte, userID, channelID int64, topicID, maxID int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
RecordDraftMessage(ctx context.Context, stateAuthKeyID [8]byte, userID int64, peer domain.Peer, topMsgID int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error)
|
||||
|
|
|
|||
|
|
@ -74,16 +74,13 @@ func (r *Router) onMessagesDeleteHistory(ctx context.Context, req *tg.MessagesDe
|
|||
})
|
||||
return &tg.MessagesAffectedHistory{Pts: res.Event.Pts, PtsCount: res.Event.PtsCount, Offset: res.Offset}, nil
|
||||
}
|
||||
if res.AvailableMinID > 0 {
|
||||
event := r.recordChannelAvailableMessages(ctx, userID, res.Channel.ID, res.AvailableMinID)
|
||||
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, event.MaxID)
|
||||
updates.Updates = appendAuxPtsBookkeeping(updates.Updates, event)
|
||||
if res.AvailableMinChanged && res.AvailableMinID > 0 {
|
||||
updates := r.channelAvailableMessagesUpdates(userID, res.Channel, res.AvailableMinID)
|
||||
r.pushUserUpdates(ctx, userID, updates)
|
||||
if event.Pts != 0 {
|
||||
return &tg.MessagesAffectedHistory{Pts: event.Pts, PtsCount: event.PtsCount, Offset: res.Offset}, nil
|
||||
}
|
||||
}
|
||||
return &tg.MessagesAffectedHistory{Pts: res.Channel.Pts, PtsCount: 0, Offset: res.Offset}, nil
|
||||
// messages.affectedHistory.pts is the caller's account-state snapshot.
|
||||
// The local channel clear itself consumes no account/channel pts.
|
||||
return r.affectedHistory(ctx, authKeyID, userID, res.Offset)
|
||||
}
|
||||
if peer.Type != domain.PeerTypeUser {
|
||||
return nil, peerIDInvalidErr()
|
||||
|
|
|
|||
|
|
@ -7,11 +7,74 @@ import (
|
|||
"github.com/iamxvbaba/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
appchannels "telesrv/internal/app/channels"
|
||||
appupdates "telesrv/internal/app/updates"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMessagesDeleteHistoryChannelLocalClearReturnsAccountStateWithoutPTS(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
channelStore := memory.NewChannelStore()
|
||||
created, err := channelStore.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: 7,
|
||||
Title: "local clear no pts",
|
||||
Megagroup: true,
|
||||
Date: 1_700_002_100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
sent, err := channelStore.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: 7,
|
||||
ChannelID: created.Channel.ID,
|
||||
RandomID: 210_001,
|
||||
Message: "clear locally",
|
||||
Date: 1_700_002_101,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send channel message: %v", err)
|
||||
}
|
||||
updateSvc := appupdates.NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
|
||||
stateBefore, err := updateSvc.CurrentState(ctx, 7)
|
||||
if err != nil {
|
||||
t.Fatalf("account state before clear: %v", err)
|
||||
}
|
||||
sessions := &captureSessions{}
|
||||
r := New(Config{}, Deps{
|
||||
Channels: appchannels.NewService(channelStore),
|
||||
Updates: updateSvc,
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
affected, err := r.onMessagesDeleteHistory(WithUserID(ctx, 7), &tg.MessagesDeleteHistoryRequest{
|
||||
Peer: &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash},
|
||||
MaxID: sent.Message.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("messages.deleteHistory local channel: %v", err)
|
||||
}
|
||||
if affected.Pts != stateBefore.Pts || affected.PtsCount != 0 || affected.Offset != 0 {
|
||||
t.Fatalf("affected history = %+v, want account pts=%d pts_count=0 offset=0", affected, stateBefore.Pts)
|
||||
}
|
||||
stateAfter, err := updateSvc.CurrentState(ctx, 7)
|
||||
if err != nil {
|
||||
t.Fatalf("account state after clear: %v", err)
|
||||
}
|
||||
if stateAfter.Pts != stateBefore.Pts {
|
||||
t.Fatalf("local channel clear advanced account pts: before=%d after=%d", stateBefore.Pts, stateAfter.Pts)
|
||||
}
|
||||
pushed := sessions.snapshot()
|
||||
updates, ok := pushed.message.(*tg.Updates)
|
||||
if !ok || len(updates.Updates) != 1 {
|
||||
t.Fatalf("pushed local clear = %T %+v, want one available update", pushed.message, pushed.message)
|
||||
}
|
||||
available, ok := updates.Updates[0].(*tg.UpdateChannelAvailableMessages)
|
||||
if !ok || available.ChannelID != created.Channel.ID || available.AvailableMinID != sent.Message.ID {
|
||||
t.Fatalf("available update = %#v, want channel=%d min=%d", updates.Updates[0], created.Channel.ID, sent.Message.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesDeleteHistoryChannelReturnsOffsetForBoundedPage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
|
|||
|
|
@ -604,14 +604,15 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) {
|
|||
return &tg.MessagesMessages{}, nil
|
||||
}
|
||||
history, err := r.deps.Channels.GetHistory(ctx, userID, domain.ChannelHistoryFilter{
|
||||
ChannelID: filter.Peer.ID,
|
||||
OffsetID: filter.OffsetID,
|
||||
OffsetDate: filter.OffsetDate,
|
||||
AddOffset: filter.AddOffset,
|
||||
Limit: filter.Limit,
|
||||
MaxID: filter.MaxID,
|
||||
MinID: filter.MinID,
|
||||
Hash: filter.Hash,
|
||||
ChannelID: filter.Peer.ID,
|
||||
OffsetID: filter.OffsetID,
|
||||
OffsetDate: filter.OffsetDate,
|
||||
AddOffset: filter.AddOffset,
|
||||
Limit: filter.Limit,
|
||||
MaxID: filter.MaxID,
|
||||
MinID: filter.MinID,
|
||||
Hash: filter.Hash,
|
||||
IncludeHistoryClearAnchor: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, channelInvalidErr(err)
|
||||
|
|
|
|||
|
|
@ -284,15 +284,6 @@ func (s *captureUpdates) RecordFolderPeers(_ context.Context, authKeyID [8]byte,
|
|||
return s.recordCapturedEvent(authKeyID, userID, domain.UpdateEvent{Type: domain.UpdateEventFolderPeers, FolderPeers: append([]domain.FolderPeerUpdate(nil), peers...)})
|
||||
}
|
||||
|
||||
func (s *captureUpdates) RecordChannelAvailableMessages(_ context.Context, authKeyID [8]byte, userID, channelID int64, availableMinID int, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error) {
|
||||
s.captureExclude(excludeAuthKeyID, excludeSessionID)
|
||||
return s.recordCapturedEvent(authKeyID, userID, domain.UpdateEvent{
|
||||
Type: domain.UpdateEventChannelAvailable,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
||||
MaxID: availableMinID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *captureUpdates) RecordChannelViewForumAsMessages(_ context.Context, authKeyID [8]byte, userID, channelID int64, enabled bool, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, domain.UpdateState, error) {
|
||||
s.captureExclude(excludeAuthKeyID, excludeSessionID)
|
||||
return s.recordCapturedEvent(authKeyID, userID, domain.UpdateEvent{
|
||||
|
|
|
|||
|
|
@ -102,6 +102,21 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD
|
|||
return nil, internalErr()
|
||||
}
|
||||
st.ChannelNudges = r.accountChannelDifferenceNudges(ctx, userID, req.Date)
|
||||
if !st.Partial {
|
||||
for _, nudge := range st.ChannelNudges {
|
||||
if nudge.AvailableMinID <= 0 {
|
||||
continue
|
||||
}
|
||||
// No-PTS owner-local clear recovery is date-indexed. Advance only
|
||||
// on the final account page and never beyond the server clock; the
|
||||
// indexed query deliberately overlaps equality, so a same-second
|
||||
// reconnect can receive an idempotent duplicate rather than miss.
|
||||
if now := int(r.clock.Now().Unix()); now > st.State.Date {
|
||||
st.State.Date = now
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 密聊设备级 qts 消息(独立于账号级 pts 事件):按当前设备 req.Qts 补回。
|
||||
encMsgs, newQts := r.encryptedDifference(ctx, req.Qts)
|
||||
// 密聊握手/已读状态事件(无 qts):按未投递标记补回 OtherUpdates。
|
||||
|
|
@ -154,10 +169,19 @@ func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int6
|
|||
}
|
||||
}
|
||||
for _, item := range dirty {
|
||||
if len(out) >= maxNudges {
|
||||
break
|
||||
}
|
||||
if item.ChannelID == 0 {
|
||||
continue
|
||||
}
|
||||
nudge := domain.ChannelDifferenceNudge{ChannelID: item.ChannelID, Pts: item.Pts}
|
||||
nudge := domain.ChannelDifferenceNudge{
|
||||
ChannelID: item.ChannelID,
|
||||
Pts: item.Pts,
|
||||
ChannelUpdatesDirty: item.ChannelUpdatesDirty,
|
||||
AvailableMinID: item.AvailableMinID,
|
||||
HistoryClearDate: item.HistoryClearDate,
|
||||
}
|
||||
if view, ok := viewsByID[item.ChannelID]; ok {
|
||||
nudge.Channel = &view
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue