fix: sync refresh complete self profile on session ready
This commit is contained in:
parent
2512eab51d
commit
f67e87689c
8 changed files with 405 additions and 28 deletions
|
|
@ -1962,7 +1962,7 @@ func (r *Router) pushSelfUserChangedUpdate(ctx context.Context, u domain.User) {
|
||||||
}
|
}
|
||||||
r.pushUserUpdates(ctx, u.ID, &tg.Updates{
|
r.pushUserUpdates(ctx, u.ID, &tg.Updates{
|
||||||
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
|
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
|
||||||
Users: []tg.UserClass{r.tgSelfUser(u)},
|
Users: []tg.UserClass{r.tgSelfUserWithReadModels(ctx, u)},
|
||||||
Date: int(r.clock.Now().Unix()),
|
Date: int(r.clock.Now().Unix()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,7 @@ func (r *Router) authLoginTokenSuccess(ctx context.Context, a domain.Authorizati
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
return &tg.AuthLoginTokenSuccess{
|
return &tg.AuthLoginTokenSuccess{
|
||||||
Authorization: &tg.AuthAuthorization{User: r.tgSelfUser(u)},
|
Authorization: &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,7 +528,7 @@ func (r *Router) finishAuthSignIn(ctx context.Context, u domain.User, needSignUp
|
||||||
}
|
}
|
||||||
r.bindSessionUser(ctx, u.ID)
|
r.bindSessionUser(ctx, u.ID)
|
||||||
r.pushSignInServiceNotificationToOthers(ctx, u)
|
r.pushSignInServiceNotificationToOthers(ctx, u)
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeRequest) (tg.AuthSentCodeClass, error) {
|
func (r *Router) onAuthResendCode(ctx context.Context, req *tg.AuthResendCodeRequest) (tg.AuthSentCodeClass, error) {
|
||||||
|
|
@ -637,7 +637,7 @@ func (r *Router) onAuthCheckPassword(ctx context.Context, password tg.InputCheck
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) onAuthRequestPasswordRecovery(ctx context.Context) (*tg.AuthPasswordRecovery, error) {
|
func (r *Router) onAuthRequestPasswordRecovery(ctx context.Context) (*tg.AuthPasswordRecovery, error) {
|
||||||
|
|
@ -678,7 +678,7 @@ func (r *Router) onAuthRecoverPassword(ctx context.Context, req *tg.AuthRecoverP
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) onAuthCheckRecoveryPassword(ctx context.Context, code string) (bool, error) {
|
func (r *Router) onAuthCheckRecoveryPassword(ctx context.Context, code string) (bool, error) {
|
||||||
|
|
@ -809,7 +809,7 @@ func (r *Router) onAuthFinishPasskeyLogin(ctx context.Context, req *tg.AuthFinis
|
||||||
r.setAuthUserCache(id, u.ID, true)
|
r.setAuthUserCache(id, u.ID, true)
|
||||||
}
|
}
|
||||||
r.bindSessionUser(ctx, u.ID)
|
r.bindSessionUser(ctx, u.ID)
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func emailVerificationCode(v tg.EmailVerificationClass) string {
|
func emailVerificationCode(v tg.EmailVerificationClass) string {
|
||||||
|
|
@ -840,7 +840,7 @@ func (r *Router) onAuthImportBotAuthorization(ctx context.Context, req *tg.AuthI
|
||||||
r.setAuthUserCache(id, u.ID, true)
|
r.setAuthUserCache(id, u.ID, true)
|
||||||
}
|
}
|
||||||
r.bindSessionUser(ctx, u.ID)
|
r.bindSessionUser(ctx, u.ID)
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// onAuthSignUp 处理 auth.signUp:创建用户并绑定授权。
|
// onAuthSignUp 处理 auth.signUp:创建用户并绑定授权。
|
||||||
|
|
@ -854,7 +854,7 @@ func (r *Router) onAuthSignUp(ctx context.Context, req *tg.AuthSignUpRequest) (t
|
||||||
}
|
}
|
||||||
r.bindSessionUser(ctx, u.ID)
|
r.bindSessionUser(ctx, u.ID)
|
||||||
r.enqueueLoginMessageBootstrap(ctx, loginMessage)
|
r.enqueueLoginMessageBootstrap(ctx, loginMessage)
|
||||||
return &tg.AuthAuthorization{User: r.tgSelfUser(u)}, nil
|
return &tg.AuthAuthorization{User: r.tgSelfUserWithReadModels(ctx, u)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// onAuthLogOut 处理 auth.logOut:解绑当前 auth_key 的授权。
|
// onAuthLogOut 处理 auth.logOut:解绑当前 auth_key 的授权。
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/clock"
|
"github.com/iamxvbaba/td/clock"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
"github.com/iamxvbaba/td/tgerr"
|
"github.com/iamxvbaba/td/tgerr"
|
||||||
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
appchannels "telesrv/internal/app/channels"
|
appchannels "telesrv/internal/app/channels"
|
||||||
|
|
@ -288,6 +289,149 @@ func TestUsersGetUsersProjectsCollectibleUsernamesInOneBatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveUsernamePreservesCompleteUsernamesThroughLayer228(t *testing.T) {
|
||||||
|
registry := newFakeUsernameRegistry()
|
||||||
|
f := newUsernameProjectionFixture(t, registry)
|
||||||
|
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
|
||||||
|
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 0},
|
||||||
|
{Username: "owner_collectible_b", Active: true, SortOrder: 1, CollectibleID: 22},
|
||||||
|
{Username: "owner_collectible_a", Active: true, SortOrder: 2, CollectibleID: 21},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := f.router.onContactsResolveUsername(
|
||||||
|
WithUserID(context.Background(), f.friend.ID),
|
||||||
|
&tg.ContactsResolveUsernameRequest{Username: "@owner_slot"},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve username: %v", err)
|
||||||
|
}
|
||||||
|
assertResolvedUsernames := func(stage string, value *tg.ContactsResolvedPeer) {
|
||||||
|
t.Helper()
|
||||||
|
if value == nil || len(value.Users) != 1 {
|
||||||
|
t.Fatalf("%s resolved users = %+v, want one user", stage, value)
|
||||||
|
}
|
||||||
|
user, ok := value.Users[0].(*tg.User)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s resolved user = %T, want *tg.User", stage, value.Users[0])
|
||||||
|
}
|
||||||
|
if scalar, set := user.GetUsername(); !set || scalar != "owner_slot" {
|
||||||
|
t.Fatalf("%s scalar username = %q (set %v), want owner_slot", stage, scalar, set)
|
||||||
|
}
|
||||||
|
vector, set := user.GetUsernames()
|
||||||
|
want := []string{"owner_slot", "owner_collectible_b", "owner_collectible_a"}
|
||||||
|
if !set || !reflect.DeepEqual(usernameStrings(vector), want) {
|
||||||
|
t.Fatalf("%s usernames = %v (set %v), want %v", stage, usernameStrings(vector), set, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertResolvedUsernames("canonical", resolved)
|
||||||
|
|
||||||
|
var wire bin.Buffer
|
||||||
|
if err := tlprofile.EncodeObject(tlprofile.Profile228, resolved, &wire); err != nil {
|
||||||
|
t.Fatalf("encode Layer 228 resolved peer: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := tlprofile.DecodeObject(
|
||||||
|
tlprofile.Profile228,
|
||||||
|
&bin.Buffer{Buf: wire.Copy()},
|
||||||
|
tlprofile.Limits{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode Layer 228 resolved peer: %v", err)
|
||||||
|
}
|
||||||
|
decodedResolved, ok := decoded.(*tg.ContactsResolvedPeer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("decoded Layer 228 object = %T, want *tg.ContactsResolvedPeer", decoded)
|
||||||
|
}
|
||||||
|
assertResolvedUsernames("Layer 228", decodedResolved)
|
||||||
|
|
||||||
|
requestBody := encodeExactLayerRPC(t, tlprofile.Profile228, &tg.ContactsResolveUsernameRequest{
|
||||||
|
Username: "@owner_slot",
|
||||||
|
})
|
||||||
|
admitted, err := f.router.AdmitLayer(tlprofile.Profile228, &requestBody, tlprofile.Limits{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admit Layer 228 contacts.resolveUsername: %v", err)
|
||||||
|
}
|
||||||
|
result, method, err := f.router.DispatchAdmitted(
|
||||||
|
WithUserID(context.Background(), f.friend.ID),
|
||||||
|
[8]byte{1},
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
admitted,
|
||||||
|
)
|
||||||
|
if err != nil || method != "contacts.resolveUsername" {
|
||||||
|
t.Fatalf("dispatch Layer 228 method=%q err=%v", method, err)
|
||||||
|
}
|
||||||
|
var resultWire bin.Buffer
|
||||||
|
if err := result.Encode(&resultWire); err != nil {
|
||||||
|
t.Fatalf("encode Layer 228 method result: %v", err)
|
||||||
|
}
|
||||||
|
decodedResult, err := tlprofile.DecodeObject(
|
||||||
|
tlprofile.Profile228,
|
||||||
|
&bin.Buffer{Buf: resultWire.Copy()},
|
||||||
|
tlprofile.Limits{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode Layer 228 method result: %v", err)
|
||||||
|
}
|
||||||
|
methodResolved, ok := decodedResult.(*tg.ContactsResolvedPeer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("decoded Layer 228 method result = %T, want *tg.ContactsResolvedPeer", decodedResult)
|
||||||
|
}
|
||||||
|
assertResolvedUsernames("Layer 228 method result", methodResolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthLoginTokenSuccessProjectsCompleteSelfUsernames(t *testing.T) {
|
||||||
|
registry := newFakeUsernameRegistry()
|
||||||
|
f := newUsernameProjectionFixture(t, registry)
|
||||||
|
registry.byPeer[domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}] = []domain.Username{
|
||||||
|
{Username: "owner_slot", Editable: true, Active: true, SortOrder: 0},
|
||||||
|
{Username: "owner_collectible_b", Active: true, SortOrder: 1, CollectibleID: 22},
|
||||||
|
{Username: "owner_collectible_a", Active: true, SortOrder: 2, CollectibleID: 21},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := f.router.authLoginTokenSuccess(context.Background(), domain.Authorization{UserID: f.owner.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("auth login token success: %v", err)
|
||||||
|
}
|
||||||
|
success, ok := result.(*tg.AuthLoginTokenSuccess)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("auth login token result = %T, want *tg.AuthLoginTokenSuccess", result)
|
||||||
|
}
|
||||||
|
authorization, ok := success.Authorization.(*tg.AuthAuthorization)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("authorization = %T, want *tg.AuthAuthorization", success.Authorization)
|
||||||
|
}
|
||||||
|
self, ok := authorization.User.(*tg.User)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("authorization user = %T, want *tg.User", authorization.User)
|
||||||
|
}
|
||||||
|
want := []string{"owner_slot", "owner_collectible_b", "owner_collectible_a"}
|
||||||
|
vector, set := self.GetUsernames()
|
||||||
|
if !set || !reflect.DeepEqual(usernameStrings(vector), want) {
|
||||||
|
t.Fatalf("authorization usernames = %v (set %v), want %v", usernameStrings(vector), set, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wire bin.Buffer
|
||||||
|
if err := tlprofile.EncodeObject(tlprofile.Profile228, success, &wire); err != nil {
|
||||||
|
t.Fatalf("encode Layer 228 login token success: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := tlprofile.DecodeObject(tlprofile.Profile228, &bin.Buffer{Buf: wire.Copy()}, tlprofile.Limits{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode Layer 228 login token success: %v", err)
|
||||||
|
}
|
||||||
|
decodedSuccess, ok := decoded.(*tg.AuthLoginTokenSuccess)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("decoded login token success = %T", decoded)
|
||||||
|
}
|
||||||
|
decodedAuthorization := decodedSuccess.Authorization.(*tg.AuthAuthorization)
|
||||||
|
decodedSelf := decodedAuthorization.User.(*tg.User)
|
||||||
|
decodedVector, decodedSet := decodedSelf.GetUsernames()
|
||||||
|
if !decodedSet || !reflect.DeepEqual(usernameStrings(decodedVector), want) {
|
||||||
|
t.Fatalf("decoded authorization usernames = %v (set %v), want %v",
|
||||||
|
usernameStrings(decodedVector), decodedSet, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMessageEchoProjectsCompleteUsernamesInOneBatch(t *testing.T) {
|
func TestMessageEchoProjectsCompleteUsernamesInOneBatch(t *testing.T) {
|
||||||
registry := newFakeUsernameRegistry()
|
registry := newFakeUsernameRegistry()
|
||||||
f := newUsernameProjectionFixture(t, registry)
|
f := newUsernameProjectionFixture(t, registry)
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ func (r *Router) pushPremiumStatusUpdate(ctx context.Context, u domain.User) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
r.pushUserUpdates(pushCtx, u.ID, &tg.Updates{
|
r.pushUserUpdates(pushCtx, u.ID, &tg.Updates{
|
||||||
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
|
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
|
||||||
Users: []tg.UserClass{r.tgSelfUser(u)},
|
Users: []tg.UserClass{r.tgSelfUserWithReadModels(pushCtx, u)},
|
||||||
Date: int(r.clock.Now().Unix()),
|
Date: int(r.clock.Now().Unix()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/iamxvbaba/td/bin"
|
"github.com/iamxvbaba/td/bin"
|
||||||
"github.com/iamxvbaba/td/clock"
|
"github.com/iamxvbaba/td/clock"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
appchannels "telesrv/internal/app/channels"
|
appchannels "telesrv/internal/app/channels"
|
||||||
|
|
@ -64,6 +65,145 @@ func TestDispatchMarksSessionReceivesUpdates(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type updatesStateCaptureSessions struct {
|
||||||
|
*captureSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updatesStateCaptureSessions) ReceivesUpdatesForAuthKey([8]byte, int64) bool {
|
||||||
|
return s.snapshot().receives
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatchPushesCompleteSelfProfileOnceWhenSessionBecomesReady(t *testing.T) {
|
||||||
|
const (
|
||||||
|
userID = int64(1000000311)
|
||||||
|
sessionID = int64(311)
|
||||||
|
)
|
||||||
|
rawAuthKeyID := [8]byte{31}
|
||||||
|
self := domain.User{
|
||||||
|
ID: userID,
|
||||||
|
AccessHash: 3111,
|
||||||
|
FirstName: "Alice",
|
||||||
|
Username: "Alice",
|
||||||
|
}
|
||||||
|
peer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
||||||
|
registry := newFakeUsernameRegistry()
|
||||||
|
registry.byPeer[peer] = []domain.Username{
|
||||||
|
{Username: "Alice", Active: true, Editable: true, SortOrder: 0},
|
||||||
|
{Username: "aliceCollect0728b", Active: true, SortOrder: 1, CollectibleID: 2},
|
||||||
|
{Username: "aliceCollect0728a", Active: true, SortOrder: 2, CollectibleID: 1},
|
||||||
|
}
|
||||||
|
sessions := &updatesStateCaptureSessions{captureSessions: &captureSessions{}}
|
||||||
|
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||||
|
Sessions: sessions,
|
||||||
|
Users: staticUsersService{user: self},
|
||||||
|
Usernames: registry,
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
dispatch := func() context.Context {
|
||||||
|
t.Helper()
|
||||||
|
var in bin.Buffer
|
||||||
|
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
|
||||||
|
t.Fatalf("encode help.getConfig: %v", err)
|
||||||
|
}
|
||||||
|
ctx := postresponse.WithCallbacks(WithUserID(context.Background(), userID))
|
||||||
|
if _, err := r.Dispatch(ctx, rawAuthKeyID, sessionID, &in); err != nil {
|
||||||
|
t.Fatalf("dispatch help.getConfig: %v", err)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := dispatch()
|
||||||
|
if got := sessions.snapshot(); got.receives || got.sessionPushCalls != 0 {
|
||||||
|
t.Fatalf("pre-delivery readiness = receives:%v pushes:%d, want false/0", got.receives, got.sessionPushCalls)
|
||||||
|
}
|
||||||
|
postresponse.Run(ctx)
|
||||||
|
|
||||||
|
got := sessions.snapshot()
|
||||||
|
if !got.receives || got.receivesCalls != 1 || got.sessionPushCalls != 1 {
|
||||||
|
t.Fatalf("post-delivery readiness = receives:%v ready_calls:%d pushes:%d, want true/1/1",
|
||||||
|
got.receives, got.receivesCalls, got.sessionPushCalls)
|
||||||
|
}
|
||||||
|
updates, ok := got.message.(*tg.Updates)
|
||||||
|
if !ok || len(updates.Updates) != 1 || len(updates.Users) != 1 {
|
||||||
|
t.Fatalf("self refresh = %T %+v, want one update and one user", got.message, got.message)
|
||||||
|
}
|
||||||
|
refresh, ok := updates.Updates[0].(*tg.UpdateUser)
|
||||||
|
if !ok || refresh.UserID != userID {
|
||||||
|
t.Fatalf("self refresh update = %T %+v, want updateUser(%d)", updates.Updates[0], updates.Updates[0], userID)
|
||||||
|
}
|
||||||
|
projected, ok := updates.Users[0].(*tg.User)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("self refresh user = %T, want *tg.User", updates.Users[0])
|
||||||
|
}
|
||||||
|
vector, set := projected.GetUsernames()
|
||||||
|
wantUsernames := []string{"Alice", "aliceCollect0728b", "aliceCollect0728a"}
|
||||||
|
if !set || !reflect.DeepEqual(usernameStrings(vector), wantUsernames) {
|
||||||
|
t.Fatalf("self refresh usernames = %v (set %v), want %v", usernameStrings(vector), set, wantUsernames)
|
||||||
|
}
|
||||||
|
if scalar, set := projected.GetUsername(); !set || scalar != "Alice" {
|
||||||
|
t.Fatalf("self refresh scalar username = %q (set %v), want Alice", scalar, set)
|
||||||
|
}
|
||||||
|
if updates.Seq != 0 {
|
||||||
|
t.Fatalf("self refresh seq = %d, want 0", updates.Seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wire bin.Buffer
|
||||||
|
if err := tlprofile.EncodeObject(tlprofile.Profile228, updates, &wire); err != nil {
|
||||||
|
t.Fatalf("encode Layer 228 self refresh: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := tlprofile.DecodeObject(tlprofile.Profile228, &bin.Buffer{Buf: wire.Copy()}, tlprofile.Limits{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode Layer 228 self refresh: %v", err)
|
||||||
|
}
|
||||||
|
decodedUpdates, ok := decoded.(*tg.Updates)
|
||||||
|
if !ok || len(decodedUpdates.Users) != 1 {
|
||||||
|
t.Fatalf("decoded Layer 228 self refresh = %T %+v", decoded, decoded)
|
||||||
|
}
|
||||||
|
decodedUser := decodedUpdates.Users[0].(*tg.User)
|
||||||
|
decodedVector, decodedSet := decodedUser.GetUsernames()
|
||||||
|
if !decodedSet || !reflect.DeepEqual(usernameStrings(decodedVector), wantUsernames) {
|
||||||
|
t.Fatalf("decoded Layer 228 usernames = %v (set %v), want %v",
|
||||||
|
usernameStrings(decodedVector), decodedSet, wantUsernames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A session that is already fully ready must not receive the bootstrap again
|
||||||
|
// on every ordinary RPC.
|
||||||
|
postresponse.Run(dispatch())
|
||||||
|
got = sessions.snapshot()
|
||||||
|
if got.receivesCalls != 1 || got.sessionPushCalls != 1 || registry.peerCalls != 1 {
|
||||||
|
t.Fatalf("repeat dispatch effects = ready_calls:%d pushes:%d registry_reads:%d, want 1/1/1",
|
||||||
|
got.receivesCalls, got.sessionPushCalls, registry.peerCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatchSuppressesSelfProfileWhenUsernameRegistryFails(t *testing.T) {
|
||||||
|
const userID = int64(1000000312)
|
||||||
|
registry := newFakeUsernameRegistry()
|
||||||
|
registry.err = errors.New("registry unavailable")
|
||||||
|
sessions := &updatesStateCaptureSessions{captureSessions: &captureSessions{}}
|
||||||
|
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||||
|
Sessions: sessions,
|
||||||
|
Users: staticUsersService{user: domain.User{
|
||||||
|
ID: userID, FirstName: "Alice", Username: "Alice",
|
||||||
|
}},
|
||||||
|
Usernames: registry,
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
var in bin.Buffer
|
||||||
|
if err := (&tg.HelpGetConfigRequest{}).Encode(&in); err != nil {
|
||||||
|
t.Fatalf("encode help.getConfig: %v", err)
|
||||||
|
}
|
||||||
|
ctx := postresponse.WithCallbacks(WithUserID(context.Background(), userID))
|
||||||
|
if _, err := r.Dispatch(ctx, [8]byte{32}, 312, &in); err != nil {
|
||||||
|
t.Fatalf("dispatch help.getConfig: %v", err)
|
||||||
|
}
|
||||||
|
postresponse.Run(ctx)
|
||||||
|
got := sessions.snapshot()
|
||||||
|
if !got.receives || got.sessionPushCalls != 0 {
|
||||||
|
t.Fatalf("registry failure effects = receives:%v pushes:%d, want true/0", got.receives, got.sessionPushCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates 验证 invokeWithoutUpdates
|
// TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates 验证 invokeWithoutUpdates
|
||||||
// 包装的请求(media/temp 连接)不会把该 session 标记为 updates 接收者。
|
// 包装的请求(media/temp 连接)不会把该 session 标记为 updates 接收者。
|
||||||
func TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates(t *testing.T) {
|
func TestDispatchSkipsReceivesUpdatesForInvokeWithoutUpdates(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type captureSessions struct {
|
||||||
authKeyResolved bool
|
authKeyResolved bool
|
||||||
receives bool
|
receives bool
|
||||||
receivesCalls int
|
receivesCalls int
|
||||||
|
sessionPushCalls int
|
||||||
messageType proto.MessageType
|
messageType proto.MessageType
|
||||||
message bin.Encoder
|
message bin.Encoder
|
||||||
userMessage bin.Encoder // 最近一次 PushToUser* 的消息(与 message 区分:message 也被 PushToSession 覆盖)
|
userMessage bin.Encoder // 最近一次 PushToUser* 的消息(与 message 区分:message 也被 PushToSession 覆盖)
|
||||||
|
|
@ -40,6 +41,7 @@ type captureSessionsSnapshot struct {
|
||||||
authKeyResolved bool
|
authKeyResolved bool
|
||||||
receives bool
|
receives bool
|
||||||
receivesCalls int
|
receivesCalls int
|
||||||
|
sessionPushCalls int
|
||||||
messageType proto.MessageType
|
messageType proto.MessageType
|
||||||
message bin.Encoder
|
message bin.Encoder
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +57,7 @@ func (s *captureSessions) snapshot() captureSessionsSnapshot {
|
||||||
authKeyResolved: s.authKeyResolved,
|
authKeyResolved: s.authKeyResolved,
|
||||||
receives: s.receives,
|
receives: s.receives,
|
||||||
receivesCalls: s.receivesCalls,
|
receivesCalls: s.receivesCalls,
|
||||||
|
sessionPushCalls: s.sessionPushCalls,
|
||||||
messageType: s.messageType,
|
messageType: s.messageType,
|
||||||
message: s.message,
|
message: s.message,
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +159,7 @@ func (s *captureSessions) PushToSessionForAuthKey(_ context.Context, rawAuthKeyI
|
||||||
s.sessionID = sessionID
|
s.sessionID = sessionID
|
||||||
s.messageType = t
|
s.messageType = t
|
||||||
s.message = msg
|
s.message = msg
|
||||||
|
s.sessionPushCalls++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
84
internal/rpc/self_profile_refresh.go
Normal file
84
internal/rpc/self_profile_refresh.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/iamxvbaba/td/tg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tgSelfUserWithReadModels is the single-object response-boundary projection
|
||||||
|
// used by authorization results and self-profile updates. tgSelfUser itself is
|
||||||
|
// intentionally a pure domain -> TL conversion because many list paths call it
|
||||||
|
// in loops; the read-model pass belongs here, where it stays one query per
|
||||||
|
// response.
|
||||||
|
func (r *Router) tgSelfUserWithReadModels(ctx context.Context, u domain.User) *tg.User {
|
||||||
|
self := r.tgSelfUser(u)
|
||||||
|
users := []tg.UserClass{self}
|
||||||
|
r.applyPeerReadModels(ctx, u.ID, users, nil)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushUpdatesReadySelfProfile repairs the current session's cached self user
|
||||||
|
// when it first becomes eligible for proactive updates. Telegram's updateUser
|
||||||
|
// contract requires the complete User to be carried by the outer updates.users
|
||||||
|
// vector; TDLib applies that vector before invalidating userFull for updateUser.
|
||||||
|
//
|
||||||
|
// This is an ephemeral cache-convergence update: it allocates no PTS, writes no
|
||||||
|
// durable update event and targets only the physical session that just became
|
||||||
|
// ready. A username-registry read failure suppresses the refresh instead of
|
||||||
|
// replacing a possibly richer client cache with the legacy scalar-only shape.
|
||||||
|
func (r *Router) pushUpdatesReadySelfProfile(ctx context.Context, userID int64) {
|
||||||
|
updates, err := r.updatesReadySelfProfile(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
r.log.Warn("build updates-ready self profile",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if updates == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.pushCurrentSessionMessage(ctx, "push updates-ready self profile", updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) updatesReadySelfProfile(ctx context.Context, userID int64) (*tg.Updates, error) {
|
||||||
|
if userID == 0 || r.deps.Users == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
u, err := r.deps.Users.Self(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load self user: %w", err)
|
||||||
|
}
|
||||||
|
if u.ID != userID || u.Deleted {
|
||||||
|
return nil, fmt.Errorf("invalid self user: requested %d, got %d deleted=%v", userID, u.ID, u.Deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
self := r.tgSelfUser(u)
|
||||||
|
users := []tg.UserClass{self}
|
||||||
|
if r.deps.Usernames != nil {
|
||||||
|
peer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
||||||
|
list, err := r.deps.Usernames.PeerUsernames(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load self usernames: %w", err)
|
||||||
|
}
|
||||||
|
if len(list) != 0 {
|
||||||
|
applyUsernamesFromRegistry(users, nil, map[domain.Peer][]domain.Username{peer: list})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// These read models are optional projections. Their existing response-boundary
|
||||||
|
// contract degrades independently; the username registry above is handled
|
||||||
|
// strictly because losing that vector is the cache corruption fixed here.
|
||||||
|
r.applyStoryMaxIDsToPeerObjects(ctx, userID, users, nil)
|
||||||
|
r.applyBotVerificationIconsToPeerObjects(ctx, users, nil)
|
||||||
|
|
||||||
|
return &tg.Updates{
|
||||||
|
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: userID}},
|
||||||
|
Users: users,
|
||||||
|
Date: int(r.clock.Now().Unix()),
|
||||||
|
Seq: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -202,7 +202,8 @@ func (r *Router) registerUpdatesDeliveryPlan(ctx context.Context, plan *updatesD
|
||||||
// 1. commit the exact account cursor carried by the delivered result;
|
// 1. commit the exact account cursor carried by the delivered result;
|
||||||
// 2. the just-delivered difference may retire its projected secret-chat events;
|
// 2. the just-delivered difference may retire its projected secret-chat events;
|
||||||
// 3. membership routing is rebuilt before SetReceivesUpdates starts FIFO flush;
|
// 3. membership routing is rebuilt before SetReceivesUpdates starts FIFO flush;
|
||||||
// 4. bootstrap jobs are published last, so they queue behind older pending updates.
|
// 4. the current session receives one complete self profile after becoming ready;
|
||||||
|
// 5. bootstrap jobs are published last, so they queue behind older pending updates.
|
||||||
//
|
//
|
||||||
// Each phase gets an independent timeout so one failed side effect cannot starve
|
// Each phase gets an independent timeout so one failed side effect cannot starve
|
||||||
// the remaining delivery-safe transitions.
|
// the remaining delivery-safe transitions.
|
||||||
|
|
@ -238,6 +239,10 @@ func (r *Router) runUpdatesDeliveryPlan(plan updatesDeliveryPlan) {
|
||||||
ctx, cancel := context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
ctx, cancel := context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
||||||
r.markSessionReceivesUpdatesNow(ctx, plan.readyUserID)
|
r.markSessionReceivesUpdatesNow(ctx, plan.readyUserID)
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(baseCtx, updatesDeliveryPhaseTimeout)
|
||||||
|
r.pushUpdatesReadySelfProfile(ctx, plan.readyUserID)
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
if plan.publishBootstrap {
|
if plan.publishBootstrap {
|
||||||
r.publishBootstrapAfterBaseline(baseCtx, plan.bootstrapUserID)
|
r.publishBootstrapAfterBaseline(baseCtx, plan.bootstrapUserID)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue