this repo has no description
1package state
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log"
9 "math/rand/v2"
10 "net/http"
11 "path"
12 "slices"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/bluesky-social/indigo/atproto/identity"
18 "github.com/bluesky-social/indigo/atproto/syntax"
19 securejoin "github.com/cyphar/filepath-securejoin"
20 "github.com/go-chi/chi/v5"
21 "github.com/sotangled/tangled/api/tangled"
22 "github.com/sotangled/tangled/appview/auth"
23 "github.com/sotangled/tangled/appview/db"
24 "github.com/sotangled/tangled/appview/pages"
25 "github.com/sotangled/tangled/types"
26
27 comatproto "github.com/bluesky-social/indigo/api/atproto"
28 lexutil "github.com/bluesky-social/indigo/lex/util"
29)
30
31func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
32 ref := chi.URLParam(r, "ref")
33 f, err := fullyResolvedRepo(r)
34 if err != nil {
35 log.Println("failed to fully resolve repo", err)
36 return
37 }
38 var reqUrl string
39 if ref != "" {
40 reqUrl = fmt.Sprintf("http://%s/%s/%s/tree/%s", f.Knot, f.OwnerDid(), f.RepoName, ref)
41 } else {
42 reqUrl = fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName)
43 }
44
45 resp, err := http.Get(reqUrl)
46 if err != nil {
47 s.pages.Error503(w)
48 log.Println("failed to reach knotserver", err)
49 return
50 }
51 defer resp.Body.Close()
52
53 body, err := io.ReadAll(resp.Body)
54 if err != nil {
55 log.Printf("Error reading response body: %v", err)
56 return
57 }
58
59 var result types.RepoIndexResponse
60 err = json.Unmarshal(body, &result)
61 if err != nil {
62 log.Printf("Error unmarshalling response body: %v", err)
63 return
64 }
65
66 tagMap := make(map[string][]string)
67 for _, tag := range result.Tags {
68 hash := tag.Hash
69 tagMap[hash] = append(tagMap[hash], tag.Name)
70 }
71
72 for _, branch := range result.Branches {
73 hash := branch.Hash
74 tagMap[hash] = append(tagMap[hash], branch.Name)
75 }
76
77 user := s.auth.GetUser(r)
78
79 s.pages.RepoIndexPage(w, pages.RepoIndexParams{
80 LoggedInUser: user,
81 RepoInfo: f.RepoInfo(s, user),
82 TagMap: tagMap,
83 RepoIndexResponse: result,
84 })
85
86 return
87}
88
89func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
90 f, err := fullyResolvedRepo(r)
91 if err != nil {
92 log.Println("failed to fully resolve repo", err)
93 return
94 }
95
96 page := 1
97 if r.URL.Query().Get("page") != "" {
98 page, err = strconv.Atoi(r.URL.Query().Get("page"))
99 if err != nil {
100 page = 1
101 }
102 }
103
104 ref := chi.URLParam(r, "ref")
105 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s?page=%d&per_page=30", f.Knot, f.OwnerDid(), f.RepoName, ref, page))
106 if err != nil {
107 log.Println("failed to reach knotserver", err)
108 return
109 }
110
111 body, err := io.ReadAll(resp.Body)
112 if err != nil {
113 log.Printf("error reading response body: %v", err)
114 return
115 }
116
117 var repolog types.RepoLogResponse
118 err = json.Unmarshal(body, &repolog)
119 if err != nil {
120 log.Println("failed to parse json response", err)
121 return
122 }
123
124 user := s.auth.GetUser(r)
125 s.pages.RepoLog(w, pages.RepoLogParams{
126 LoggedInUser: user,
127 RepoInfo: f.RepoInfo(s, user),
128 RepoLogResponse: repolog,
129 })
130 return
131}
132
133func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
134 f, err := fullyResolvedRepo(r)
135 if err != nil {
136 log.Println("failed to fully resolve repo", err)
137 return
138 }
139
140 ref := chi.URLParam(r, "ref")
141 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
142 if err != nil {
143 log.Println("failed to reach knotserver", err)
144 return
145 }
146
147 body, err := io.ReadAll(resp.Body)
148 if err != nil {
149 log.Printf("Error reading response body: %v", err)
150 return
151 }
152
153 var result types.RepoCommitResponse
154 err = json.Unmarshal(body, &result)
155 if err != nil {
156 log.Println("failed to parse response:", err)
157 return
158 }
159
160 user := s.auth.GetUser(r)
161 s.pages.RepoCommit(w, pages.RepoCommitParams{
162 LoggedInUser: user,
163 RepoInfo: f.RepoInfo(s, user),
164 RepoCommitResponse: result,
165 })
166 return
167}
168
169func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
170 f, err := fullyResolvedRepo(r)
171 if err != nil {
172 log.Println("failed to fully resolve repo", err)
173 return
174 }
175
176 ref := chi.URLParam(r, "ref")
177 treePath := chi.URLParam(r, "*")
178 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
179 if err != nil {
180 log.Println("failed to reach knotserver", err)
181 return
182 }
183
184 body, err := io.ReadAll(resp.Body)
185 if err != nil {
186 log.Printf("Error reading response body: %v", err)
187 return
188 }
189
190 var result types.RepoTreeResponse
191 err = json.Unmarshal(body, &result)
192 if err != nil {
193 log.Println("failed to parse response:", err)
194 return
195 }
196
197 user := s.auth.GetUser(r)
198
199 var breadcrumbs [][]string
200 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
201 if treePath != "" {
202 for idx, elem := range strings.Split(treePath, "/") {
203 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
204 }
205 }
206
207 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath)
208 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath)
209
210 s.pages.RepoTree(w, pages.RepoTreeParams{
211 LoggedInUser: user,
212 BreadCrumbs: breadcrumbs,
213 BaseTreeLink: baseTreeLink,
214 BaseBlobLink: baseBlobLink,
215 RepoInfo: f.RepoInfo(s, user),
216 RepoTreeResponse: result,
217 })
218 return
219}
220
221func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
222 f, err := fullyResolvedRepo(r)
223 if err != nil {
224 log.Println("failed to get repo and knot", err)
225 return
226 }
227
228 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName))
229 if err != nil {
230 log.Println("failed to reach knotserver", err)
231 return
232 }
233
234 body, err := io.ReadAll(resp.Body)
235 if err != nil {
236 log.Printf("Error reading response body: %v", err)
237 return
238 }
239
240 var result types.RepoTagsResponse
241 err = json.Unmarshal(body, &result)
242 if err != nil {
243 log.Println("failed to parse response:", err)
244 return
245 }
246
247 user := s.auth.GetUser(r)
248 s.pages.RepoTags(w, pages.RepoTagsParams{
249 LoggedInUser: user,
250 RepoInfo: f.RepoInfo(s, user),
251 RepoTagsResponse: result,
252 })
253 return
254}
255
256func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
257 f, err := fullyResolvedRepo(r)
258 if err != nil {
259 log.Println("failed to get repo and knot", err)
260 return
261 }
262
263 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName))
264 if err != nil {
265 log.Println("failed to reach knotserver", err)
266 return
267 }
268
269 body, err := io.ReadAll(resp.Body)
270 if err != nil {
271 log.Printf("Error reading response body: %v", err)
272 return
273 }
274
275 var result types.RepoBranchesResponse
276 err = json.Unmarshal(body, &result)
277 if err != nil {
278 log.Println("failed to parse response:", err)
279 return
280 }
281
282 user := s.auth.GetUser(r)
283 s.pages.RepoBranches(w, pages.RepoBranchesParams{
284 LoggedInUser: user,
285 RepoInfo: f.RepoInfo(s, user),
286 RepoBranchesResponse: result,
287 })
288 return
289}
290
291func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
292 f, err := fullyResolvedRepo(r)
293 if err != nil {
294 log.Println("failed to get repo and knot", err)
295 return
296 }
297
298 ref := chi.URLParam(r, "ref")
299 filePath := chi.URLParam(r, "*")
300 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
301 if err != nil {
302 log.Println("failed to reach knotserver", err)
303 return
304 }
305
306 body, err := io.ReadAll(resp.Body)
307 if err != nil {
308 log.Printf("Error reading response body: %v", err)
309 return
310 }
311
312 var result types.RepoBlobResponse
313 err = json.Unmarshal(body, &result)
314 if err != nil {
315 log.Println("failed to parse response:", err)
316 return
317 }
318
319 var breadcrumbs [][]string
320 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
321 if filePath != "" {
322 for idx, elem := range strings.Split(filePath, "/") {
323 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
324 }
325 }
326
327 user := s.auth.GetUser(r)
328 s.pages.RepoBlob(w, pages.RepoBlobParams{
329 LoggedInUser: user,
330 RepoInfo: f.RepoInfo(s, user),
331 RepoBlobResponse: result,
332 BreadCrumbs: breadcrumbs,
333 })
334 return
335}
336
337func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
338 f, err := fullyResolvedRepo(r)
339 if err != nil {
340 log.Println("failed to get repo and knot", err)
341 return
342 }
343
344 collaborator := r.FormValue("collaborator")
345 if collaborator == "" {
346 http.Error(w, "malformed form", http.StatusBadRequest)
347 return
348 }
349
350 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
351 if err != nil {
352 w.Write([]byte("failed to resolve collaborator did to a handle"))
353 return
354 }
355 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
356
357 // TODO: create an atproto record for this
358
359 secret, err := db.GetRegistrationKey(s.db, f.Knot)
360 if err != nil {
361 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
362 return
363 }
364
365 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
366 if err != nil {
367 log.Println("failed to create client to ", f.Knot)
368 return
369 }
370
371 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
372 if err != nil {
373 log.Printf("failed to make request to %s: %s", f.Knot, err)
374 return
375 }
376
377 if ksResp.StatusCode != http.StatusNoContent {
378 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
379 return
380 }
381
382 tx, err := s.db.BeginTx(r.Context(), nil)
383 if err != nil {
384 log.Println("failed to start tx")
385 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
386 return
387 }
388 defer func() {
389 tx.Rollback()
390 err = s.enforcer.E.LoadPolicy()
391 if err != nil {
392 log.Println("failed to rollback policies")
393 }
394 }()
395
396 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
397 if err != nil {
398 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
399 return
400 }
401
402 err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
403 if err != nil {
404 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
405 return
406 }
407
408 err = tx.Commit()
409 if err != nil {
410 log.Println("failed to commit changes", err)
411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
413 }
414
415 err = s.enforcer.E.SavePolicy()
416 if err != nil {
417 log.Println("failed to update ACLs", err)
418 http.Error(w, err.Error(), http.StatusInternalServerError)
419 return
420 }
421
422 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
423
424}
425
426func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
427 f, err := fullyResolvedRepo(r)
428 if err != nil {
429 log.Println("failed to get repo and knot", err)
430 return
431 }
432
433 switch r.Method {
434 case http.MethodGet:
435 // for now, this is just pubkeys
436 user := s.auth.GetUser(r)
437 repoCollaborators, err := f.Collaborators(r.Context(), s)
438 if err != nil {
439 log.Println("failed to get collaborators", err)
440 }
441
442 isCollaboratorInviteAllowed := false
443 if user != nil {
444 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
445 if err == nil && ok {
446 isCollaboratorInviteAllowed = true
447 }
448 }
449
450 s.pages.RepoSettings(w, pages.RepoSettingsParams{
451 LoggedInUser: user,
452 RepoInfo: f.RepoInfo(s, user),
453 Collaborators: repoCollaborators,
454 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
455 })
456 }
457}
458
459type FullyResolvedRepo struct {
460 Knot string
461 OwnerId identity.Identity
462 RepoName string
463 RepoAt syntax.ATURI
464}
465
466func (f *FullyResolvedRepo) OwnerDid() string {
467 return f.OwnerId.DID.String()
468}
469
470func (f *FullyResolvedRepo) OwnerHandle() string {
471 return f.OwnerId.Handle.String()
472}
473
474func (f *FullyResolvedRepo) OwnerSlashRepo() string {
475 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
476 return p
477}
478
479func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) {
480 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
481 if err != nil {
482 return nil, err
483 }
484
485 var collaborators []pages.Collaborator
486 for _, item := range repoCollaborators {
487 // currently only two roles: owner and member
488 var role string
489 if item[3] == "repo:owner" {
490 role = "owner"
491 } else if item[3] == "repo:collaborator" {
492 role = "collaborator"
493 } else {
494 continue
495 }
496
497 did := item[0]
498
499 c := pages.Collaborator{
500 Did: did,
501 Handle: "",
502 Role: role,
503 }
504 collaborators = append(collaborators, c)
505 }
506
507 // populate all collborators with handles
508 identsToResolve := make([]string, len(collaborators))
509 for i, collab := range collaborators {
510 identsToResolve[i] = collab.Did
511 }
512
513 resolvedIdents := s.resolver.ResolveIdents(ctx, identsToResolve)
514 for i, resolved := range resolvedIdents {
515 if resolved != nil {
516 collaborators[i].Handle = resolved.Handle.String()
517 }
518 }
519
520 return collaborators, nil
521}
522
523func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo {
524 isStarred := false
525 if u != nil {
526 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
527 }
528
529 starCount, err := db.GetStarCount(s.db, f.RepoAt)
530 if err != nil {
531 log.Println("failed to get star count for ", f.RepoAt)
532 }
533 issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
534 if err != nil {
535 log.Println("failed to get issue count for ", f.RepoAt)
536 }
537
538 knot := f.Knot
539 if knot == "knot1.tangled.sh" {
540 knot = "tangled.sh"
541 }
542
543 return pages.RepoInfo{
544 OwnerDid: f.OwnerDid(),
545 OwnerHandle: f.OwnerHandle(),
546 Name: f.RepoName,
547 RepoAt: f.RepoAt,
548 SettingsAllowed: settingsAllowed(s, u, f),
549 IsStarred: isStarred,
550 Knot: knot,
551 Stats: db.RepoStats{
552 StarCount: starCount,
553 IssueCount: issueCount,
554 },
555 }
556}
557
558func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
559 user := s.auth.GetUser(r)
560 f, err := fullyResolvedRepo(r)
561 if err != nil {
562 log.Println("failed to get repo and knot", err)
563 return
564 }
565
566 issueId := chi.URLParam(r, "issue")
567 issueIdInt, err := strconv.Atoi(issueId)
568 if err != nil {
569 http.Error(w, "bad issue id", http.StatusBadRequest)
570 log.Println("failed to parse issue id", err)
571 return
572 }
573
574 issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
575 if err != nil {
576 log.Println("failed to get issue and comments", err)
577 s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
578 return
579 }
580
581 issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
582 if err != nil {
583 log.Println("failed to resolve issue owner", err)
584 }
585
586 identsToResolve := make([]string, len(comments))
587 for i, comment := range comments {
588 identsToResolve[i] = comment.OwnerDid
589 }
590 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
591 didHandleMap := make(map[string]string)
592 for _, identity := range resolvedIds {
593 if !identity.Handle.IsInvalidHandle() {
594 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
595 } else {
596 didHandleMap[identity.DID.String()] = identity.DID.String()
597 }
598 }
599
600 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
601 LoggedInUser: user,
602 RepoInfo: f.RepoInfo(s, user),
603 Issue: *issue,
604 Comments: comments,
605
606 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
607 DidHandleMap: didHandleMap,
608 })
609
610}
611
612func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
613 user := s.auth.GetUser(r)
614 f, err := fullyResolvedRepo(r)
615 if err != nil {
616 log.Println("failed to get repo and knot", err)
617 return
618 }
619
620 issueId := chi.URLParam(r, "issue")
621 issueIdInt, err := strconv.Atoi(issueId)
622 if err != nil {
623 http.Error(w, "bad issue id", http.StatusBadRequest)
624 log.Println("failed to parse issue id", err)
625 return
626 }
627
628 issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
629 if err != nil {
630 log.Println("failed to get issue", err)
631 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
632 return
633 }
634
635 collaborators, err := f.Collaborators(r.Context(), s)
636 if err != nil {
637 log.Println("failed to fetch repo collaborators: %w", err)
638 }
639 isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
640 return user.Did == collab.Did
641 })
642 isIssueOwner := user.Did == issue.OwnerDid
643
644 // TODO: make this more granular
645 if isIssueOwner || isCollaborator {
646
647 closed := tangled.RepoIssueStateClosed
648
649 client, _ := s.auth.AuthorizedClient(r)
650 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
651 Collection: tangled.RepoIssueStateNSID,
652 Repo: issue.OwnerDid,
653 Rkey: s.TID(),
654 Record: &lexutil.LexiconTypeDecoder{
655 Val: &tangled.RepoIssueState{
656 Issue: issue.IssueAt,
657 State: &closed,
658 },
659 },
660 })
661
662 if err != nil {
663 log.Println("failed to update issue state", err)
664 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
665 return
666 }
667
668 err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
669 if err != nil {
670 log.Println("failed to close issue", err)
671 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
672 return
673 }
674
675 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
676 return
677 } else {
678 log.Println("user is not permitted to close issue")
679 http.Error(w, "for biden", http.StatusUnauthorized)
680 return
681 }
682}
683
684func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
685 user := s.auth.GetUser(r)
686 f, err := fullyResolvedRepo(r)
687 if err != nil {
688 log.Println("failed to get repo and knot", err)
689 return
690 }
691
692 issueId := chi.URLParam(r, "issue")
693 issueIdInt, err := strconv.Atoi(issueId)
694 if err != nil {
695 http.Error(w, "bad issue id", http.StatusBadRequest)
696 log.Println("failed to parse issue id", err)
697 return
698 }
699
700 if user.Did == f.OwnerDid() {
701 err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
702 if err != nil {
703 log.Println("failed to reopen issue", err)
704 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
705 return
706 }
707 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
708 return
709 } else {
710 log.Println("user is not the owner of the repo")
711 http.Error(w, "forbidden", http.StatusUnauthorized)
712 return
713 }
714}
715
716func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
717 user := s.auth.GetUser(r)
718 f, err := fullyResolvedRepo(r)
719 if err != nil {
720 log.Println("failed to get repo and knot", err)
721 return
722 }
723
724 issueId := chi.URLParam(r, "issue")
725 issueIdInt, err := strconv.Atoi(issueId)
726 if err != nil {
727 http.Error(w, "bad issue id", http.StatusBadRequest)
728 log.Println("failed to parse issue id", err)
729 return
730 }
731
732 switch r.Method {
733 case http.MethodPost:
734 body := r.FormValue("body")
735 if body == "" {
736 s.pages.Notice(w, "issue", "Body is required")
737 return
738 }
739
740 commentId := rand.IntN(1000000)
741
742 err := db.NewComment(s.db, &db.Comment{
743 OwnerDid: user.Did,
744 RepoAt: f.RepoAt,
745 Issue: issueIdInt,
746 CommentId: commentId,
747 Body: body,
748 })
749 if err != nil {
750 log.Println("failed to create comment", err)
751 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
752 return
753 }
754
755 createdAt := time.Now().Format(time.RFC3339)
756 commentIdInt64 := int64(commentId)
757 ownerDid := user.Did
758 issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
759 if err != nil {
760 log.Println("failed to get issue at", err)
761 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
762 return
763 }
764
765 atUri := f.RepoAt.String()
766 client, _ := s.auth.AuthorizedClient(r)
767 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
768 Collection: tangled.RepoIssueCommentNSID,
769 Repo: user.Did,
770 Rkey: s.TID(),
771 Record: &lexutil.LexiconTypeDecoder{
772 Val: &tangled.RepoIssueComment{
773 Repo: &atUri,
774 Issue: issueAt,
775 CommentId: &commentIdInt64,
776 Owner: &ownerDid,
777 Body: &body,
778 CreatedAt: &createdAt,
779 },
780 },
781 })
782 if err != nil {
783 log.Println("failed to create comment", err)
784 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
785 return
786 }
787
788 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
789 return
790 }
791}
792
793func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
794 user := s.auth.GetUser(r)
795 f, err := fullyResolvedRepo(r)
796 if err != nil {
797 log.Println("failed to get repo and knot", err)
798 return
799 }
800
801 issues, err := db.GetIssues(s.db, f.RepoAt)
802 if err != nil {
803 log.Println("failed to get issues", err)
804 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
805 return
806 }
807
808 identsToResolve := make([]string, len(issues))
809 for i, issue := range issues {
810 identsToResolve[i] = issue.OwnerDid
811 }
812 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
813 didHandleMap := make(map[string]string)
814 for _, identity := range resolvedIds {
815 if !identity.Handle.IsInvalidHandle() {
816 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
817 } else {
818 didHandleMap[identity.DID.String()] = identity.DID.String()
819 }
820 }
821
822 s.pages.RepoIssues(w, pages.RepoIssuesParams{
823 LoggedInUser: s.auth.GetUser(r),
824 RepoInfo: f.RepoInfo(s, user),
825 Issues: issues,
826 DidHandleMap: didHandleMap,
827 })
828 return
829}
830
831func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
832 user := s.auth.GetUser(r)
833
834 f, err := fullyResolvedRepo(r)
835 if err != nil {
836 log.Println("failed to get repo and knot", err)
837 return
838 }
839
840 switch r.Method {
841 case http.MethodGet:
842 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
843 LoggedInUser: user,
844 RepoInfo: f.RepoInfo(s, user),
845 })
846 case http.MethodPost:
847 title := r.FormValue("title")
848 body := r.FormValue("body")
849
850 if title == "" || body == "" {
851 s.pages.Notice(w, "issues", "Title and body are required")
852 return
853 }
854
855 tx, err := s.db.BeginTx(r.Context(), nil)
856 if err != nil {
857 s.pages.Notice(w, "issues", "Failed to create issue, try again later")
858 return
859 }
860
861 err = db.NewIssue(tx, &db.Issue{
862 RepoAt: f.RepoAt,
863 Title: title,
864 Body: body,
865 OwnerDid: user.Did,
866 })
867 if err != nil {
868 log.Println("failed to create issue", err)
869 s.pages.Notice(w, "issues", "Failed to create issue.")
870 return
871 }
872
873 issueId, err := db.GetIssueId(s.db, f.RepoAt)
874 if err != nil {
875 log.Println("failed to get issue id", err)
876 s.pages.Notice(w, "issues", "Failed to create issue.")
877 return
878 }
879
880 client, _ := s.auth.AuthorizedClient(r)
881 atUri := f.RepoAt.String()
882 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
883 Collection: tangled.RepoIssueNSID,
884 Repo: user.Did,
885 Rkey: s.TID(),
886 Record: &lexutil.LexiconTypeDecoder{
887 Val: &tangled.RepoIssue{
888 Repo: atUri,
889 Title: title,
890 Body: &body,
891 Owner: user.Did,
892 IssueId: int64(issueId),
893 },
894 },
895 })
896 if err != nil {
897 log.Println("failed to create issue", err)
898 s.pages.Notice(w, "issues", "Failed to create issue.")
899 return
900 }
901
902 err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
903 if err != nil {
904 log.Println("failed to set issue at", err)
905 s.pages.Notice(w, "issues", "Failed to create issue.")
906 return
907 }
908
909 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
910 return
911 }
912}
913
914func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
915 user := s.auth.GetUser(r)
916 f, err := fullyResolvedRepo(r)
917 if err != nil {
918 log.Println("failed to get repo and knot", err)
919 return
920 }
921
922 switch r.Method {
923 case http.MethodGet:
924 s.pages.RepoPulls(w, pages.RepoPullsParams{
925 LoggedInUser: user,
926 RepoInfo: f.RepoInfo(s, user),
927 })
928 }
929}
930
931func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
932 repoName := chi.URLParam(r, "repo")
933 knot, ok := r.Context().Value("knot").(string)
934 if !ok {
935 log.Println("malformed middleware")
936 return nil, fmt.Errorf("malformed middleware")
937 }
938 id, ok := r.Context().Value("resolvedId").(identity.Identity)
939 if !ok {
940 log.Println("malformed middleware")
941 return nil, fmt.Errorf("malformed middleware")
942 }
943
944 repoAt, ok := r.Context().Value("repoAt").(string)
945 if !ok {
946 log.Println("malformed middleware")
947 return nil, fmt.Errorf("malformed middleware")
948 }
949
950 parsedRepoAt, err := syntax.ParseATURI(repoAt)
951 if err != nil {
952 log.Println("malformed repo at-uri")
953 return nil, fmt.Errorf("malformed middleware")
954 }
955
956 return &FullyResolvedRepo{
957 Knot: knot,
958 OwnerId: id,
959 RepoName: repoName,
960 RepoAt: parsedRepoAt,
961 }, nil
962}
963
964func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
965 settingsAllowed := false
966 if u != nil {
967 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
968 if err == nil && ok {
969 settingsAllowed = true
970 } else {
971 log.Println(err, ok)
972 }
973 }
974
975 return settingsAllowed
976}