owpengram-server/internal/app/userprojection/projection_sparse.go
2026-09-01 12:06:31 +03:00

179 lines
5.5 KiB
Go

package userprojection
import (
"context"
"errors"
"golang.org/x/sync/errgroup"
"telesrv/internal/domain"
"telesrv/internal/store"
)
var (
ErrSparseContactProjectionUnsupported = errors.New("sparse contact projection is not supported")
ErrSparsePrivacyProjectionUnsupported = errors.New("sparse privacy projection is not supported")
)
// SparsePrivacyEvaluator evaluates only the supplied viewer->owner pairs. The
// inverse contact rows are supplied from the projector's shared sparse contact
// read so privacy does not issue another contact query.
type SparsePrivacyEvaluator interface {
CanSeeForViewerUserIDs(
ctx context.Context,
ownerUserIDsByViewer map[int64][]int64,
keys []domain.PrivacyKey,
contactsByOwner map[int64]map[int64]domain.Contact,
) (map[int64]map[int64]map[domain.PrivacyKey]bool, error)
}
// ForViewerUserIDs projects a sparse viewer->owner graph. Viewer-independent
// facts are loaded for the union once; viewer-specific facts are read only for
// graph edges that occur in the request (plus their inverse contact edge needed
// by privacy evaluation).
func (p *Projector) ForViewerUserIDs(ctx context.Context, userIDsByViewer map[int64][]int64, baseUsers []domain.User) (map[int64][]domain.User, error) {
requested := normalizeSparseUserIDs(userIDsByViewer)
out := make(map[int64][]domain.User, len(requested))
if len(requested) == 0 {
return out, nil
}
baseUsers = sanitizeDeletedUsers(baseUsers)
baseByID := make(map[int64]domain.User, len(baseUsers))
for _, user := range baseUsers {
if user.ID != 0 {
baseByID[user.ID] = user
}
}
if p == nil {
for viewerID, ids := range requested {
out[viewerID] = sparseBaseUsers(ids, baseByID)
}
return out, nil
}
unionIDs := make([]int64, 0, len(baseByID))
seenUnion := make(map[int64]struct{}, len(baseByID))
contactPairs := make(map[int64][]int64)
privacyPairs := make(map[int64][]int64)
for viewerID, ids := range requested {
for _, ownerID := range ids {
user, found := baseByID[ownerID]
if !found {
continue
}
if _, ok := seenUnion[ownerID]; !ok && !user.Deleted {
seenUnion[ownerID] = struct{}{}
unionIDs = append(unionIDs, ownerID)
}
if user.Deleted || ownerID == viewerID {
continue
}
// Personal-photo overlay applies independently of contact/privacy
// exemptions, so retain every real viewer->owner edge here.
contactPairs[viewerID] = append(contactPairs[viewerID], ownerID)
if ownerID == domain.OfficialSystemUserID || user.Bot {
continue
}
privacyPairs[viewerID] = append(privacyPairs[viewerID], ownerID)
// Privacy's ViewerIsContact is the inverse owner->viewer row. Merge it
// into the same exact-pair store call.
contactPairs[ownerID] = append(contactPairs[ownerID], viewerID)
}
}
var (
profileRefs map[int64]domain.ProfilePhotoRef
fallbackRefs map[int64]domain.ProfilePhotoRef
contactBatch domain.ContactProjectionBatch
visibility map[int64]map[int64]map[domain.PrivacyKey]bool
freezes map[int64]domain.AccountFreeze
)
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
profileRefs, fallbackRefs, err = p.batchProfileFallbackPhotos(gctx, unionIDs)
return err
})
if p.contacts != nil && len(contactPairs) > 0 {
g.Go(func() error {
loader, ok := p.contacts.(store.SparseContactProjectionStore)
if !ok {
return ErrSparseContactProjectionUnsupported
}
var err error
contactBatch, err = loader.ContactProjectionForViewerUserIDs(gctx, contactPairs)
return err
})
}
if p.freezes != nil && len(unionIDs) > 0 {
g.Go(func() error {
var err error
freezes, err = p.freezes.AccountFreezes(gctx, unionIDs)
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
}
if p.privacy != nil && len(privacyPairs) > 0 {
evaluator, ok := p.privacy.(SparsePrivacyEvaluator)
if !ok {
return nil, ErrSparsePrivacyProjectionUnsupported
}
var err error
visibility, err = evaluator.CanSeeForViewerUserIDs(ctx, privacyPairs, privacyProjectionKeys, contactBatch.Contacts)
if err != nil {
return nil, err
}
}
for viewerID, ids := range requested {
projected := sparseBaseUsers(ids, baseByID)
personalRefs := contactBatch.PersonalPhotos[viewerID]
for i := range projected {
user := projected[i]
if user.Deleted {
projected[i] = user.DeletedTombstone()
continue
}
user = applyBasePhotos(user, profileRefs, fallbackRefs, personalRefs, viewerID)
if viewerID != 0 && user.ID != viewerID && user.ID != domain.OfficialSystemUserID && !user.Bot {
contact, found := contactBatch.Contacts[viewerID][user.ID]
user = applyContactProjection(user, contact, found)
var vis map[domain.PrivacyKey]bool
if visibility != nil {
vis = visibility[user.ID][viewerID]
}
var err error
user, err = applyPrivacy(ctx, p.privacy, viewerID, user, found && contact.Phone != "", vis, profileRefs, fallbackRefs, personalRefs)
if err != nil {
return nil, err
}
}
user = applyAccountFreezeProjection(user, viewerID, freezes[user.ID])
projected[i] = user
}
out[viewerID] = projected
}
return out, nil
}
func normalizeSparseUserIDs(in map[int64][]int64) map[int64][]int64 {
out := make(map[int64][]int64, len(in))
for viewerID, ids := range in {
if viewerID == 0 {
continue
}
out[viewerID] = dedupNonZeroInt64(ids)
}
return out
}
func sparseBaseUsers(ids []int64, baseByID map[int64]domain.User) []domain.User {
users := make([]domain.User, 0, len(ids))
for _, id := range ids {
if user, ok := baseByID[id]; ok {
users = append(users, user)
}
}
return cloneUsers(users)
}