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