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