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