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