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