chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -2,6 +2,7 @@ package updates
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
@ -77,6 +78,38 @@ func TestRecordReadHistoryFeedsGetDifference(t *testing.T) {
}
}
func TestRecordChannelReadHistoryKeepsChannelPtsPayload(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 7
svc := NewService(memory.NewUpdateStateStore(), memory.NewUpdateEventStore())
ownerUserID := int64(1000000001)
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: 7001}
event, state, err := svc.RecordReadHistory(ctx, authKeyID, ownerUserID, domain.ReadHistoryResult{
OwnerUserID: ownerUserID,
Peer: peer,
MaxID: 11,
StillUnreadCount: 3,
ChannelPts: 77,
Changed: true,
}, 0)
if err != nil {
t.Fatalf("RecordReadHistory: %v", err)
}
if event.Pts != 1 || state.Pts != 1 || event.ChannelPts != 77 {
t.Fatalf("event/state = %+v / %+v, want account pts=1 and channel pts payload=77", event, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if len(diff.Events) != 1 || diff.Events[0].Peer != peer || diff.Events[0].ChannelPts != 77 {
t.Fatalf("diff = %+v, want recorded channel read payload with channel pts=77", diff)
}
}
func TestRecordSettingsEventsFeedGetDifference(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
@ -88,31 +121,34 @@ func TestRecordSettingsEventsFeedGetDifference(t *testing.T) {
if _, _, err := svc.RecordContactsReset(ctx, authKeyID, ownerUserID, 0); err != nil {
t.Fatalf("RecordContactsReset: %v", err)
}
if _, _, err := svc.RecordDialogPinned(ctx, authKeyID, ownerUserID, peer, true, 0); err != nil {
if _, _, err := svc.RecordDialogPinned(ctx, authKeyID, ownerUserID, peer, true, 0, 0); err != nil {
t.Fatalf("RecordDialogPinned: %v", err)
}
order := []domain.Peer{peer}
if _, _, err := svc.RecordPinnedDialogs(ctx, authKeyID, ownerUserID, order, 0); err != nil {
if _, _, err := svc.RecordPinnedDialogs(ctx, authKeyID, ownerUserID, 0, order, 0); err != nil {
t.Fatalf("RecordPinnedDialogs: %v", err)
}
if _, _, err := svc.RecordDialogUnreadMark(ctx, authKeyID, ownerUserID, peer, false, 0); err != nil {
t.Fatalf("RecordDialogUnreadMark: %v", err)
}
settings := domain.PeerSettings{ShareContact: true}
stateEvent, state, err := svc.RecordPeerSettings(ctx, authKeyID, ownerUserID, peer, settings, 0)
if err != nil {
if _, _, err := svc.RecordPeerSettings(ctx, authKeyID, ownerUserID, peer, settings, 0); err != nil {
t.Fatalf("RecordPeerSettings: %v", err)
}
if stateEvent.Pts != 5 || state.Pts != 5 {
t.Fatalf("last event/state = %+v / %+v, want pts=5", stateEvent, state)
stateEvent, state, err := svc.RecordPeerStoryBlocked(ctx, authKeyID, ownerUserID, peer, true, 0)
if err != nil {
t.Fatalf("RecordPeerStoryBlocked: %v", err)
}
if stateEvent.Pts != 6 || state.Pts != 6 {
t.Fatalf("last event/state = %+v / %+v, want pts=6", stateEvent, state)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerUserID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 5 || len(diff.Events) != 5 {
t.Fatalf("diff = %+v, want five settings events", diff)
if diff.State.Pts != 6 || len(diff.Events) != 6 {
t.Fatalf("diff = %+v, want six settings events", diff)
}
wantTypes := []domain.UpdateEventType{
domain.UpdateEventContactsReset,
@ -120,6 +156,7 @@ func TestRecordSettingsEventsFeedGetDifference(t *testing.T) {
domain.UpdateEventPinnedDialogs,
domain.UpdateEventDialogUnreadMark,
domain.UpdateEventPeerSettings,
domain.UpdateEventPeerStoryBlocked,
}
for i, typ := range wantTypes {
if diff.Events[i].Type != typ || diff.Events[i].Pts != i+1 || diff.Events[i].PtsCount != 1 {
@ -138,6 +175,9 @@ func TestRecordSettingsEventsFeedGetDifference(t *testing.T) {
if diff.Events[4].Peer != peer || !diff.Events[4].Settings.ShareContact {
t.Fatalf("peer settings event = %+v, want peer and settings", diff.Events[4])
}
if diff.Events[5].Peer != peer || !diff.Events[5].Bool {
t.Fatalf("peer story blocked event = %+v, want peer and blocked=true", diff.Events[5])
}
}
func TestRecordSettingsEventUsesDispatchAppender(t *testing.T) {
@ -148,7 +188,7 @@ func TestRecordSettingsEventUsesDispatchAppender(t *testing.T) {
svc := NewService(memory.NewUpdateStateStore(), events)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
event, state, err := svc.RecordDialogPinned(ctx, authKeyID, 1000000001, peer, true, 42)
event, state, err := svc.RecordDialogPinned(ctx, authKeyID, 1000000001, peer, true, 0, 42)
if err != nil {
t.Fatalf("RecordDialogPinned: %v", err)
}
@ -160,6 +200,177 @@ func TestRecordSettingsEventUsesDispatchAppender(t *testing.T) {
}
}
func TestRecordSettingsEventDispatchFailureDoesNotRecordEvent(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 6
events := &failingDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
_, _, err := svc.RecordDialogPinned(ctx, authKeyID, 1000000001, domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}, true, 0, 42)
if !errors.Is(err, errDispatchFailed) {
t.Fatalf("RecordDialogPinned err = %v, want dispatch failure", err)
}
diff, err := svc.GetDifference(ctx, authKeyID, 1000000001, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 0 || len(diff.Events) != 0 {
t.Fatalf("diff after dispatch failure = %+v, want no durable event before allocated append commits", diff)
}
}
func TestRecordPeerStoryBlockedUsesDispatchAppender(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 7
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
event, state, err := svc.RecordPeerStoryBlocked(ctx, authKeyID, 1000000001, peer, true, 91)
if err != nil {
t.Fatalf("RecordPeerStoryBlocked: %v", err)
}
if event.Pts != 1 || state.Pts != 1 || !event.LacksWirePts() {
t.Fatalf("event/state = %+v / %+v, want first aux pts event", event, state)
}
if !events.dispatched || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 91 || events.event.Type != domain.UpdateEventPeerStoryBlocked || events.event.Peer != peer || !events.event.Bool {
t.Fatalf("dispatch capture = %+v exclude_auth=%v exclude_session=%d dispatched=%v, want peer_story_blocked outbox", events.event, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
}
func TestRecordStoryUsesDispatchAppenderExcludeCurrentSession(t *testing.T) {
ctx := context.Background()
authKeyID := [8]byte{8, 1, 0}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001}
story := domain.Story{
Owner: owner,
ID: 3,
Date: 1700000100,
ExpireDate: 1700086500,
Public: true,
Caption: "owner story",
}
event, state, err := svc.RecordStory(ctx, authKeyID, owner.ID, story, 1234)
if err != nil {
t.Fatalf("RecordStory: %v", err)
}
if event.Type != domain.UpdateEventStory || event.Pts != 1 || event.PtsCount != 1 || state.Pts != 1 {
t.Fatalf("event/state = %+v / %+v, want first story pts event", event, state)
}
if !events.dispatched || events.userID != owner.ID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 1234 {
t.Fatalf("dispatch capture = user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
if events.event.Type != domain.UpdateEventStory || events.event.Peer != owner || events.event.Story.ID != story.ID {
t.Fatalf("dispatch event = %+v, want story update for owner story", events.event)
}
}
func TestRecordStoryReadAndSentReactionExcludeCurrentSession(t *testing.T) {
ctx := context.Background()
authKeyID := [8]byte{8, 1, 4}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(memory.NewUpdateStateStore(), events)
viewerID := int64(1000000002)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000001}
story := domain.Story{
Owner: peer,
ID: 5,
Date: 1700000200,
ExpireDate: 1700086600,
Public: true,
}
event, state, err := svc.RecordReadStories(ctx, authKeyID, viewerID, domain.StoryReadResult{
ViewerID: viewerID,
Peer: peer,
MaxReadID: story.ID,
Advanced: true,
Date: 1700000201,
}, 2233)
if err != nil {
t.Fatalf("RecordReadStories: %v", err)
}
if event.Type != domain.UpdateEventReadStories || event.Pts != 1 || state.Pts != 1 {
t.Fatalf("read event/state = %+v / %+v, want first read story pts event", event, state)
}
if !events.dispatched || events.userID != viewerID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 2233 || events.event.Type != domain.UpdateEventReadStories || events.event.MaxID != story.ID {
t.Fatalf("read dispatch capture = %+v user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.event, events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "🔥"}
event, state, err = svc.RecordSentStoryReaction(ctx, authKeyID, viewerID, domain.StoryReactionResult{
ViewerID: viewerID,
Peer: peer,
StoryID: story.ID,
Story: story,
Reaction: reaction,
Changed: true,
Date: 1700000202,
}, 2233)
if err != nil {
t.Fatalf("RecordSentStoryReaction: %v", err)
}
if event.Type != domain.UpdateEventSentStoryReaction || event.Pts != 2 || state.Pts != 2 {
t.Fatalf("reaction event/state = %+v / %+v, want second sent story reaction pts event", event, state)
}
if !events.dispatched || events.userID != viewerID || events.excludeAuthKeyID != authKeyID || events.excludeSessionID != 2233 || events.event.Type != domain.UpdateEventSentStoryReaction || events.event.Reaction == nil || events.event.Reaction.Emoticon != "🔥" {
t.Fatalf("reaction dispatch capture = %+v user %d exclude_auth=%v exclude_session=%d dispatched=%v, want current session excluded", events.event, events.userID, events.excludeAuthKeyID, events.excludeSessionID, events.dispatched)
}
}
func TestRecordNewStoryReactionDispatchesWithoutSavingDeviceState(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 5
states := &captureStateStore{}
events := &captureDispatchAppender{UpdateEventStore: memory.NewUpdateEventStore()}
svc := NewService(states, events)
ownerID := int64(1000000001)
viewerID := int64(1000000002)
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "👍"}
event, state, err := svc.RecordNewStoryReaction(ctx, authKeyID, 0, domain.StoryReactionResult{
ViewerID: viewerID,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
StoryID: 3,
Story: domain.Story{
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
ID: 3,
Date: 1700000100,
},
Reaction: reaction,
Date: 1700000101,
}, 0)
if err != nil {
t.Fatalf("RecordNewStoryReaction: %v", err)
}
if event.Type != domain.UpdateEventNewStoryReaction || event.UserID != ownerID || event.Peer.ID != viewerID || event.Reaction == nil || event.Reaction.Emoticon != "👍" {
t.Fatalf("event = %+v, want owner-side new story reaction from viewer", event)
}
if state.Pts != 1 || state.Seq != 0 {
t.Fatalf("state = %+v, want first account pts", state)
}
if states.saveCount != 0 {
t.Fatalf("state saves = %d, want no device state save for remote owner notification", states.saveCount)
}
if !events.dispatched || events.userID != ownerID || events.event.Type != domain.UpdateEventNewStoryReaction {
t.Fatalf("dispatch capture = %+v user=%d dispatched=%v, want owner outbox event", events.event, events.userID, events.dispatched)
}
diff, err := svc.GetDifference(ctx, authKeyID, ownerID, domain.UpdateState{})
if err != nil {
t.Fatalf("GetDifference: %v", err)
}
if diff.State.Pts != 1 || len(diff.Events) != 1 || diff.Events[0].Type != domain.UpdateEventNewStoryReaction {
t.Fatalf("diff = %+v, want one durable new story reaction", diff)
}
}
func TestClearAuthKeyDropsStateAndEvents(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
@ -233,6 +444,52 @@ func TestDeleteMessagesPtsRangeFeedsGetDifference(t *testing.T) {
}
}
// TestAcknowledgeCurrentStateAdvancesConfirmedWatermark 验证 updates.getState
// 的语义:返回账号当前最新连续 pts(而非设备旧确认水位),并把确认水位推进
// 到此——TDesktop 不持久化 pts,启动靠 getState+getDialogs 快照对齐,返回旧
// 水位会诱导其重放快照前差分(未读重复累计、dialog 预览被旧消息抢占)。
func TestAcknowledgeCurrentStateAdvancesConfirmedWatermark(t *testing.T) {
ctx := context.Background()
var authKeyID [8]byte
authKeyID[0] = 11
userID := int64(1000000001)
events := memory.NewUpdateEventStore()
svc := NewService(memory.NewUpdateStateStore(), events)
if err := events.Append(ctx, userID, domain.UpdateEvent{
UserID: userID, Type: domain.UpdateEventNewMessage, Pts: 1, PtsCount: 1,
Date: 1700000001, Message: domain.Message{ID: 1, OwnerUserID: userID},
}); err != nil {
t.Fatalf("append: %v", err)
}
// 设备确认水位停在 pts=1 后账号又推进两格。
if _, err := svc.GetDifference(ctx, authKeyID, userID, domain.UpdateState{Pts: 1}); err != nil {
t.Fatalf("GetDifference: %v", err)
}
for pts := 2; pts <= 3; pts++ {
if err := events.Append(ctx, userID, domain.UpdateEvent{
UserID: userID, Type: domain.UpdateEventNewMessage, Pts: pts, PtsCount: 1,
Date: 1700000001 + pts, Message: domain.Message{ID: pts, OwnerUserID: userID},
}); err != nil {
t.Fatalf("append pts=%d: %v", pts, err)
}
}
st, err := svc.AcknowledgeCurrentState(ctx, authKeyID, userID)
if err != nil {
t.Fatalf("AcknowledgeCurrentState: %v", err)
}
if st.Pts != 3 {
t.Fatalf("acknowledged state pts = %d, want account current 3", st.Pts)
}
confirmed, err := svc.GetState(ctx, authKeyID, userID)
if err != nil {
t.Fatalf("GetState after acknowledge: %v", err)
}
if confirmed.Pts != 3 {
t.Fatalf("confirmed watermark = %d, want advanced to 3", confirmed.Pts)
}
}
type captureDispatchAppender struct {
*memory.UpdateEventStore
dispatched bool
@ -242,11 +499,74 @@ type captureDispatchAppender struct {
excludeSessionID int64
}
func (s *captureDispatchAppender) AppendWithDispatch(ctx context.Context, userID int64, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64) error {
func (s *captureDispatchAppender) AppendAllocatedWithDispatch(ctx context.Context, userID int64, event domain.UpdateEvent, excludeAuthKeyID [8]byte, excludeSessionID int64) (domain.UpdateEvent, error) {
s.dispatched = true
s.userID = userID
s.event = event
s.excludeAuthKeyID = excludeAuthKeyID
s.excludeSessionID = excludeSessionID
return s.UpdateEventStore.Append(ctx, userID, event)
event, err := s.UpdateEventStore.AppendAllocated(ctx, userID, event)
s.event = event
return event, err
}
var errDispatchFailed = errors.New("dispatch failed")
type failingDispatchAppender struct {
*memory.UpdateEventStore
}
func (s *failingDispatchAppender) AppendAllocatedWithDispatch(context.Context, int64, domain.UpdateEvent, [8]byte, int64) (domain.UpdateEvent, error) {
return domain.UpdateEvent{}, errDispatchFailed
}
type captureStateStore struct {
saveCount int
states map[[16]byte]domain.UpdateState
}
func (s *captureStateStore) Get(_ context.Context, authKeyID [8]byte, userID int64) (domain.UpdateState, bool, error) {
if s.states == nil {
return domain.UpdateState{}, false, nil
}
st, ok := s.states[captureStateKey(authKeyID, userID)]
return st, ok, nil
}
func (s *captureStateStore) Save(_ context.Context, authKeyID [8]byte, userID int64, state domain.UpdateState) error {
if s.states == nil {
s.states = make(map[[16]byte]domain.UpdateState)
}
s.saveCount++
s.states[captureStateKey(authKeyID, userID)] = state
return nil
}
func (s *captureStateStore) Delete(_ context.Context, authKeyID [8]byte, userID int64) error {
if s.states != nil {
delete(s.states, captureStateKey(authKeyID, userID))
}
return nil
}
func (s *captureStateStore) DeleteAuthKey(_ context.Context, authKeyID [8]byte) error {
if s.states == nil {
return nil
}
for key := range s.states {
var got [8]byte
copy(got[:], key[:8])
if got == authKeyID {
delete(s.states, key)
}
}
return nil
}
func captureStateKey(authKeyID [8]byte, userID int64) [16]byte {
var key [16]byte
copy(key[:8], authKeyID[:])
for i := 0; i < 8; i++ {
key[8+i] = byte(userID >> (8 * i))
}
return key
}