Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

at icy/qmlqxq 5.3 kB View raw
1package knotacl 2 3import ( 4 "context" 5 "errors" 6 "log/slog" 7 "slices" 8 "sync" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "golang.org/x/sync/errgroup" 13 14 "tangled.org/core/appview/db" 15 "tangled.org/core/appview/knotcompat" 16 "tangled.org/core/appview/models" 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/pages/repoinfo" 19 "tangled.org/core/consts" 20 "tangled.org/core/rbac" 21) 22 23var ErrKnotUnreachable = errors.New("knot unreachable") 24 25const ( 26 pickerFanoutBudget = 3 * time.Second 27 pickerFanoutConcurrency = 16 28) 29 30type Service struct { 31 dev bool 32 log *slog.Logger 33 leg *legacyReader 34 nat *nativeReader 35} 36 37func NewService(enforcer *rbac.Enforcer, store *db.DB, dev bool, logger *slog.Logger) *Service { 38 return &Service{ 39 dev: dev, 40 log: logger, 41 leg: &legacyReader{enforcer: enforcer}, 42 nat: &nativeReader{client: newRoster(store, NewClient(dev, logger), reconcileTTL, nil, logger), execer: store}, 43 } 44} 45 46func (s *Service) reader(ctx context.Context, host string) reader { 47 if knotcompat.KnotHasCapability(ctx, host, s.dev, consts.CapKnotACL) { 48 return s.nat 49 } 50 return s.leg 51} 52 53func (s *Service) RolesInRepo(ctx context.Context, repo *models.Repo, userDid string) repoinfo.RolesInRepo { 54 return repoinfo.RolesInRepo{Roles: s.repoPerms(ctx, repo, userDid)} 55} 56 57func (s *Service) HasRepoPermission(ctx context.Context, repo *models.Repo, userDid, perm string) bool { 58 return slices.Contains(s.repoPerms(ctx, repo, userDid), perm) 59} 60 61func (s *Service) HasRepoPermissionErr(ctx context.Context, repo *models.Repo, userDid, perm string) (bool, error) { 62 perms, err := s.repoPermsErr(ctx, repo, userDid) 63 if err != nil { 64 return false, err 65 } 66 return slices.Contains(perms, perm), nil 67} 68 69func (s *Service) IsRepoCreateAllowed(ctx context.Context, host, userDid string) bool { 70 return s.reader(ctx, host).isRepoCreateAllowed(ctx, host, userDid) 71} 72 73func (s *Service) KnotMembers(ctx context.Context, host string) []string { 74 return s.reader(ctx, host).knotMembers(ctx, host) 75} 76 77func (s *Service) Collaborators(ctx context.Context, repo *models.Repo) []pages.Collaborator { 78 return s.reader(ctx, repo.Knot).collaborators(ctx, repo) 79} 80 81func (s *Service) IsKnotMember(ctx context.Context, host, userDid string) bool { 82 return s.reader(ctx, host).isKnotMember(ctx, host, userDid) 83} 84 85func (s *Service) InvalidateMembers(host string) { 86 s.nat.client.InvalidateMembers(host) 87} 88 89func (s *Service) InvalidateCollaborators(host, repoDid string) { 90 s.nat.client.InvalidateCollaborators(host, repoDid) 91} 92 93func (s *Service) AddKnotMember(host string, subject syntax.DID, cursor Cursor) error { 94 return s.nat.client.AddKnotMember(host, subject, cursor) 95} 96 97func (s *Service) RemoveKnotMember(host string, subject syntax.DID, cursor Cursor) error { 98 return s.nat.client.RemoveKnotMember(host, subject, cursor) 99} 100 101func (s *Service) AddCollaborator(repoDid, subject syntax.DID, cursor Cursor) error { 102 return s.nat.client.AddCollaborator(repoDid, subject, cursor) 103} 104 105func (s *Service) RemoveCollaborator(repoDid, subject syntax.DID, cursor Cursor) error { 106 return s.nat.client.RemoveCollaborator(repoDid, subject, cursor) 107} 108 109func (s *Service) KnotsForUser(ctx context.Context, userDid string) []string { 110 legacyKnots, err := s.leg.enforcer.GetKnotsForUser(userDid) 111 if err != nil { 112 s.log.Error("knotsForUser: enforcer lookup failed, returning a partial list", "did", userDid, "err", err) 113 } 114 115 regs, err := db.GetRegistrations(s.nat.execer) 116 if err != nil { 117 s.log.Error("knotsForUser: registrations lookup failed, skipping native knots", "did", userDid, "err", err) 118 } 119 domains := dedup(filterMap(regs, func(r models.Registration) (string, bool) { 120 return r.Domain, r.Registered != nil 121 })) 122 owned := filterMap(regs, func(r models.Registration) (string, bool) { 123 return r.Domain, r.Registered != nil && r.ByDid == userDid 124 }) 125 nativeMember := s.nativeMemberships(ctx, domains, userDid) 126 127 all := make([]string, 0, len(legacyKnots)+len(owned)+len(nativeMember)) 128 all = append(all, legacyKnots...) 129 all = append(all, owned...) 130 all = append(all, nativeMember...) 131 return dedup(all) 132} 133 134func (s *Service) nativeMemberships(ctx context.Context, domains []string, userDid string) []string { 135 ctx, cancel := context.WithTimeout(ctx, pickerFanoutBudget) 136 defer cancel() 137 138 g, gctx := errgroup.WithContext(ctx) 139 g.SetLimit(pickerFanoutConcurrency) 140 141 var mu sync.Mutex 142 var hits []string 143 144 for _, host := range domains { 145 g.Go(func() error { 146 if !knotcompat.KnotHasCapability(gctx, host, s.dev, consts.CapKnotACL) { 147 return nil 148 } 149 members, err := s.nat.client.GetKnotMembers(gctx, host) 150 if err != nil || !slices.Contains(members, userDid) { 151 return nil 152 } 153 mu.Lock() 154 hits = append(hits, host) 155 mu.Unlock() 156 return nil 157 }) 158 } 159 _ = g.Wait() 160 return hits 161} 162 163func (s *Service) repoPerms(ctx context.Context, repo *models.Repo, userDid string) []string { 164 perms, _ := s.repoPermsErr(ctx, repo, userDid) 165 return perms 166} 167 168func (s *Service) repoPermsErr(ctx context.Context, repo *models.Repo, userDid string) ([]string, error) { 169 return s.reader(ctx, repo.Knot).repoPerms(ctx, repo, userDid) 170} 171 172func filterMap[T, U any](items []T, f func(T) (U, bool)) []U { 173 var out []U 174 for _, it := range items { 175 if u, ok := f(it); ok { 176 out = append(out, u) 177 } 178 } 179 return out 180}