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

@ -124,6 +124,13 @@ func TestPublicChannelPreviewRPCsAllowNonMember(t *testing.T) {
if !ok || !historyChat.Left || historyChat.ID != public.Channel.ID {
t.Fatalf("history chat = %T %+v, want left public channel", history.Chats[0], history.Chats[0])
}
readOK, err := r.onChannelsReadHistory(WithUserID(ctx, viewer.ID), &tg.ChannelsReadHistoryRequest{
Channel: input,
MaxID: sent.Message.ID,
})
if err != nil || !readOK {
t.Fatalf("non-member readHistory public preview = %v err=%v, want successful no-op", readOK, err)
}
viewerCtx := WithSessionID(WithRawAuthKeyID(WithUserID(ctx, viewer.ID), [8]byte{9, 2}), 9202)
diff, err := r.onUpdatesGetChannelDifference(viewerCtx, &tg.UpdatesGetChannelDifferenceRequest{
@ -266,3 +273,44 @@ func TestPublicChannelPreviewRPCsAllowNonMember(t *testing.T) {
t.Fatalf("joined peer dialog chat = %T %+v, want active channel with left=false", joinedPeerDialogs.Chats[0], joinedPeerDialogs.Chats[0])
}
}
func TestChannelsReadHistoryAllowsSyntheticMonoforumViewers(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
owner, err := userStore.Create(ctx, domain.User{AccessHash: 92101, Phone: "15550092101", FirstName: "Owner"})
if err != nil {
t.Fatal(err)
}
subscriber, err := userStore.Create(ctx, domain.User{AccessHash: 92102, Phone: "15550092102", FirstName: "Subscriber"})
if err != nil {
t.Fatal(err)
}
channelStore := memory.NewChannelStore()
channels := appchannels.NewService(channelStore)
parent, err := channels.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Monoforum Read RPC", Broadcast: true, Date: 1_700_011_000,
})
if err != nil {
t.Fatal(err)
}
enabled, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, parent.Channel.ID, 0, true)
if err != nil {
t.Fatal(err)
}
mono, err := channelStore.GetChannelByID(ctx, enabled.Channel.LinkedMonoforumID)
if err != nil {
t.Fatal(err)
}
r := New(Config{}, Deps{
Users: appusers.NewService(userStore), Channels: channels,
}, zaptest.NewLogger(t), clock.System)
for _, userID := range []int64{owner.ID, subscriber.ID} {
ok, err := r.onChannelsReadHistory(WithUserID(ctx, userID), &tg.ChannelsReadHistoryRequest{
Channel: &tg.InputChannel{ChannelID: mono.ID, AccessHash: mono.AccessHash},
MaxID: mono.TopMessageID,
})
if err != nil || !ok {
t.Fatalf("synthetic monoforum read for %d = %v err=%v, want successful no-op", userID, ok, err)
}
}
}

View file

@ -58,6 +58,9 @@ func (r *Router) onChannelsReadHistory(ctx context.Context, req *tg.ChannelsRead
if r.deps.Channels == nil {
return true, nil
}
if req == nil {
return false, inputRequestInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
@ -75,6 +78,9 @@ func (r *Router) onChannelsReadHistory(ctx context.Context, req *tg.ChannelsRead
if err != nil {
return false, channelInvalidErr(err)
}
if read.ReadOnly {
return true, nil
}
if _, err := r.recordChannelReadInbox(ctx, userID, read); err != nil {
return false, err
}

View file

@ -19,8 +19,6 @@ const (
)
const (
minSuggestedPostScheduleDelay = 5 * 60
maxSuggestedPostScheduleDelay = 31 * 24 * 60 * 60
maxSuggestedPostRejectComment = 1024
)
@ -54,7 +52,7 @@ func (r *Router) onMessagesToggleSuggestedPostApproval(ctx context.Context, req
}
now := int(r.clock.Now().Unix())
scheduleDate, hasScheduleDate := req.GetScheduleDate()
if hasScheduleDate && (req.Reject || scheduleDate < now+minSuggestedPostScheduleDelay || scheduleDate > now+maxSuggestedPostScheduleDelay) {
if hasScheduleDate && (req.Reject || scheduleDate <= 0) {
return nil, scheduleDateInvalidErr()
}
result, err := service.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{

View file

@ -3,6 +3,7 @@ package rpc
import (
"context"
"testing"
"time"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/clock"
@ -110,6 +111,107 @@ func TestMessagesToggleSuggestedPostApprovalRegisteredAndProjectsLifecycle(t *te
}
}
func TestMessagesToggleSuggestedPostApprovalAcceptsDelayedAbsoluteDate(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
owner, err := users.Create(ctx, domain.User{AccessHash: 111, Phone: "15551110111", FirstName: "Owner"})
if err != nil {
t.Fatal(err)
}
subscriber, err := users.Create(ctx, domain.User{AccessHash: 112, Phone: "15551110112", FirstName: "Subscriber"})
if err != nil {
t.Fatal(err)
}
channelsStore := memory.NewChannelStore()
channels := appchannels.NewService(channelsStore)
created, err := channels.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
Title: "Delayed Suggested", Broadcast: true, Date: 1_700_030_000,
})
if err != nil {
t.Fatal(err)
}
enabled, err := channelsStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true)
if err != nil {
t.Fatal(err)
}
mono, err := channelsStore.GetChannelByID(ctx, enabled.Channel.LinkedMonoforumID)
if err != nil {
t.Fatal(err)
}
saved := domain.Peer{Type: domain.PeerTypeUser, ID: subscriber.ID}
suggestion, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: saved,
RandomID: 101, Message: "delayed RPC suggestion", SuggestedPost: &domain.SuggestedPost{}, Date: 1_700_030_010,
})
if err != nil {
t.Fatal(err)
}
const now = 1_700_030_100
req := &tg.MessagesToggleSuggestedPostApprovalRequest{
Peer: &tg.InputPeerChannel{ChannelID: mono.ID, AccessHash: mono.AccessHash},
MsgID: suggestion.Message.ID,
}
req.SetScheduleDate(now + 2*60)
dispatch := func(t *testing.T, router *Router, layer int) *tg.Updates {
t.Helper()
var raw bin.Buffer
if err := req.Encode(&raw); err != nil {
t.Fatal(err)
}
response, err := router.Dispatch(WithLayer(WithUserID(ctx, owner.ID), layer), [8]byte{}, 0, &raw)
if err != nil {
t.Fatalf("dispatch delayed approval at Layer %d: %v", layer, err)
}
updates, ok := response.(*tg.Updates)
if !ok {
t.Fatalf("delayed approval response=%T", response)
}
return updates
}
first := dispatch(t, New(Config{}, Deps{
Users: appusers.NewService(users), Channels: channels,
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now, 0)}), 228)
if len(first.Updates) != 2 {
t.Fatalf("near-future approval updates=%d, want edit + approval service", len(first.Updates))
}
replay := dispatch(t, New(Config{}, Deps{
Users: appusers.NewService(users), Channels: channels,
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now+10*60, 0)}), 227)
if len(replay.Updates) != 2 {
t.Fatalf("late duplicate updates=%d, want persisted edit + approval service", len(replay.Updates))
}
due, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: saved,
RandomID: 102, Message: "due RPC suggestion", SuggestedPost: &domain.SuggestedPost{}, Date: now + 1,
})
if err != nil {
t.Fatal(err)
}
dueReq := &tg.MessagesToggleSuggestedPostApprovalRequest{
Peer: &tg.InputPeerChannel{ChannelID: mono.ID, AccessHash: mono.AccessHash},
MsgID: due.Message.ID,
}
dueReq.SetScheduleDate(now - 1)
var dueRaw bin.Buffer
if err := dueReq.Encode(&dueRaw); err != nil {
t.Fatal(err)
}
response, err := New(Config{}, Deps{
Users: appusers.NewService(users), Channels: channels,
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(now+2, 0)}).Dispatch(
WithLayer(WithUserID(ctx, owner.ID), 228), [8]byte{}, 0, &dueRaw,
)
if err != nil {
t.Fatalf("dispatch already-due approval: %v", err)
}
dueUpdates, ok := response.(*tg.Updates)
if !ok || len(dueUpdates.Updates) != 3 {
t.Fatalf("already-due response=%T %#v, want edit + approval + published", response, response)
}
}
func TestSuggestedPostTLProjectionSeparatesSuggestionAndPublishedPaymentFlags(t *testing.T) {
original := domain.ChannelMessage{ChannelID: 10, ID: 1, SenderUserID: 20, From: domain.Peer{Type: domain.PeerTypeUser, ID: 20}, SavedPeer: domain.Peer{Type: domain.PeerTypeUser, ID: 20}, Date: 100, Body: "proposal", SuggestedPost: &domain.SuggestedPost{Accepted: true, Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}}
proposal := tgChannelMessage(20, original).(*tg.Message)