forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}