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