forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
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 CreatedAt string
35 Ref string
36 CurrentDir string
37
38 rr *RepoResolver
39}
40
41type RepoResolver struct {
42 config *config.Config
43 enforcer *rbac.Enforcer
44 idResolver *idresolver.Resolver
45 execer db.Execer
46}
47
48func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Resolver, execer db.Execer) *RepoResolver {
49 return &RepoResolver{config: config, enforcer: enforcer, idResolver: resolver, execer: execer}
50}
51
52func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
53 repoName := chi.URLParam(r, "repo")
54 knot, ok := r.Context().Value("knot").(string)
55 if !ok {
56 log.Println("malformed middleware")
57 return nil, fmt.Errorf("malformed middleware")
58 }
59 id, ok := r.Context().Value("resolvedId").(identity.Identity)
60 if !ok {
61 log.Println("malformed middleware")
62 return nil, fmt.Errorf("malformed middleware")
63 }
64
65 repoAt, ok := r.Context().Value("repoAt").(string)
66 if !ok {
67 log.Println("malformed middleware")
68 return nil, fmt.Errorf("malformed middleware")
69 }
70
71 parsedRepoAt, err := syntax.ParseATURI(repoAt)
72 if err != nil {
73 log.Println("malformed repo at-uri")
74 return nil, fmt.Errorf("malformed middleware")
75 }
76
77 ref := chi.URLParam(r, "ref")
78
79 if ref == "" {
80 us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev)
81 if err != nil {
82 return nil, err
83 }
84
85 defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName)
86 if err != nil {
87 return nil, err
88 }
89
90 ref = defaultBranch.Branch
91 }
92
93 currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref))
94
95 // pass through values from the middleware
96 description, ok := r.Context().Value("repoDescription").(string)
97 addedAt, ok := r.Context().Value("repoAddedAt").(string)
98
99 return &ResolvedRepo{
100 Knot: knot,
101 OwnerId: id,
102 RepoName: repoName,
103 RepoAt: parsedRepoAt,
104 Description: description,
105 CreatedAt: addedAt,
106 Ref: ref,
107 CurrentDir: currentDir,
108
109 rr: rr,
110 }, nil
111}
112
113func (f *ResolvedRepo) OwnerDid() string {
114 return f.OwnerId.DID.String()
115}
116
117func (f *ResolvedRepo) OwnerHandle() string {
118 return f.OwnerId.Handle.String()
119}
120
121func (f *ResolvedRepo) OwnerSlashRepo() string {
122 handle := f.OwnerId.Handle
123
124 var p string
125 if handle != "" && !handle.IsInvalidHandle() {
126 p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName)
127 } else {
128 p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
129 }
130
131 return p
132}
133
134func (f *ResolvedRepo) DidSlashRepo() string {
135 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
136 return p
137}
138
139func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) {
140 repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
141 if err != nil {
142 return nil, err
143 }
144
145 var collaborators []pages.Collaborator
146 for _, item := range repoCollaborators {
147 // currently only two roles: owner and member
148 var role string
149 if item[3] == "repo:owner" {
150 role = "owner"
151 } else if item[3] == "repo:collaborator" {
152 role = "collaborator"
153 } else {
154 continue
155 }
156
157 did := item[0]
158
159 c := pages.Collaborator{
160 Did: did,
161 Handle: "",
162 Role: role,
163 }
164 collaborators = append(collaborators, c)
165 }
166
167 // populate all collborators with handles
168 identsToResolve := make([]string, len(collaborators))
169 for i, collab := range collaborators {
170 identsToResolve[i] = collab.Did
171 }
172
173 resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve)
174 for i, resolved := range resolvedIdents {
175 if resolved != nil {
176 collaborators[i].Handle = resolved.Handle.String()
177 }
178 }
179
180 return collaborators, nil
181}
182
183// this function is a bit weird since it now returns RepoInfo from an entirely different
184// package. we should refactor this or get rid of RepoInfo entirely.
185func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
186 isStarred := false
187 if user != nil {
188 isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt))
189 }
190
191 starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt)
192 if err != nil {
193 log.Println("failed to get star count for ", f.RepoAt)
194 }
195 issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt)
196 if err != nil {
197 log.Println("failed to get issue count for ", f.RepoAt)
198 }
199 pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt)
200 if err != nil {
201 log.Println("failed to get issue count for ", f.RepoAt)
202 }
203 source, err := db.GetRepoSource(f.rr.execer, f.RepoAt)
204 if errors.Is(err, sql.ErrNoRows) {
205 source = ""
206 } else if err != nil {
207 log.Println("failed to get repo source for ", f.RepoAt, err)
208 }
209
210 var sourceRepo *db.Repo
211 if source != "" {
212 sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
213 if err != nil {
214 log.Println("failed to get repo by at uri", err)
215 }
216 }
217
218 var sourceHandle *identity.Identity
219 if sourceRepo != nil {
220 sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did)
221 if err != nil {
222 log.Println("failed to resolve source repo", err)
223 }
224 }
225
226 knot := f.Knot
227 var disableFork bool
228 us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev)
229 if err != nil {
230 log.Printf("failed to create unsigned client for %s: %v", knot, err)
231 } else {
232 result, err := us.Branches(f.OwnerDid(), f.RepoName)
233 if err != nil {
234 log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
235 }
236
237 if len(result.Branches) == 0 {
238 disableFork = true
239 }
240 }
241
242 repoInfo := repoinfo.RepoInfo{
243 OwnerDid: f.OwnerDid(),
244 OwnerHandle: f.OwnerHandle(),
245 Name: f.RepoName,
246 RepoAt: f.RepoAt,
247 Description: f.Description,
248 Ref: f.Ref,
249 IsStarred: isStarred,
250 Knot: knot,
251 Roles: f.RolesInRepo(user),
252 Stats: db.RepoStats{
253 StarCount: starCount,
254 IssueCount: issueCount,
255 PullCount: pullCount,
256 },
257 DisableFork: disableFork,
258 CurrentDir: f.CurrentDir,
259 }
260
261 if sourceRepo != nil {
262 repoInfo.Source = sourceRepo
263 repoInfo.SourceHandle = sourceHandle.Handle.String()
264 }
265
266 return repoInfo
267}
268
269func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
270 if u != nil {
271 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
272 return repoinfo.RolesInRepo{r}
273 } else {
274 return repoinfo.RolesInRepo{}
275 }
276}
277
278// extractPathAfterRef gets the actual repository path
279// after the ref. for example:
280//
281// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
282func extractPathAfterRef(fullPath, ref string) string {
283 fullPath = strings.TrimPrefix(fullPath, "/")
284
285 ref = url.PathEscape(ref)
286
287 prefixes := []string{
288 fmt.Sprintf("blob/%s/", ref),
289 fmt.Sprintf("tree/%s/", ref),
290 fmt.Sprintf("raw/%s/", ref),
291 }
292
293 for _, prefix := range prefixes {
294 idx := strings.Index(fullPath, prefix)
295 if idx != -1 {
296 return fullPath[idx+len(prefix):]
297 }
298 }
299
300 return ""
301}