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