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 RepoAt: repoAt,
188 Description: f.Description,
189 IsStarred: isStarred,
190 Knot: knot,
191 Spindle: f.Spindle,
192 Roles: f.RolesInRepo(user),
193 Stats: db.RepoStats{
194 StarCount: starCount,
195 IssueCount: issueCount,
196 PullCount: pullCount,
197 },
198 CurrentDir: f.CurrentDir,
199 Ref: f.Ref,
200 }
201
202 if sourceRepo != nil {
203 repoInfo.Source = sourceRepo
204 repoInfo.SourceHandle = sourceHandle.Handle.String()
205 }
206
207 return repoInfo
208}
209
210func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
211 if u != nil {
212 r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
213 return repoinfo.RolesInRepo{r}
214 } else {
215 return repoinfo.RolesInRepo{}
216 }
217}
218
219// extractPathAfterRef gets the actual repository path
220// after the ref. for example:
221//
222// /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/
223func extractPathAfterRef(fullPath string) string {
224 fullPath = strings.TrimPrefix(fullPath, "/")
225
226 // match blob/, tree/, or raw/ followed by any ref and then a slash
227 //
228 // captures everything after the final slash
229 pattern := `(?:blob|tree|raw)/[^/]+/(.*)$`
230
231 re := regexp.MustCompile(pattern)
232 matches := re.FindStringSubmatch(fullPath)
233
234 if len(matches) > 1 {
235 return matches[1]
236 }
237
238 return ""
239}