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