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.Printf("error reading response body: %v", err)
104 return
105 }
106
107 var repolog types.RepoLogResponse
108 err = json.Unmarshal(body, &repolog)
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: repolog,
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 identsToResolve := make([]string, len(comments))
549 for i, comment := range comments {
550 identsToResolve[i] = comment.OwnerDid
551 }
552 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
553 didHandleMap := make(map[string]string)
554 for _, identity := range resolvedIds {
555 if !identity.Handle.IsInvalidHandle() {
556 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
557 } else {
558 didHandleMap[identity.DID.String()] = identity.DID.String()
559 }
560 }
561
562 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
563 LoggedInUser: user,
564 RepoInfo: pages.RepoInfo{
565 OwnerDid: f.OwnerDid(),
566 OwnerHandle: f.OwnerHandle(),
567 Name: f.RepoName,
568 SettingsAllowed: settingsAllowed(s, user, f),
569 },
570 Issue: *issue,
571 Comments: comments,
572
573 IssueOwnerHandle: issueOwnerIdent.Handle.String(),
574 DidHandleMap: didHandleMap,
575 })
576
577}
578
579func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
580 user := s.auth.GetUser(r)
581 f, err := fullyResolvedRepo(r)
582 if err != nil {
583 log.Println("failed to get repo and knot", err)
584 return
585 }
586
587 issueId := chi.URLParam(r, "issue")
588 issueIdInt, err := strconv.Atoi(issueId)
589 if err != nil {
590 http.Error(w, "bad issue id", http.StatusBadRequest)
591 log.Println("failed to parse issue id", err)
592 return
593 }
594
595 issue, err := s.db.GetIssue(f.RepoAt, issueIdInt)
596 if err != nil {
597 log.Println("failed to get issue", err)
598 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
599 return
600 }
601
602 // TODO: make this more granular
603 if user.Did == f.OwnerDid() {
604
605 closed := tangled.RepoIssueStateClosed
606
607 client, _ := s.auth.AuthorizedClient(r)
608 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
609 Collection: tangled.RepoIssueStateNSID,
610 Repo: issue.OwnerDid,
611 Rkey: s.TID(),
612 Record: &lexutil.LexiconTypeDecoder{
613 Val: &tangled.RepoIssueState{
614 Issue: issue.IssueAt,
615 State: &closed,
616 },
617 },
618 })
619
620 if err != nil {
621 log.Println("failed to update issue state", err)
622 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
623 return
624 }
625
626 err := s.db.CloseIssue(f.RepoAt, issueIdInt)
627 if err != nil {
628 log.Println("failed to close issue", err)
629 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
630 return
631 }
632
633 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
634 return
635 } else {
636 log.Println("user is not the owner of the repo")
637 http.Error(w, "for biden", http.StatusUnauthorized)
638 return
639 }
640}
641
642func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
643 user := s.auth.GetUser(r)
644 f, err := fullyResolvedRepo(r)
645 if err != nil {
646 log.Println("failed to get repo and knot", err)
647 return
648 }
649
650 issueId := chi.URLParam(r, "issue")
651 issueIdInt, err := strconv.Atoi(issueId)
652 if err != nil {
653 http.Error(w, "bad issue id", http.StatusBadRequest)
654 log.Println("failed to parse issue id", err)
655 return
656 }
657
658 if user.Did == f.OwnerDid() {
659 err := s.db.ReopenIssue(f.RepoAt, issueIdInt)
660 if err != nil {
661 log.Println("failed to reopen issue", err)
662 s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
663 return
664 }
665 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
666 return
667 } else {
668 log.Println("user is not the owner of the repo")
669 http.Error(w, "forbidden", http.StatusUnauthorized)
670 return
671 }
672}
673
674func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
675 user := s.auth.GetUser(r)
676 f, err := fullyResolvedRepo(r)
677 if err != nil {
678 log.Println("failed to get repo and knot", err)
679 return
680 }
681
682 issueId := chi.URLParam(r, "issue")
683 issueIdInt, err := strconv.Atoi(issueId)
684 if err != nil {
685 http.Error(w, "bad issue id", http.StatusBadRequest)
686 log.Println("failed to parse issue id", err)
687 return
688 }
689
690 switch r.Method {
691 case http.MethodPost:
692 body := r.FormValue("body")
693 if body == "" {
694 s.pages.Notice(w, "issue", "Body is required")
695 return
696 }
697
698 commentId := rand.IntN(1000000)
699
700 err := s.db.NewComment(&db.Comment{
701 OwnerDid: user.Did,
702 RepoAt: f.RepoAt,
703 Issue: issueIdInt,
704 CommentId: commentId,
705 Body: body,
706 })
707 if err != nil {
708 log.Println("failed to create comment", err)
709 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
710 return
711 }
712
713 createdAt := time.Now().Format(time.RFC3339)
714 commentIdInt64 := int64(commentId)
715 ownerDid := user.Did
716 issueAt, err := s.db.GetIssueAt(f.RepoAt, issueIdInt)
717 if err != nil {
718 log.Println("failed to get issue at", err)
719 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
720 return
721 }
722
723 client, _ := s.auth.AuthorizedClient(r)
724 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
725 Collection: tangled.RepoIssueCommentNSID,
726 Repo: user.Did,
727 Rkey: s.TID(),
728 Record: &lexutil.LexiconTypeDecoder{
729 Val: &tangled.RepoIssueComment{
730 Repo: &f.RepoAt,
731 Issue: issueAt,
732 CommentId: &commentIdInt64,
733 Owner: &ownerDid,
734 Body: &body,
735 CreatedAt: &createdAt,
736 },
737 },
738 })
739 if err != nil {
740 log.Println("failed to create comment", err)
741 s.pages.Notice(w, "issue-comment", "Failed to create comment.")
742 return
743 }
744
745 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
746 return
747 }
748}
749
750func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
751 user := s.auth.GetUser(r)
752 f, err := fullyResolvedRepo(r)
753 if err != nil {
754 log.Println("failed to get repo and knot", err)
755 return
756 }
757
758 issues, err := s.db.GetIssues(f.RepoAt)
759 if err != nil {
760 log.Println("failed to get issues", err)
761 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
762 return
763 }
764
765 identsToResolve := make([]string, len(issues))
766 for i, issue := range issues {
767 identsToResolve[i] = issue.OwnerDid
768 }
769 resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
770 didHandleMap := make(map[string]string)
771 for _, identity := range resolvedIds {
772 if !identity.Handle.IsInvalidHandle() {
773 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
774 } else {
775 didHandleMap[identity.DID.String()] = identity.DID.String()
776 }
777 }
778
779 s.pages.RepoIssues(w, pages.RepoIssuesParams{
780 LoggedInUser: s.auth.GetUser(r),
781 RepoInfo: pages.RepoInfo{
782 OwnerDid: f.OwnerDid(),
783 OwnerHandle: f.OwnerHandle(),
784 Name: f.RepoName,
785 SettingsAllowed: settingsAllowed(s, user, f),
786 },
787 Issues: issues,
788 DidHandleMap: didHandleMap,
789 })
790 return
791}
792
793func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
794 user := s.auth.GetUser(r)
795
796 f, err := fullyResolvedRepo(r)
797 if err != nil {
798 log.Println("failed to get repo and knot", err)
799 return
800 }
801
802 switch r.Method {
803 case http.MethodGet:
804 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
805 LoggedInUser: user,
806 RepoInfo: pages.RepoInfo{
807 Name: f.RepoName,
808 OwnerDid: f.OwnerDid(),
809 OwnerHandle: f.OwnerHandle(),
810 SettingsAllowed: settingsAllowed(s, user, f),
811 },
812 })
813 case http.MethodPost:
814 title := r.FormValue("title")
815 body := r.FormValue("body")
816
817 if title == "" || body == "" {
818 s.pages.Notice(w, "issues", "Title and body are required")
819 return
820 }
821
822 err = s.db.NewIssue(&db.Issue{
823 RepoAt: f.RepoAt,
824 Title: title,
825 Body: body,
826 OwnerDid: user.Did,
827 })
828 if err != nil {
829 log.Println("failed to create issue", err)
830 s.pages.Notice(w, "issues", "Failed to create issue.")
831 return
832 }
833
834 issueId, err := s.db.GetIssueId(f.RepoAt)
835 if err != nil {
836 log.Println("failed to get issue id", err)
837 s.pages.Notice(w, "issues", "Failed to create issue.")
838 return
839 }
840
841 client, _ := s.auth.AuthorizedClient(r)
842 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
843 Collection: tangled.RepoIssueNSID,
844 Repo: user.Did,
845 Rkey: s.TID(),
846 Record: &lexutil.LexiconTypeDecoder{
847 Val: &tangled.RepoIssue{
848 Repo: f.RepoAt,
849 Title: title,
850 Body: &body,
851 Owner: user.Did,
852 IssueId: int64(issueId),
853 },
854 },
855 })
856 if err != nil {
857 log.Println("failed to create issue", err)
858 s.pages.Notice(w, "issues", "Failed to create issue.")
859 return
860 }
861
862 err = s.db.SetIssueAt(f.RepoAt, issueId, resp.Uri)
863 if err != nil {
864 log.Println("failed to set issue at", err)
865 s.pages.Notice(w, "issues", "Failed to create issue.")
866 return
867 }
868
869 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
870 return
871 }
872}
873
874func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
875 repoName := chi.URLParam(r, "repo")
876 knot, ok := r.Context().Value("knot").(string)
877 if !ok {
878 log.Println("malformed middleware")
879 return nil, fmt.Errorf("malformed middleware")
880 }
881 id, ok := r.Context().Value("resolvedId").(identity.Identity)
882 if !ok {
883 log.Println("malformed middleware")
884 return nil, fmt.Errorf("malformed middleware")
885 }
886
887 repoAt, ok := r.Context().Value("repoAt").(string)
888 if !ok {
889 log.Println("malformed middleware")
890 return nil, fmt.Errorf("malformed middleware")
891 }
892
893 return &FullyResolvedRepo{
894 Knot: knot,
895 OwnerId: id,
896 RepoName: repoName,
897 RepoAt: repoAt,
898 }, nil
899}
900
901func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
902 settingsAllowed := false
903 if u != nil {
904 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
905 if err == nil && ok {
906 settingsAllowed = true
907 } else {
908 log.Println(err, ok)
909 }
910 }
911
912 return settingsAllowed
913}