951 lines
24 kB
1
package state
2
3
import (
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
29
func (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
93
func (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
142
func (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
183
func (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
242
func (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
282
func (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
322
func (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
373
func (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
434
func (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
472
type FullyResolvedRepo struct {
473
Knot string
474
OwnerId identity.Identity
475
RepoName string
476
RepoAt string
477
}
478
479
func (f *FullyResolvedRepo) OwnerDid() string {
480
return f.OwnerId.DID.String()
481
}
482
483
func (f *FullyResolvedRepo) OwnerHandle() string {
484
return f.OwnerId.Handle.String()
485
}
486
487
func (f *FullyResolvedRepo) OwnerSlashRepo() string {
488
p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
489
return p
490
}
491
492
func (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
536
func (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
595
func (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
658
func (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
690
func (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
766
func (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
809
func (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
890
func (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
912
func 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
939
func 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
}
952