Monorepo for Tangled tangled.org
1package reporesolver 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "log" 9 "net/http" 10 "net/url" 11 "path" 12 "strings" 13 14 "github.com/bluesky-social/indigo/atproto/identity" 15 securejoin "github.com/cyphar/filepath-securejoin" 16 "github.com/go-chi/chi/v5" 17 "tangled.sh/tangled.sh/core/appview/config" 18 "tangled.sh/tangled.sh/core/appview/db" 19 "tangled.sh/tangled.sh/core/appview/oauth" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 22 "tangled.sh/tangled.sh/core/idresolver" 23 "tangled.sh/tangled.sh/core/knotclient" 24 "tangled.sh/tangled.sh/core/rbac" 25) 26 27type ResolvedRepo struct { 28 db.Repo 29 OwnerId identity.Identity 30 Ref string 31 CurrentDir string 32 33 rr *RepoResolver 34} 35 36type RepoResolver struct { 37 config *config.Config 38 enforcer *rbac.Enforcer 39 idResolver *idresolver.Resolver 40 execer db.Execer 41} 42 43func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Resolver, execer db.Execer) *RepoResolver { 44 return &RepoResolver{config: config, enforcer: enforcer, idResolver: resolver, execer: execer} 45} 46 47func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { 48 repo, ok := r.Context().Value("repo").(*db.Repo) 49 if !ok { 50 log.Println("malformed middleware: `repo` not exist in context") 51 return nil, fmt.Errorf("malformed middleware") 52 } 53 id, ok := r.Context().Value("resolvedId").(identity.Identity) 54 if !ok { 55 log.Println("malformed middleware") 56 return nil, fmt.Errorf("malformed middleware") 57 } 58 59 ref := chi.URLParam(r, "ref") 60 61 if ref == "" { 62 us, err := knotclient.NewUnsignedClient(repo.Knot, rr.config.Core.Dev) 63 if err != nil { 64 return nil, err 65 } 66 67 defaultBranch, err := us.DefaultBranch(id.DID.String(), repo.Name) 68 if err != nil { 69 return nil, err 70 } 71 72 ref = defaultBranch.Branch 73 } 74 75 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 76 77 return &ResolvedRepo{ 78 Repo: *repo, 79 OwnerId: id, 80 Ref: ref, 81 CurrentDir: currentDir, 82 83 rr: rr, 84 }, nil 85} 86 87func (f *ResolvedRepo) OwnerDid() string { 88 return f.OwnerId.DID.String() 89} 90 91func (f *ResolvedRepo) OwnerHandle() string { 92 return f.OwnerId.Handle.String() 93} 94 95func (f *ResolvedRepo) OwnerSlashRepo() string { 96 handle := f.OwnerId.Handle 97 98 var p string 99 if handle != "" && !handle.IsInvalidHandle() { 100 p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) 101 } else { 102 p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) 103 } 104 105 return p 106} 107 108func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) { 109 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 110 if err != nil { 111 return nil, err 112 } 113 114 var collaborators []pages.Collaborator 115 for _, item := range repoCollaborators { 116 // currently only two roles: owner and member 117 var role string 118 switch item[3] { 119 case "repo:owner": 120 role = "owner" 121 case "repo:collaborator": 122 role = "collaborator" 123 default: 124 continue 125 } 126 127 did := item[0] 128 129 c := pages.Collaborator{ 130 Did: did, 131 Handle: "", 132 Role: role, 133 } 134 collaborators = append(collaborators, c) 135 } 136 137 // populate all collborators with handles 138 identsToResolve := make([]string, len(collaborators)) 139 for i, collab := range collaborators { 140 identsToResolve[i] = collab.Did 141 } 142 143 resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve) 144 for i, resolved := range resolvedIdents { 145 if resolved != nil { 146 collaborators[i].Handle = resolved.Handle.String() 147 } 148 } 149 150 return collaborators, nil 151} 152 153// this function is a bit weird since it now returns RepoInfo from an entirely different 154// package. we should refactor this or get rid of RepoInfo entirely. 155func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 156 repoAt := f.RepoAt() 157 isStarred := false 158 if user != nil { 159 isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 160 } 161 162 starCount, err := db.GetStarCount(f.rr.execer, repoAt) 163 if err != nil { 164 log.Println("failed to get star count for ", repoAt) 165 } 166 issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 167 if err != nil { 168 log.Println("failed to get issue count for ", repoAt) 169 } 170 pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 171 if err != nil { 172 log.Println("failed to get issue count for ", repoAt) 173 } 174 source, err := db.GetRepoSource(f.rr.execer, repoAt) 175 if errors.Is(err, sql.ErrNoRows) { 176 source = "" 177 } else if err != nil { 178 log.Println("failed to get repo source for ", repoAt, err) 179 } 180 181 var sourceRepo *db.Repo 182 if source != "" { 183 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source) 184 if err != nil { 185 log.Println("failed to get repo by at uri", err) 186 } 187 } 188 189 var sourceHandle *identity.Identity 190 if sourceRepo != nil { 191 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did) 192 if err != nil { 193 log.Println("failed to resolve source repo", err) 194 } 195 } 196 197 knot := f.Knot 198 var disableFork bool 199 us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev) 200 if err != nil { 201 log.Printf("failed to create unsigned client for %s: %v", knot, err) 202 } else { 203 result, err := us.Branches(f.OwnerDid(), f.Name) 204 if err != nil { 205 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.Name, err) 206 } 207 208 if len(result.Branches) == 0 { 209 disableFork = true 210 } 211 } 212 213 repoInfo := repoinfo.RepoInfo{ 214 OwnerDid: f.OwnerDid(), 215 OwnerHandle: f.OwnerHandle(), 216 Name: f.Name, 217 RepoAt: repoAt, 218 Description: f.Description, 219 Ref: f.Ref, 220 IsStarred: isStarred, 221 Knot: knot, 222 Spindle: f.Spindle, 223 Roles: f.RolesInRepo(user), 224 Stats: db.RepoStats{ 225 StarCount: starCount, 226 IssueCount: issueCount, 227 PullCount: pullCount, 228 }, 229 DisableFork: disableFork, 230 CurrentDir: f.CurrentDir, 231 } 232 233 if sourceRepo != nil { 234 repoInfo.Source = sourceRepo 235 repoInfo.SourceHandle = sourceHandle.Handle.String() 236 } 237 238 return repoInfo 239} 240 241func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo { 242 if u != nil { 243 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 244 return repoinfo.RolesInRepo{r} 245 } else { 246 return repoinfo.RolesInRepo{} 247 } 248} 249 250// extractPathAfterRef gets the actual repository path 251// after the ref. for example: 252// 253// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 254func extractPathAfterRef(fullPath, ref string) string { 255 fullPath = strings.TrimPrefix(fullPath, "/") 256 257 ref = url.PathEscape(ref) 258 259 prefixes := []string{ 260 fmt.Sprintf("blob/%s/", ref), 261 fmt.Sprintf("tree/%s/", ref), 262 fmt.Sprintf("raw/%s/", ref), 263 } 264 265 for _, prefix := range prefixes { 266 idx := strings.Index(fullPath, prefix) 267 if idx != -1 { 268 return fullPath[idx+len(prefix):] 269 } 270 } 271 272 return "" 273}