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