232 lines
6.9 KiB
Go
232 lines
6.9 KiB
Go
package privacy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store"
|
|
)
|
|
|
|
// CanSeeForViewerUserIDs evaluates only the requested viewer->owner pairs.
|
|
// contactsByOwner must contain the inverse owner->viewer contact rows prefetched
|
|
// by the caller; accepting them here lets user projection share one sparse
|
|
// contact read for contact overlays, personal photos, and privacy relations.
|
|
func (s *Service) 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) {
|
|
out := make(map[int64]map[int64]map[domain.PrivacyKey]bool)
|
|
if len(ownerUserIDsByViewer) == 0 || len(keys) == 0 {
|
|
return out, nil
|
|
}
|
|
for _, key := range keys {
|
|
if !ValidKey(key) {
|
|
return nil, domain.ErrPrivacyKeyInvalid
|
|
}
|
|
}
|
|
viewersByOwner := make(map[int64][]int64)
|
|
viewerSet := make(map[int64]struct{})
|
|
for viewerID, ownerIDs := range ownerUserIDsByViewer {
|
|
if viewerID == 0 {
|
|
continue
|
|
}
|
|
seenOwners := make(map[int64]struct{}, len(ownerIDs))
|
|
for _, ownerID := range ownerIDs {
|
|
if ownerID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seenOwners[ownerID]; ok {
|
|
continue
|
|
}
|
|
seenOwners[ownerID] = struct{}{}
|
|
viewersByOwner[ownerID] = append(viewersByOwner[ownerID], viewerID)
|
|
viewerSet[viewerID] = struct{}{}
|
|
}
|
|
}
|
|
if len(viewersByOwner) == 0 {
|
|
return out, nil
|
|
}
|
|
if contactsByOwner == nil && s != nil && s.contacts != nil {
|
|
loader, ok := s.contacts.(store.SparseContactProjectionStore)
|
|
if !ok {
|
|
return nil, fmt.Errorf("privacy contact store does not support sparse projection")
|
|
}
|
|
batch, err := loader.ContactProjectionForViewerUserIDs(ctx, viewersByOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contactsByOwner = batch.Contacts
|
|
}
|
|
owners := make([]int64, 0, len(viewersByOwner))
|
|
for ownerID := range viewersByOwner {
|
|
owners = append(owners, ownerID)
|
|
}
|
|
rulesByOwner := make(map[int64]map[domain.PrivacyKey]domain.PrivacyRules, len(owners))
|
|
if s != nil && s.rules != nil {
|
|
list, err := s.rules.ListPrivacyRules(ctx, owners, keys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, rules := range list {
|
|
if !ValidKey(rules.Key) {
|
|
continue
|
|
}
|
|
if len(rules.Rules) == 0 {
|
|
rules.Rules = domain.DefaultPrivacyRules(rules.Key)
|
|
}
|
|
if rulesByOwner[rules.OwnerUserID] == nil {
|
|
rulesByOwner[rules.OwnerUserID] = make(map[domain.PrivacyKey]domain.PrivacyRules, len(keys))
|
|
}
|
|
rulesByOwner[rules.OwnerUserID][rules.Key] = cloneRules(rules)
|
|
}
|
|
}
|
|
|
|
needsByOwner := make(map[int64]evaluationNeeds, len(owners))
|
|
needsViewerFacts := false
|
|
membershipPairCount := 0
|
|
for _, ownerID := range owners {
|
|
var needs evaluationNeeds
|
|
for _, key := range keys {
|
|
rules, ok := rulesByOwner[ownerID][key]
|
|
if !ok {
|
|
rules = defaultRules(ownerID, key)
|
|
}
|
|
mergeNeeds(&needs, needsForRules(rules))
|
|
}
|
|
needsByOwner[ownerID] = needs
|
|
needsViewerFacts = needsViewerFacts || needs.viewerBase
|
|
viewerCount := len(viewersByOwner[ownerID])
|
|
if !activeChannelMembershipPairsAllowed(membershipPairCount, len(needs.chatIDs), viewerCount) {
|
|
return nil, activeChannelMembershipPairLimitError()
|
|
}
|
|
membershipPairCount += len(needs.chatIDs) * viewerCount
|
|
}
|
|
membershipKeys := make([]membershipKey, 0, membershipPairCount)
|
|
for _, ownerID := range owners {
|
|
needs := needsByOwner[ownerID]
|
|
for _, chatID := range needs.chatIDs {
|
|
for _, viewerID := range viewersByOwner[ownerID] {
|
|
membershipKeys = append(membershipKeys, membershipKey{ChatID: chatID, UserID: viewerID})
|
|
}
|
|
}
|
|
}
|
|
viewers := make([]int64, 0, len(viewerSet))
|
|
for viewerID := range viewerSet {
|
|
viewers = append(viewers, viewerID)
|
|
}
|
|
var baseFacts map[int64]viewerFacts
|
|
if needsViewerFacts {
|
|
var err error
|
|
baseFacts, err = s.loadViewerFacts(ctx, viewers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
membershipFacts, err := s.loadMembershipFactsForKeys(ctx, membershipKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := s.now().Unix()
|
|
for _, ownerID := range owners {
|
|
perViewer := make(map[int64]map[domain.PrivacyKey]bool, len(viewersByOwner[ownerID]))
|
|
for _, viewerID := range viewersByOwner[ownerID] {
|
|
visibility := make(map[domain.PrivacyKey]bool, len(keys))
|
|
if ownerID == viewerID {
|
|
for _, key := range keys {
|
|
visibility[key] = true
|
|
}
|
|
perViewer[viewerID] = visibility
|
|
continue
|
|
}
|
|
contact, isContact := contactsByOwner[ownerID][viewerID]
|
|
for _, key := range keys {
|
|
rules, ok := rulesByOwner[ownerID][key]
|
|
if !ok {
|
|
rules = defaultRules(ownerID, key)
|
|
}
|
|
evalCtx := domain.PrivacyContext{
|
|
OwnerUserID: ownerID, ViewerUserID: viewerID,
|
|
ViewerIsContact: isContact, ViewerCloseFriend: isContact && contact.CloseFriend,
|
|
}
|
|
applyViewerFacts(&evalCtx, baseFacts[viewerID], now)
|
|
applyMembershipFacts(&evalCtx, needsByOwner[ownerID].chatIDs, membershipFacts)
|
|
visibility[key] = Evaluate(rules, evalCtx)
|
|
}
|
|
perViewer[viewerID] = visibility
|
|
}
|
|
out[ownerID] = perViewer
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) loadMembershipFactsForKeys(ctx context.Context, input []membershipKey) (map[membershipKey]bool, error) {
|
|
capacity := len(input)
|
|
if capacity > store.MaxActiveChannelMemberPairs {
|
|
capacity = store.MaxActiveChannelMemberPairs
|
|
}
|
|
seen := make(map[membershipKey]struct{}, capacity)
|
|
keys := make([]membershipKey, 0, capacity)
|
|
for _, key := range input {
|
|
if key.ChatID == 0 || key.UserID == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
if len(keys) >= store.MaxActiveChannelMemberPairs {
|
|
return nil, activeChannelMembershipPairLimitError()
|
|
}
|
|
seen[key] = struct{}{}
|
|
keys = append(keys, key)
|
|
}
|
|
if len(keys) == 0 {
|
|
return map[membershipKey]bool{}, nil
|
|
}
|
|
loadMissing := func(ctx context.Context, missing []membershipKey) (map[membershipKey]bool, error) {
|
|
out := make(map[membershipKey]bool, len(missing))
|
|
byChat := make(map[int64][]int64)
|
|
for _, key := range missing {
|
|
out[key] = false
|
|
byChat[key.ChatID] = append(byChat[key.ChatID], key.UserID)
|
|
}
|
|
if s == nil || s.memberships == nil {
|
|
return out, nil
|
|
}
|
|
activeByChat, err := s.memberships.FilterActiveChannelMemberPairs(ctx, byChat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for chatID, userIDs := range activeByChat {
|
|
for _, userID := range userIDs {
|
|
key := membershipKey{ChatID: chatID, UserID: userID}
|
|
if _, requested := out[key]; requested {
|
|
out[key] = true
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
if s == nil || s.membershipFacts == nil {
|
|
return loadMissing(ctx, keys)
|
|
}
|
|
if len(keys) > privacyMembershipBatchAdmissionMaxPairs {
|
|
for {
|
|
loadEpoch := s.membershipFacts.LoadEpoch()
|
|
loaded, err := loadMissing(ctx, keys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.membershipFacts.LoadEpoch() == loadEpoch {
|
|
return loaded, nil
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return s.membershipFacts.GetOrLoadBatch(ctx, keys,
|
|
func(membershipKey) (int64, bool) { return 0, true }, loadMissing)
|
|
}
|