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