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 Handle: "",
117 Role: role,
118 }
119 collaborators = append(collaborators, c)
120 }
121
122 // populate all collborators with handles
123 identsToResolve := make([]string, len(collaborators))
124 for i, collab := range collaborators {
125 identsToResolve[i] = collab.Did
126 }
127
128 resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve)
129 for i, resolved := range resolvedIdents {
130 if resolved != nil {
131 collaborators[i].Handle = resolved.Handle.String()
132 }
133 }
134
135 return collaborators, nil
136}
137
138// this function is a bit weird since it now returns RepoInfo from an entirely different
139// package. we should refactor this or get rid of RepoInfo entirely.
140func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
141 repoAt := f.RepoAt()
142 isStarred := false
143 if user != nil {
144 isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt)
145 }
146
147 starCount, err := db.GetStarCount(f.rr.execer, repoAt)
148 if err != nil {
149 log.Println("failed to get star count for ", repoAt)
150 }
151 issueCount, err := db.GetIssueCount(f.rr.execer, repoAt)
152 if err != nil {
153 log.Println("failed to get issue count for ", repoAt)
154 }
155 pullCount, err := db.GetPullCount(f.rr.execer, repoAt)
156 if err != nil {
157 log.Println("failed to get issue count for ", repoAt)
158 }
159 source, err := db.GetRepoSource(f.rr.execer, repoAt)
160 if errors.Is(err, sql.ErrNoRows) {
161 source = ""
162 } else if err != nil {
163 log.Println("failed to get repo source for ", repoAt, err)
164 }
165
166 var sourceRepo *models.Repo
167 if source != "" {
168 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
169 if err != nil {
170 log.Println("failed to get repo by at uri", err)
171 }
172 }
173
174 var sourceHandle *identity.Identity
175 if sourceRepo != nil {
176 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did)
177 if err != nil {
178 log.Println("failed to resolve source repo", err)
179 }
180 }
181
182 knot := f.Knot
183
184 repoInfo := repoinfo.RepoInfo{
185 OwnerDid: f.OwnerDid(),
186 OwnerHandle: f.OwnerHandle(),
187 Name: f.Name,
188 Rkey: f.Repo.Rkey,
189 RepoAt: repoAt,
190 Description: f.Description,
191 IsStarred: isStarred,
192 Knot: knot,
193 Spindle: f.Spindle,
194 Roles: f.RolesInRepo(user),
195 Stats: models.RepoStats{
196 StarCount: starCount,
197 IssueCount: issueCount,
198 PullCount: pullCount,
199 },
200 CurrentDir: f.CurrentDir,
201 Ref: f.Ref,
202 }
203
204 if sourceRepo != nil {
205 repoInfo.Source = sourceRepo
206 repoInfo.SourceHandle = sourceHandle.Handle.String()
207 }
208
209 return repoInfo
210}
211
212func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
213 if u != nil {
214 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
215 return repoinfo.RolesInRepo{Roles: r}
216 } else {
217 return repoinfo.RolesInRepo{}
218 }
219}
220
221// extractPathAfterRef gets the actual repository path
222// after the ref. for example:
223//
224// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
225func extractPathAfterRef(fullPath string) string {
226 fullPath = strings.TrimPrefix(fullPath, "/")
227
228 // match blob/, tree/, or raw/ followed by any ref and then a slash
229 //
230 // captures everything after the final slash
231 pattern := `(?:blob|tree|raw)/[^/]+/(.*)$`
232
233 re := regexp.MustCompile(pattern)
234 matches := re.FindStringSubmatch(fullPath)
235
236 if len(matches) > 1 {
237 return matches[1]
238 }
239
240 return ""
241}