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