947 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
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
238
func (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
278
func (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
318
func (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
369
func (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
430
func (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
468
type FullyResolvedRepo struct {
469
Knot string
470
OwnerId identity.Identity
471
RepoName string
472
RepoAt string
473
}
474
475
func (f *FullyResolvedRepo) OwnerDid() string {
476
return f.OwnerId.DID.String()
477
}
478
479
func (f *FullyResolvedRepo) OwnerHandle() string {
480
return f.OwnerId.Handle.String()
481
}
482
483
func (f *FullyResolvedRepo) OwnerSlashRepo() string {
484
p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
485
return p
486
}
487
488
func (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
532
func (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
591
func (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
654
func (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
686
func (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
762
func (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
805
func (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
886
func (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
908
func 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
935
func 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
}
948