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