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