forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at knot-xrpc 7.7 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/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 "tangled.sh/tangled.sh/core/idresolver" 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 switch item[3] { 153 case "repo:owner": 154 role = "owner" 155 case "repo:collaborator": 156 role = "collaborator" 157 default: 158 continue 159 } 160 161 did := item[0] 162 163 c := pages.Collaborator{ 164 Did: did, 165 Handle: "", 166 Role: role, 167 } 168 collaborators = append(collaborators, c) 169 } 170 171 // populate all collborators with handles 172 identsToResolve := make([]string, len(collaborators)) 173 for i, collab := range collaborators { 174 identsToResolve[i] = collab.Did 175 } 176 177 resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve) 178 for i, resolved := range resolvedIdents { 179 if resolved != nil { 180 collaborators[i].Handle = resolved.Handle.String() 181 } 182 } 183 184 return collaborators, nil 185} 186 187// this function is a bit weird since it now returns RepoInfo from an entirely different 188// package. we should refactor this or get rid of RepoInfo entirely. 189func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 190 isStarred := false 191 if user != nil { 192 isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 193 } 194 195 starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 196 if err != nil { 197 log.Println("failed to get star count for ", f.RepoAt) 198 } 199 issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 200 if err != nil { 201 log.Println("failed to get issue count for ", f.RepoAt) 202 } 203 pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 204 if err != nil { 205 log.Println("failed to get issue count for ", f.RepoAt) 206 } 207 source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 208 if errors.Is(err, sql.ErrNoRows) { 209 source = "" 210 } else if err != nil { 211 log.Println("failed to get repo source for ", f.RepoAt, err) 212 } 213 214 var sourceRepo *db.Repo 215 if source != "" { 216 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source) 217 if err != nil { 218 log.Println("failed to get repo by at uri", err) 219 } 220 } 221 222 var sourceHandle *identity.Identity 223 if sourceRepo != nil { 224 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did) 225 if err != nil { 226 log.Println("failed to resolve source repo", err) 227 } 228 } 229 230 knot := f.Knot 231 var disableFork bool 232 us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev) 233 if err != nil { 234 log.Printf("failed to create unsigned client for %s: %v", knot, err) 235 } else { 236 result, err := us.Branches(f.OwnerDid(), f.RepoName) 237 if err != nil { 238 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 239 } 240 241 if len(result.Branches) == 0 { 242 disableFork = true 243 } 244 } 245 246 repoInfo := repoinfo.RepoInfo{ 247 OwnerDid: f.OwnerDid(), 248 OwnerHandle: f.OwnerHandle(), 249 Name: f.RepoName, 250 RepoAt: f.RepoAt, 251 Description: f.Description, 252 Ref: f.Ref, 253 IsStarred: isStarred, 254 Knot: knot, 255 Spindle: f.Spindle, 256 Roles: f.RolesInRepo(user), 257 Stats: db.RepoStats{ 258 StarCount: starCount, 259 IssueCount: issueCount, 260 PullCount: pullCount, 261 }, 262 DisableFork: disableFork, 263 CurrentDir: f.CurrentDir, 264 } 265 266 if sourceRepo != nil { 267 repoInfo.Source = sourceRepo 268 repoInfo.SourceHandle = sourceHandle.Handle.String() 269 } 270 271 return repoInfo 272} 273 274func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo { 275 if u != nil { 276 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 277 return repoinfo.RolesInRepo{r} 278 } else { 279 return repoinfo.RolesInRepo{} 280 } 281} 282 283// extractPathAfterRef gets the actual repository path 284// after the ref. for example: 285// 286// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 287func extractPathAfterRef(fullPath, ref string) string { 288 fullPath = strings.TrimPrefix(fullPath, "/") 289 290 ref = url.PathEscape(ref) 291 292 prefixes := []string{ 293 fmt.Sprintf("blob/%s/", ref), 294 fmt.Sprintf("tree/%s/", ref), 295 fmt.Sprintf("raw/%s/", ref), 296 } 297 298 for _, prefix := range prefixes { 299 idx := strings.Index(fullPath, prefix) 300 if idx != -1 { 301 return fullPath[idx+len(prefix):] 302 } 303 } 304 305 return "" 306}