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