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