179 lines
5.5 KiB
Go
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)
|
|
}
|