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