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