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