883 lines
22 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
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
549
LoggedInUser: user,
550
RepoInfo: pages.RepoInfo{
551
OwnerDid: f.OwnerDid(),
552
OwnerHandle: f.OwnerHandle(),
553
Name: f.RepoName,
554
SettingsAllowed: settingsAllowed(s, user, f),
555
},
556
Issue: *issue,
557
Comments: comments,
558
559
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
560
})
561
562
}
563
564
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
565
user := s.auth.GetUser(r)
566
f, err := fullyResolvedRepo(r)
567
if err != nil {
568
log.Println("failed to get repo and knot", err)
569
return
570
}
571
572
issueId := chi.URLParam(r, "issue")
573
issueIdInt, err := strconv.Atoi(issueId)
574
if err != nil {
575
http.Error(w, "bad issue id", http.StatusBadRequest)
576
log.Println("failed to parse issue id", err)
577
return
578
}
579
580
issue, err := s.db.GetIssue(f.RepoAt, issueIdInt)
581
if err != nil {
582
log.Println("failed to get issue", err)
583
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
584
return
585
}
586
587
// TODO: make this more granular
588
if user.Did == f.OwnerDid() {
589
590
closed := tangled.RepoIssueStateClosed
591
592
client, _ := s.auth.AuthorizedClient(r)
593
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
594
Collection: tangled.RepoIssueStateNSID,
595
Repo: issue.OwnerDid,
596
Rkey: s.TID(),
597
Record: &lexutil.LexiconTypeDecoder{
598
Val: &tangled.RepoIssueState{
599
Issue: issue.IssueAt,
600
State: &closed,
601
},
602
},
603
})
604
605
if err != nil {
606
log.Println("failed to update issue state", err)
607
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
608
return
609
}
610
611
err := s.db.CloseIssue(f.RepoAt, issueIdInt)
612
if err != nil {
613
log.Println("failed to close issue", err)
614
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
615
return
616
}
617
618
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
619
return
620
} else {
621
log.Println("user is not the owner of the repo")
622
http.Error(w, "for biden", http.StatusUnauthorized)
623
return
624
}
625
}
626
627
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
628
user := s.auth.GetUser(r)
629
f, err := fullyResolvedRepo(r)
630
if err != nil {
631
log.Println("failed to get repo and knot", err)
632
return
633
}
634
635
issueId := chi.URLParam(r, "issue")
636
issueIdInt, err := strconv.Atoi(issueId)
637
if err != nil {
638
http.Error(w, "bad issue id", http.StatusBadRequest)
639
log.Println("failed to parse issue id", err)
640
return
641
}
642
643
if user.Did == f.OwnerDid() {
644
err := s.db.ReopenIssue(f.RepoAt, issueIdInt)
645
if err != nil {
646
log.Println("failed to reopen issue", err)
647
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
648
return
649
}
650
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
651
return
652
} else {
653
log.Println("user is not the owner of the repo")
654
http.Error(w, "forbidden", http.StatusUnauthorized)
655
return
656
}
657
}
658
659
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
660
user := s.auth.GetUser(r)
661
f, err := fullyResolvedRepo(r)
662
if err != nil {
663
log.Println("failed to get repo and knot", err)
664
return
665
}
666
667
issueId := chi.URLParam(r, "issue")
668
issueIdInt, err := strconv.Atoi(issueId)
669
if err != nil {
670
http.Error(w, "bad issue id", http.StatusBadRequest)
671
log.Println("failed to parse issue id", err)
672
return
673
}
674
675
switch r.Method {
676
case http.MethodPost:
677
body := r.FormValue("body")
678
if body == "" {
679
s.pages.Notice(w, "issue", "Body is required")
680
return
681
}
682
683
commentId := rand.IntN(1000000)
684
685
err := s.db.NewComment(&db.Comment{
686
OwnerDid: user.Did,
687
RepoAt: f.RepoAt,
688
Issue: issueIdInt,
689
CommentId: commentId,
690
Body: body,
691
})
692
if err != nil {
693
log.Println("failed to create comment", err)
694
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
695
return
696
}
697
698
createdAt := time.Now().Format(time.RFC3339)
699
commentIdInt64 := int64(commentId)
700
ownerDid := user.Did
701
issueAt, err := s.db.GetIssueAt(f.RepoAt, issueIdInt)
702
if err != nil {
703
log.Println("failed to get issue at", err)
704
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
705
return
706
}
707
708
client, _ := s.auth.AuthorizedClient(r)
709
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
710
Collection: tangled.RepoIssueCommentNSID,
711
Repo: user.Did,
712
Rkey: s.TID(),
713
Record: &lexutil.LexiconTypeDecoder{
714
Val: &tangled.RepoIssueComment{
715
Repo: &f.RepoAt,
716
Issue: issueAt,
717
CommentId: &commentIdInt64,
718
Owner: &ownerDid,
719
Body: &body,
720
CreatedAt: &createdAt,
721
},
722
},
723
})
724
if err != nil {
725
log.Println("failed to create comment", err)
726
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
727
return
728
}
729
730
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
731
return
732
}
733
}
734
735
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
736
user := s.auth.GetUser(r)
737
f, err := fullyResolvedRepo(r)
738
if err != nil {
739
log.Println("failed to get repo and knot", err)
740
return
741
}
742
743
issues, err := s.db.GetIssues(f.RepoAt)
744
if err != nil {
745
log.Println("failed to get issues", err)
746
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
747
return
748
}
749
750
s.pages.RepoIssues(w, pages.RepoIssuesParams{
751
LoggedInUser: s.auth.GetUser(r),
752
RepoInfo: pages.RepoInfo{
753
OwnerDid: f.OwnerDid(),
754
OwnerHandle: f.OwnerHandle(),
755
Name: f.RepoName,
756
SettingsAllowed: settingsAllowed(s, user, f),
757
},
758
Issues: issues,
759
})
760
return
761
}
762
763
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
764
user := s.auth.GetUser(r)
765
766
f, err := fullyResolvedRepo(r)
767
if err != nil {
768
log.Println("failed to get repo and knot", err)
769
return
770
}
771
772
switch r.Method {
773
case http.MethodGet:
774
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
775
LoggedInUser: user,
776
RepoInfo: pages.RepoInfo{
777
Name: f.RepoName,
778
OwnerDid: f.OwnerDid(),
779
OwnerHandle: f.OwnerHandle(),
780
SettingsAllowed: settingsAllowed(s, user, f),
781
},
782
})
783
case http.MethodPost:
784
title := r.FormValue("title")
785
body := r.FormValue("body")
786
787
if title == "" || body == "" {
788
s.pages.Notice(w, "issues", "Title and body are required")
789
return
790
}
791
792
err = s.db.NewIssue(&db.Issue{
793
RepoAt: f.RepoAt,
794
Title: title,
795
Body: body,
796
OwnerDid: user.Did,
797
})
798
if err != nil {
799
log.Println("failed to create issue", err)
800
s.pages.Notice(w, "issues", "Failed to create issue.")
801
return
802
}
803
804
issueId, err := s.db.GetIssueId(f.RepoAt)
805
if err != nil {
806
log.Println("failed to get issue id", err)
807
s.pages.Notice(w, "issues", "Failed to create issue.")
808
return
809
}
810
811
client, _ := s.auth.AuthorizedClient(r)
812
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
813
Collection: tangled.RepoIssueNSID,
814
Repo: user.Did,
815
Rkey: s.TID(),
816
Record: &lexutil.LexiconTypeDecoder{
817
Val: &tangled.RepoIssue{
818
Repo: f.RepoAt,
819
Title: title,
820
Body: &body,
821
Owner: user.Did,
822
IssueId: int64(issueId),
823
},
824
},
825
})
826
if err != nil {
827
log.Println("failed to create issue", err)
828
s.pages.Notice(w, "issues", "Failed to create issue.")
829
return
830
}
831
832
err = s.db.SetIssueAt(f.RepoAt, issueId, resp.Uri)
833
if err != nil {
834
log.Println("failed to set issue at", err)
835
s.pages.Notice(w, "issues", "Failed to create issue.")
836
return
837
}
838
839
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
840
return
841
}
842
}
843
844
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
845
repoName := chi.URLParam(r, "repo")
846
knot, ok := r.Context().Value("knot").(string)
847
if !ok {
848
log.Println("malformed middleware")
849
return nil, fmt.Errorf("malformed middleware")
850
}
851
id, ok := r.Context().Value("resolvedId").(identity.Identity)
852
if !ok {
853
log.Println("malformed middleware")
854
return nil, fmt.Errorf("malformed middleware")
855
}
856
857
repoAt, ok := r.Context().Value("repoAt").(string)
858
if !ok {
859
log.Println("malformed middleware")
860
return nil, fmt.Errorf("malformed middleware")
861
}
862
863
return &FullyResolvedRepo{
864
Knot: knot,
865
OwnerId: id,
866
RepoName: repoName,
867
RepoAt: repoAt,
868
}, nil
869
}
870
871
func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
872
settingsAllowed := false
873
if u != nil {
874
ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
875
if err == nil && ok {
876
settingsAllowed = true
877
} else {
878
log.Println(err, ok)
879
}
880
}
881
882
return settingsAllowed
883
}
884