Monorepo for Tangled tangled.org
2

Configure Feed

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

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