owpengram-server/internal/rpc/self_profile_refresh.go

100 lines
3.5 KiB
Go

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)
usernames := tgUsernames(u.Username)
if vector, ok := self.GetUsernames(); ok && len(vector) != 0 {
usernames = vector
}
return &tg.Updates{
Updates: []tg.UpdateClass{
// updateUserName is the authoritative basic-name/username cache write.
// TDLib handles it by calling on_update_user_usernames directly.
&tg.UpdateUserName{
UserID: userID,
FirstName: u.FirstName,
LastName: u.LastName,
Usernames: usernames,
},
// updateUser additionally invalidates userFull while the complete basic
// User remains bundled in the outer users vector.
&tg.UpdateUser{UserID: userID},
},
Users: users,
Date: int(r.clock.Now().Unix()),
Seq: 0,
}, nil
}