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