fix: sync harden channel reads and suggested approvals

This commit is contained in:
iamxvbaba 2026-07-28 16:22:33 +08:00
parent 037ce017d4
commit 209a0d279b
14 changed files with 467 additions and 26 deletions

View file

@ -41,6 +41,23 @@ func TestSetPaidMessagesPriceCreatesAndReusesMonoforum(t *testing.T) {
if mono.TopMessageID == 0 || mono.Pts == 0 {
t.Fatalf("monoforum top/pts = %d/%d, want service top message", mono.TopMessageID, mono.Pts)
}
for _, userID := range []int64{1, 42} {
read, err := store.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
UserID: userID, ChannelID: monoID, MaxID: mono.TopMessageID, Date: 1_700_000_901,
})
if err != nil {
t.Fatalf("synthetic monoforum read for %d: %v", userID, err)
}
if !read.ReadOnly || read.Changed || read.MaxID != mono.TopMessageID {
t.Fatalf("synthetic monoforum read for %d = %+v, want read-only no-op at %d", userID, read, mono.TopMessageID)
}
if _, exists := store.members[monoID][userID]; exists {
t.Fatalf("synthetic monoforum read persisted member for %d", userID)
}
if _, exists := store.dialogs[userID][monoID]; exists {
t.Fatalf("synthetic monoforum read persisted dialog for %d", userID)
}
}
dialogs, err := store.ListChannelDialogs(ctx, 1, domain.DialogFilter{Limit: 100})
if err != nil {
t.Fatalf("list dialogs after enable: %v", err)

View file

@ -252,7 +252,7 @@ func (s *ChannelStore) ReadChannelMentions(_ context.Context, req domain.ReadCha
func (s *ChannelStore) ReadChannelHistory(_ context.Context, req domain.ReadChannelHistoryRequest) (domain.ReadChannelHistoryResult, error) {
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
channel, _, readOnly, err := s.channelForViewerLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ReadChannelHistoryResult{}, err
}
@ -260,6 +260,15 @@ func (s *ChannelStore) ReadChannelHistory(_ context.Context, req domain.ReadChan
if maxID <= 0 || maxID > channel.TopMessageID {
maxID = channel.TopMessageID
}
if readOnly {
return domain.ReadChannelHistoryResult{
ChannelID: req.ChannelID,
MaxID: maxID,
ReadOnly: true,
Pts: channel.Pts,
Forum: channel.Forum,
}, nil
}
member := s.members[req.ChannelID][req.UserID]
previous := member.ReadInboxMaxID
changed := maxID > member.ReadInboxMaxID

View file

@ -85,8 +85,12 @@ func (s *ChannelStore) toggleSuggestedPostApprovalLocked(req domain.ToggleSugges
if req.ScheduleDate > 0 {
scheduleDate = req.ScheduleDate
}
if !req.Reject && scheduleDate > 0 && (scheduleDate < req.Date+5*60 || scheduleDate > req.Date+31*24*60*60) {
return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid
if !req.Reject {
effectiveDate, scheduleErr := domain.EffectiveSuggestedPostPublishDate(scheduleDate, req.Date)
if scheduleErr != nil {
return domain.ToggleSuggestedPostApprovalResult{}, scheduleErr
}
scheduleDate = effectiveDate
}
recipients := s.monoforumRecipientsLocked(parent.ID, original.SavedPeer.ID)
base := domain.ToggleSuggestedPostApprovalResult{
@ -136,13 +140,6 @@ func (s *ChannelStore) toggleSuggestedPostApprovalLocked(req domain.ToggleSugges
original.SuggestedPost.Accepted = true
original.SuggestedPost.Rejected = false
effectivePublishDate := scheduleDate
if effectivePublishDate == 0 {
// TDesktop deliberately omits schedule_date for "Publish Now", but
// renders the approval service action as an absolute date. Persist one
// effective publication timestamp across the edited suggestion, action
// and approval record instead of leaking an accepted zero date.
effectivePublishDate = req.Date
}
original.SuggestedPost.ScheduleDate = effectivePublishDate
original.Pts = s.nextChannelPtsLocked(mono.ID)
s.messages[mono.ID][idx] = cloneChannelMessage(original)

View file

@ -179,6 +179,89 @@ func TestSuggestedPostLowBalanceRetryScheduleAndRoleMatrix(t *testing.T) {
}
}
func TestSuggestedPostApprovalAcceptsDelayedScheduleAndKeepsPTSIdempotent(t *testing.T) {
ctx := context.Background()
store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t)
const now = 1_700_010_000
near, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber,
RandomID: 71, Message: "near schedule", SuggestedPost: &domain.SuggestedPost{}, Date: now - 10,
})
if err != nil {
t.Fatal(err)
}
nearDate := now + 2*60
accepted, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{
UserID: 1, MonoforumID: mono.ID, MessageID: near.Message.ID,
ScheduleDate: nearDate, Date: now,
})
if err != nil {
t.Fatalf("accept schedule below former five-minute gate: %v", err)
}
if accepted.State != domain.SuggestedPostStateScheduled || accepted.Published != nil ||
accepted.OriginalMessage.SuggestedPost.ScheduleDate != nearDate ||
accepted.ServiceMessage.Action == nil ||
accepted.ServiceMessage.Action.SuggestedPostScheduleDate != nearDate {
t.Fatalf("near schedule approval = %+v, want scheduled at %d", accepted, nearDate)
}
monoPts, parentPts := store.channels[mono.ID].Pts, store.channels[parent.ID].Pts
monoEvents, parentEvents := len(store.events[mono.ID]), len(store.events[parent.ID])
replay, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{
UserID: 1, MonoforumID: mono.ID, MessageID: near.Message.ID,
ScheduleDate: nearDate, Date: now + 10*60,
})
if err != nil || !replay.Duplicate {
t.Fatalf("late duplicate approval = %+v err=%v", replay, err)
}
if store.channels[mono.ID].Pts != monoPts || store.channels[parent.ID].Pts != parentPts ||
len(store.events[mono.ID]) != monoEvents || len(store.events[parent.ID]) != parentEvents {
t.Fatal("late duplicate approval advanced PTS or appended an event")
}
due, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber,
RandomID: 72, Message: "already due", SuggestedPost: &domain.SuggestedPost{}, Date: now + 20,
})
if err != nil {
t.Fatal(err)
}
approvedAt := now + 30
dueResult, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{
UserID: 1, MonoforumID: mono.ID, MessageID: due.Message.ID,
ScheduleDate: now - 1, Date: approvedAt,
})
if err != nil {
t.Fatalf("approve already-due schedule: %v", err)
}
if dueResult.State != domain.SuggestedPostStateCompleted || dueResult.Published == nil ||
dueResult.OriginalMessage.SuggestedPost.ScheduleDate != approvedAt ||
dueResult.ServiceMessage.Action == nil ||
dueResult.ServiceMessage.Action.SuggestedPostScheduleDate != approvedAt {
t.Fatalf("due schedule approval = %+v, want immediate publish at %d", dueResult, approvedAt)
}
far, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber,
RandomID: 73, Message: "too far", SuggestedPost: &domain.SuggestedPost{}, Date: now + 40,
})
if err != nil {
t.Fatal(err)
}
monoPts, parentPts = store.channels[mono.ID].Pts, store.channels[parent.ID].Pts
monoEvents, parentEvents = len(store.events[mono.ID]), len(store.events[parent.ID])
if _, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{
UserID: 1, MonoforumID: mono.ID, MessageID: far.Message.ID,
ScheduleDate: approvedAt + domain.MaxSuggestedPostScheduleDelay + 1, Date: approvedAt,
}); !errors.Is(err, domain.ErrSuggestedPostInvalid) {
t.Fatalf("far schedule err=%v, want suggested post invalid", err)
}
if store.channels[mono.ID].Pts != monoPts || store.channels[parent.ID].Pts != parentPts ||
len(store.events[mono.ID]) != monoEvents || len(store.events[parent.ID]) != parentEvents {
t.Fatal("far schedule rejection advanced PTS or appended an event")
}
}
func TestChannelAuthoredSuggestedPostAcceptedBySubscriber(t *testing.T) {
ctx := context.Background()
store, _, mono, subscriber := newSuggestedPostMemoryFixture(t)