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