this repo has no description
1package reporesolver 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "log" 9 "net/http" 10 "path" 11 "regexp" 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.org/core/appview/config" 18 "tangled.org/core/appview/db" 19 "tangled.org/core/appview/models" 20 "tangled.org/core/appview/oauth" 21 "tangled.org/core/appview/pages" 22 "tangled.org/core/appview/pages/repoinfo" 23 "tangled.org/core/idresolver" 24 "tangled.org/core/rbac" 25) 26 27type ResolvedRepo struct { 28 models.Repo 29 OwnerId identity.Identity 30 CurrentDir string 31 Ref 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").(*models.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 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 60 ref := chi.URLParam(r, "ref") 61 62 return &ResolvedRepo{ 63 Repo: *repo, 64 OwnerId: id, 65 CurrentDir: currentDir, 66 Ref: ref, 67 68 rr: rr, 69 }, nil 70} 71 72func (f *ResolvedRepo) OwnerDid() string { 73 return f.OwnerId.DID.String() 74} 75 76func (f *ResolvedRepo) OwnerHandle() string { 77 return f.OwnerId.Handle.String() 78} 79 80func (f *ResolvedRepo) OwnerSlashRepo() string { 81 handle := f.OwnerId.Handle 82 83 var p string 84 if handle != "" && !handle.IsInvalidHandle() { 85 p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) 86 } else { 87 p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) 88 } 89 90 return p 91} 92 93func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) { 94 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 95 if err != nil { 96 return nil, err 97 } 98 99 var collaborators []pages.Collaborator 100 for _, item := range repoCollaborators { 101 // currently only two roles: owner and member 102 var role string 103 switch item[3] { 104 case "repo:owner": 105 role = "owner" 106 case "repo:collaborator": 107 role = "collaborator" 108 default: 109 continue 110 } 111 112 did := item[0] 113 114 c := pages.Collaborator{ 115 Did: did, 116 Role: role, 117 } 118 collaborators = append(collaborators, c) 119 } 120 121 return collaborators, nil 122} 123 124// this function is a bit weird since it now returns RepoInfo from an entirely different 125// package. we should refactor this or get rid of RepoInfo entirely. 126func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 127 repoAt := f.RepoAt() 128 isStarred := false 129 if user != nil { 130 isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 131 } 132 133 starCount, err := db.GetStarCount(f.rr.execer, repoAt) 134 if err != nil { 135 log.Println("failed to get star count for ", repoAt) 136 } 137 issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 138 if err != nil { 139 log.Println("failed to get issue count for ", repoAt) 140 } 141 pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 142 if err != nil { 143 log.Println("failed to get issue count for ", repoAt) 144 } 145 source, err := db.GetRepoSource(f.rr.execer, repoAt) 146 if errors.Is(err, sql.ErrNoRows) { 147 source = "" 148 } else if err != nil { 149 log.Println("failed to get repo source for ", repoAt, err) 150 } 151 152 var sourceRepo *models.Repo 153 if source != "" { 154 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source) 155 if err != nil { 156 log.Println("failed to get repo by at uri", err) 157 } 158 } 159 160 var sourceHandle *identity.Identity 161 if sourceRepo != nil { 162 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did) 163 if err != nil { 164 log.Println("failed to resolve source repo", err) 165 } 166 } 167 168 knot := f.Knot 169 170 repoInfo := repoinfo.RepoInfo{ 171 OwnerDid: f.OwnerDid(), 172 OwnerHandle: f.OwnerHandle(), 173 Name: f.Name, 174 Rkey: f.Repo.Rkey, 175 RepoAt: repoAt, 176 Description: f.Description, 177 Website: f.Website, 178 Topics: f.Topics, 179 IsStarred: isStarred, 180 Knot: knot, 181 Spindle: f.Spindle, 182 Roles: f.RolesInRepo(user), 183 Stats: models.RepoStats{ 184 StarCount: starCount, 185 IssueCount: issueCount, 186 PullCount: pullCount, 187 }, 188 CurrentDir: f.CurrentDir, 189 Ref: f.Ref, 190 } 191 192 if sourceRepo != nil { 193 repoInfo.Source = sourceRepo 194 repoInfo.SourceHandle = sourceHandle.Handle.String() 195 } 196 197 return repoInfo 198} 199 200func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo { 201 if u != nil { 202 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 203 return repoinfo.RolesInRepo{Roles: r} 204 } else { 205 return repoinfo.RolesInRepo{} 206 } 207} 208 209// extractPathAfterRef gets the actual repository path 210// after the ref. for example: 211// 212// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 213func extractPathAfterRef(fullPath string) string { 214 fullPath = strings.TrimPrefix(fullPath, "/") 215 216 // match blob/, tree/, or raw/ followed by any ref and then a slash 217 // 218 // captures everything after the final slash 219 pattern := `(?:blob|tree|raw)/[^/]+/(.*)$` 220 221 re := regexp.MustCompile(pattern) 222 matches := re.FindStringSubmatch(fullPath) 223 224 if len(matches) > 1 { 225 return matches[1] 226 } 227 228 return "" 229}