+1
-1
.air/appview.toml
+1
-1
.air/appview.toml
+4
-1
.gitignore
+4
-1
.gitignore
+31
api/tangled/actorprofile.go
+31
api/tangled/actorprofile.go
···
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.actor.profile
6
+
7
+
import (
8
+
"github.com/bluesky-social/indigo/lex/util"
9
+
)
10
+
11
+
const (
12
+
ActorProfileNSID = "sh.tangled.actor.profile"
13
+
)
14
+
15
+
func init() {
16
+
util.RegisterType("sh.tangled.actor.profile", &ActorProfile{})
17
+
} //
18
+
// RECORDTYPE: ActorProfile
19
+
type ActorProfile struct {
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
21
+
// bluesky: Include link to this account on Bluesky.
22
+
Bluesky bool `json:"bluesky" cborgen:"bluesky"`
23
+
// description: Free-form profile description text.
24
+
Description *string `json:"description,omitempty" cborgen:"description,omitempty"`
25
+
Links []string `json:"links,omitempty" cborgen:"links,omitempty"`
26
+
// location: Free-form location text.
27
+
Location *string `json:"location,omitempty" cborgen:"location,omitempty"`
28
+
// pinnedRepositories: Any ATURI, it is up to appviews to validate these fields.
29
+
PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"`
30
+
Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
31
+
}
+485
api/tangled/cbor_gen.go
+485
api/tangled/cbor_gen.go
···
3389
3390
return nil
3391
}
3392
+
func (t *ActorProfile) MarshalCBOR(w io.Writer) error {
3393
+
if t == nil {
3394
+
_, err := w.Write(cbg.CborNull)
3395
+
return err
3396
+
}
3397
+
3398
+
cw := cbg.NewCborWriter(w)
3399
+
fieldCount := 7
3400
+
3401
+
if t.Description == nil {
3402
+
fieldCount--
3403
+
}
3404
+
3405
+
if t.Links == nil {
3406
+
fieldCount--
3407
+
}
3408
+
3409
+
if t.Location == nil {
3410
+
fieldCount--
3411
+
}
3412
+
3413
+
if t.PinnedRepositories == nil {
3414
+
fieldCount--
3415
+
}
3416
+
3417
+
if t.Stats == nil {
3418
+
fieldCount--
3419
+
}
3420
+
3421
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
3422
+
return err
3423
+
}
3424
+
3425
+
// t.LexiconTypeID (string) (string)
3426
+
if len("$type") > 1000000 {
3427
+
return xerrors.Errorf("Value in field \"$type\" was too long")
3428
+
}
3429
+
3430
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
3431
+
return err
3432
+
}
3433
+
if _, err := cw.WriteString(string("$type")); err != nil {
3434
+
return err
3435
+
}
3436
+
3437
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.actor.profile"))); err != nil {
3438
+
return err
3439
+
}
3440
+
if _, err := cw.WriteString(string("sh.tangled.actor.profile")); err != nil {
3441
+
return err
3442
+
}
3443
+
3444
+
// t.Links ([]string) (slice)
3445
+
if t.Links != nil {
3446
+
3447
+
if len("links") > 1000000 {
3448
+
return xerrors.Errorf("Value in field \"links\" was too long")
3449
+
}
3450
+
3451
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil {
3452
+
return err
3453
+
}
3454
+
if _, err := cw.WriteString(string("links")); err != nil {
3455
+
return err
3456
+
}
3457
+
3458
+
if len(t.Links) > 8192 {
3459
+
return xerrors.Errorf("Slice value in field t.Links was too long")
3460
+
}
3461
+
3462
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil {
3463
+
return err
3464
+
}
3465
+
for _, v := range t.Links {
3466
+
if len(v) > 1000000 {
3467
+
return xerrors.Errorf("Value in field v was too long")
3468
+
}
3469
+
3470
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3471
+
return err
3472
+
}
3473
+
if _, err := cw.WriteString(string(v)); err != nil {
3474
+
return err
3475
+
}
3476
+
3477
+
}
3478
+
}
3479
+
3480
+
// t.Stats ([]string) (slice)
3481
+
if t.Stats != nil {
3482
+
3483
+
if len("stats") > 1000000 {
3484
+
return xerrors.Errorf("Value in field \"stats\" was too long")
3485
+
}
3486
+
3487
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil {
3488
+
return err
3489
+
}
3490
+
if _, err := cw.WriteString(string("stats")); err != nil {
3491
+
return err
3492
+
}
3493
+
3494
+
if len(t.Stats) > 8192 {
3495
+
return xerrors.Errorf("Slice value in field t.Stats was too long")
3496
+
}
3497
+
3498
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil {
3499
+
return err
3500
+
}
3501
+
for _, v := range t.Stats {
3502
+
if len(v) > 1000000 {
3503
+
return xerrors.Errorf("Value in field v was too long")
3504
+
}
3505
+
3506
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3507
+
return err
3508
+
}
3509
+
if _, err := cw.WriteString(string(v)); err != nil {
3510
+
return err
3511
+
}
3512
+
3513
+
}
3514
+
}
3515
+
3516
+
// t.Bluesky (bool) (bool)
3517
+
if len("bluesky") > 1000000 {
3518
+
return xerrors.Errorf("Value in field \"bluesky\" was too long")
3519
+
}
3520
+
3521
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil {
3522
+
return err
3523
+
}
3524
+
if _, err := cw.WriteString(string("bluesky")); err != nil {
3525
+
return err
3526
+
}
3527
+
3528
+
if err := cbg.WriteBool(w, t.Bluesky); err != nil {
3529
+
return err
3530
+
}
3531
+
3532
+
// t.Location (string) (string)
3533
+
if t.Location != nil {
3534
+
3535
+
if len("location") > 1000000 {
3536
+
return xerrors.Errorf("Value in field \"location\" was too long")
3537
+
}
3538
+
3539
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil {
3540
+
return err
3541
+
}
3542
+
if _, err := cw.WriteString(string("location")); err != nil {
3543
+
return err
3544
+
}
3545
+
3546
+
if t.Location == nil {
3547
+
if _, err := cw.Write(cbg.CborNull); err != nil {
3548
+
return err
3549
+
}
3550
+
} else {
3551
+
if len(*t.Location) > 1000000 {
3552
+
return xerrors.Errorf("Value in field t.Location was too long")
3553
+
}
3554
+
3555
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Location))); err != nil {
3556
+
return err
3557
+
}
3558
+
if _, err := cw.WriteString(string(*t.Location)); err != nil {
3559
+
return err
3560
+
}
3561
+
}
3562
+
}
3563
+
3564
+
// t.Description (string) (string)
3565
+
if t.Description != nil {
3566
+
3567
+
if len("description") > 1000000 {
3568
+
return xerrors.Errorf("Value in field \"description\" was too long")
3569
+
}
3570
+
3571
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil {
3572
+
return err
3573
+
}
3574
+
if _, err := cw.WriteString(string("description")); err != nil {
3575
+
return err
3576
+
}
3577
+
3578
+
if t.Description == nil {
3579
+
if _, err := cw.Write(cbg.CborNull); err != nil {
3580
+
return err
3581
+
}
3582
+
} else {
3583
+
if len(*t.Description) > 1000000 {
3584
+
return xerrors.Errorf("Value in field t.Description was too long")
3585
+
}
3586
+
3587
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil {
3588
+
return err
3589
+
}
3590
+
if _, err := cw.WriteString(string(*t.Description)); err != nil {
3591
+
return err
3592
+
}
3593
+
}
3594
+
}
3595
+
3596
+
// t.PinnedRepositories ([]string) (slice)
3597
+
if t.PinnedRepositories != nil {
3598
+
3599
+
if len("pinnedRepositories") > 1000000 {
3600
+
return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long")
3601
+
}
3602
+
3603
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil {
3604
+
return err
3605
+
}
3606
+
if _, err := cw.WriteString(string("pinnedRepositories")); err != nil {
3607
+
return err
3608
+
}
3609
+
3610
+
if len(t.PinnedRepositories) > 8192 {
3611
+
return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long")
3612
+
}
3613
+
3614
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil {
3615
+
return err
3616
+
}
3617
+
for _, v := range t.PinnedRepositories {
3618
+
if len(v) > 1000000 {
3619
+
return xerrors.Errorf("Value in field v was too long")
3620
+
}
3621
+
3622
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
3623
+
return err
3624
+
}
3625
+
if _, err := cw.WriteString(string(v)); err != nil {
3626
+
return err
3627
+
}
3628
+
3629
+
}
3630
+
}
3631
+
return nil
3632
+
}
3633
+
3634
+
func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) {
3635
+
*t = ActorProfile{}
3636
+
3637
+
cr := cbg.NewCborReader(r)
3638
+
3639
+
maj, extra, err := cr.ReadHeader()
3640
+
if err != nil {
3641
+
return err
3642
+
}
3643
+
defer func() {
3644
+
if err == io.EOF {
3645
+
err = io.ErrUnexpectedEOF
3646
+
}
3647
+
}()
3648
+
3649
+
if maj != cbg.MajMap {
3650
+
return fmt.Errorf("cbor input should be of type map")
3651
+
}
3652
+
3653
+
if extra > cbg.MaxLength {
3654
+
return fmt.Errorf("ActorProfile: map struct too large (%d)", extra)
3655
+
}
3656
+
3657
+
n := extra
3658
+
3659
+
nameBuf := make([]byte, 18)
3660
+
for i := uint64(0); i < n; i++ {
3661
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
3662
+
if err != nil {
3663
+
return err
3664
+
}
3665
+
3666
+
if !ok {
3667
+
// Field doesn't exist on this type, so ignore it
3668
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
3669
+
return err
3670
+
}
3671
+
continue
3672
+
}
3673
+
3674
+
switch string(nameBuf[:nameLen]) {
3675
+
// t.LexiconTypeID (string) (string)
3676
+
case "$type":
3677
+
3678
+
{
3679
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3680
+
if err != nil {
3681
+
return err
3682
+
}
3683
+
3684
+
t.LexiconTypeID = string(sval)
3685
+
}
3686
+
// t.Links ([]string) (slice)
3687
+
case "links":
3688
+
3689
+
maj, extra, err = cr.ReadHeader()
3690
+
if err != nil {
3691
+
return err
3692
+
}
3693
+
3694
+
if extra > 8192 {
3695
+
return fmt.Errorf("t.Links: array too large (%d)", extra)
3696
+
}
3697
+
3698
+
if maj != cbg.MajArray {
3699
+
return fmt.Errorf("expected cbor array")
3700
+
}
3701
+
3702
+
if extra > 0 {
3703
+
t.Links = make([]string, extra)
3704
+
}
3705
+
3706
+
for i := 0; i < int(extra); i++ {
3707
+
{
3708
+
var maj byte
3709
+
var extra uint64
3710
+
var err error
3711
+
_ = maj
3712
+
_ = extra
3713
+
_ = err
3714
+
3715
+
{
3716
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3717
+
if err != nil {
3718
+
return err
3719
+
}
3720
+
3721
+
t.Links[i] = string(sval)
3722
+
}
3723
+
3724
+
}
3725
+
}
3726
+
// t.Stats ([]string) (slice)
3727
+
case "stats":
3728
+
3729
+
maj, extra, err = cr.ReadHeader()
3730
+
if err != nil {
3731
+
return err
3732
+
}
3733
+
3734
+
if extra > 8192 {
3735
+
return fmt.Errorf("t.Stats: array too large (%d)", extra)
3736
+
}
3737
+
3738
+
if maj != cbg.MajArray {
3739
+
return fmt.Errorf("expected cbor array")
3740
+
}
3741
+
3742
+
if extra > 0 {
3743
+
t.Stats = make([]string, extra)
3744
+
}
3745
+
3746
+
for i := 0; i < int(extra); i++ {
3747
+
{
3748
+
var maj byte
3749
+
var extra uint64
3750
+
var err error
3751
+
_ = maj
3752
+
_ = extra
3753
+
_ = err
3754
+
3755
+
{
3756
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3757
+
if err != nil {
3758
+
return err
3759
+
}
3760
+
3761
+
t.Stats[i] = string(sval)
3762
+
}
3763
+
3764
+
}
3765
+
}
3766
+
// t.Bluesky (bool) (bool)
3767
+
case "bluesky":
3768
+
3769
+
maj, extra, err = cr.ReadHeader()
3770
+
if err != nil {
3771
+
return err
3772
+
}
3773
+
if maj != cbg.MajOther {
3774
+
return fmt.Errorf("booleans must be major type 7")
3775
+
}
3776
+
switch extra {
3777
+
case 20:
3778
+
t.Bluesky = false
3779
+
case 21:
3780
+
t.Bluesky = true
3781
+
default:
3782
+
return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra)
3783
+
}
3784
+
// t.Location (string) (string)
3785
+
case "location":
3786
+
3787
+
{
3788
+
b, err := cr.ReadByte()
3789
+
if err != nil {
3790
+
return err
3791
+
}
3792
+
if b != cbg.CborNull[0] {
3793
+
if err := cr.UnreadByte(); err != nil {
3794
+
return err
3795
+
}
3796
+
3797
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3798
+
if err != nil {
3799
+
return err
3800
+
}
3801
+
3802
+
t.Location = (*string)(&sval)
3803
+
}
3804
+
}
3805
+
// t.Description (string) (string)
3806
+
case "description":
3807
+
3808
+
{
3809
+
b, err := cr.ReadByte()
3810
+
if err != nil {
3811
+
return err
3812
+
}
3813
+
if b != cbg.CborNull[0] {
3814
+
if err := cr.UnreadByte(); err != nil {
3815
+
return err
3816
+
}
3817
+
3818
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3819
+
if err != nil {
3820
+
return err
3821
+
}
3822
+
3823
+
t.Description = (*string)(&sval)
3824
+
}
3825
+
}
3826
+
// t.PinnedRepositories ([]string) (slice)
3827
+
case "pinnedRepositories":
3828
+
3829
+
maj, extra, err = cr.ReadHeader()
3830
+
if err != nil {
3831
+
return err
3832
+
}
3833
+
3834
+
if extra > 8192 {
3835
+
return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra)
3836
+
}
3837
+
3838
+
if maj != cbg.MajArray {
3839
+
return fmt.Errorf("expected cbor array")
3840
+
}
3841
+
3842
+
if extra > 0 {
3843
+
t.PinnedRepositories = make([]string, extra)
3844
+
}
3845
+
3846
+
for i := 0; i < int(extra); i++ {
3847
+
{
3848
+
var maj byte
3849
+
var extra uint64
3850
+
var err error
3851
+
_ = maj
3852
+
_ = extra
3853
+
_ = err
3854
+
3855
+
{
3856
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
3857
+
if err != nil {
3858
+
return err
3859
+
}
3860
+
3861
+
t.PinnedRepositories[i] = string(sval)
3862
+
}
3863
+
3864
+
}
3865
+
}
3866
+
3867
+
default:
3868
+
// Field doesn't exist on this type, so ignore it
3869
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
3870
+
return err
3871
+
}
3872
+
}
3873
+
}
3874
+
3875
+
return nil
3876
+
}
-217
appview/auth/auth.go
-217
appview/auth/auth.go
···
1
-
package auth
2
-
3
-
import (
4
-
"context"
5
-
"fmt"
6
-
"net/http"
7
-
"time"
8
-
9
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
-
"github.com/bluesky-social/indigo/atproto/identity"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
"github.com/gorilla/sessions"
13
-
"tangled.sh/tangled.sh/core/appview"
14
-
)
15
-
16
-
type Auth struct {
17
-
Store *sessions.CookieStore
18
-
}
19
-
20
-
type AtSessionCreate struct {
21
-
comatproto.ServerCreateSession_Output
22
-
PDSEndpoint string
23
-
}
24
-
25
-
type AtSessionRefresh struct {
26
-
comatproto.ServerRefreshSession_Output
27
-
PDSEndpoint string
28
-
}
29
-
30
-
func Make(secret string) (*Auth, error) {
31
-
store := sessions.NewCookieStore([]byte(secret))
32
-
return &Auth{store}, nil
33
-
}
34
-
35
-
func (a *Auth) CreateInitialSession(ctx context.Context, resolved *identity.Identity, appPassword string) (*comatproto.ServerCreateSession_Output, error) {
36
-
37
-
pdsUrl := resolved.PDSEndpoint()
38
-
client := xrpc.Client{
39
-
Host: pdsUrl,
40
-
}
41
-
42
-
atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
43
-
Identifier: resolved.DID.String(),
44
-
Password: appPassword,
45
-
})
46
-
if err != nil {
47
-
return nil, fmt.Errorf("invalid app password")
48
-
}
49
-
50
-
return atSession, nil
51
-
}
52
-
53
-
// Sessionish is an interface that provides access to the common fields of both types.
54
-
type Sessionish interface {
55
-
GetAccessJwt() string
56
-
GetActive() *bool
57
-
GetDid() string
58
-
GetDidDoc() *interface{}
59
-
GetHandle() string
60
-
GetRefreshJwt() string
61
-
GetStatus() *string
62
-
}
63
-
64
-
// Create a wrapper type for ServerRefreshSession_Output
65
-
type RefreshSessionWrapper struct {
66
-
*comatproto.ServerRefreshSession_Output
67
-
}
68
-
69
-
func (s *RefreshSessionWrapper) GetAccessJwt() string {
70
-
return s.AccessJwt
71
-
}
72
-
73
-
func (s *RefreshSessionWrapper) GetActive() *bool {
74
-
return s.Active
75
-
}
76
-
77
-
func (s *RefreshSessionWrapper) GetDid() string {
78
-
return s.Did
79
-
}
80
-
81
-
func (s *RefreshSessionWrapper) GetDidDoc() *interface{} {
82
-
return s.DidDoc
83
-
}
84
-
85
-
func (s *RefreshSessionWrapper) GetHandle() string {
86
-
return s.Handle
87
-
}
88
-
89
-
func (s *RefreshSessionWrapper) GetRefreshJwt() string {
90
-
return s.RefreshJwt
91
-
}
92
-
93
-
func (s *RefreshSessionWrapper) GetStatus() *string {
94
-
return s.Status
95
-
}
96
-
97
-
// Create a wrapper type for ServerRefreshSession_Output
98
-
type CreateSessionWrapper struct {
99
-
*comatproto.ServerCreateSession_Output
100
-
}
101
-
102
-
func (s *CreateSessionWrapper) GetAccessJwt() string {
103
-
return s.AccessJwt
104
-
}
105
-
106
-
func (s *CreateSessionWrapper) GetActive() *bool {
107
-
return s.Active
108
-
}
109
-
110
-
func (s *CreateSessionWrapper) GetDid() string {
111
-
return s.Did
112
-
}
113
-
114
-
func (s *CreateSessionWrapper) GetDidDoc() *interface{} {
115
-
return s.DidDoc
116
-
}
117
-
118
-
func (s *CreateSessionWrapper) GetHandle() string {
119
-
return s.Handle
120
-
}
121
-
122
-
func (s *CreateSessionWrapper) GetRefreshJwt() string {
123
-
return s.RefreshJwt
124
-
}
125
-
126
-
func (s *CreateSessionWrapper) GetStatus() *string {
127
-
return s.Status
128
-
}
129
-
130
-
func (a *Auth) ClearSession(r *http.Request, w http.ResponseWriter) error {
131
-
clientSession, err := a.Store.Get(r, appview.SessionName)
132
-
if err != nil {
133
-
return fmt.Errorf("invalid session", err)
134
-
}
135
-
if clientSession.IsNew {
136
-
return fmt.Errorf("invalid session")
137
-
}
138
-
clientSession.Options.MaxAge = -1
139
-
return clientSession.Save(r, w)
140
-
}
141
-
142
-
func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionish Sessionish, pdsEndpoint string) error {
143
-
clientSession, _ := a.Store.Get(r, appview.SessionName)
144
-
clientSession.Values[appview.SessionHandle] = atSessionish.GetHandle()
145
-
clientSession.Values[appview.SessionDid] = atSessionish.GetDid()
146
-
clientSession.Values[appview.SessionPds] = pdsEndpoint
147
-
clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt()
148
-
clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt()
149
-
clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339)
150
-
clientSession.Values[appview.SessionAuthenticated] = true
151
-
return clientSession.Save(r, w)
152
-
}
153
-
154
-
func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
155
-
clientSession, err := a.Store.Get(r, "appview-session")
156
-
if err != nil || clientSession.IsNew {
157
-
return nil, err
158
-
}
159
-
160
-
did := clientSession.Values["did"].(string)
161
-
pdsUrl := clientSession.Values["pds"].(string)
162
-
accessJwt := clientSession.Values["accessJwt"].(string)
163
-
refreshJwt := clientSession.Values["refreshJwt"].(string)
164
-
165
-
client := &xrpc.Client{
166
-
Host: pdsUrl,
167
-
Auth: &xrpc.AuthInfo{
168
-
AccessJwt: accessJwt,
169
-
RefreshJwt: refreshJwt,
170
-
Did: did,
171
-
},
172
-
}
173
-
174
-
return client, nil
175
-
}
176
-
177
-
func (a *Auth) GetSession(r *http.Request) (*sessions.Session, error) {
178
-
return a.Store.Get(r, appview.SessionName)
179
-
}
180
-
181
-
func (a *Auth) GetDid(r *http.Request) string {
182
-
clientSession, err := a.Store.Get(r, appview.SessionName)
183
-
if err != nil || clientSession.IsNew {
184
-
return ""
185
-
}
186
-
187
-
return clientSession.Values[appview.SessionDid].(string)
188
-
}
189
-
190
-
func (a *Auth) GetHandle(r *http.Request) string {
191
-
clientSession, err := a.Store.Get(r, appview.SessionName)
192
-
if err != nil || clientSession.IsNew {
193
-
return ""
194
-
}
195
-
196
-
return clientSession.Values[appview.SessionHandle].(string)
197
-
}
198
-
199
-
type User struct {
200
-
Handle string
201
-
Did string
202
-
Pds string
203
-
}
204
-
205
-
func (a *Auth) GetUser(r *http.Request) *User {
206
-
clientSession, err := a.Store.Get(r, appview.SessionName)
207
-
208
-
if err != nil || clientSession.IsNew {
209
-
return nil
210
-
}
211
-
212
-
return &User{
213
-
Handle: clientSession.Values[appview.SessionHandle].(string),
214
-
Did: clientSession.Values[appview.SessionDid].(string),
215
-
Pds: clientSession.Values[appview.SessionPds].(string),
216
-
}
217
-
}
···
+36
-10
appview/config.go
+36
-10
appview/config.go
···
6
"github.com/sethvargo/go-envconfig"
7
)
8
9
type Config struct {
10
-
CookieSecret string `env:"TANGLED_COOKIE_SECRET, default=00000000000000000000000000000000"`
11
-
DbPath string `env:"TANGLED_DB_PATH, default=appview.db"`
12
-
ListenAddr string `env:"TANGLED_LISTEN_ADDR, default=0.0.0.0:3000"`
13
-
Dev bool `env:"TANGLED_DEV, default=false"`
14
-
JetstreamEndpoint string `env:"TANGLED_JETSTREAM_ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
15
-
ResendApiKey string `env:"TANGLED_RESEND_API_KEY"`
16
-
CamoHost string `env:"TANGLED_CAMO_HOST, default=https://camo.tangled.sh"`
17
-
CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"`
18
-
AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"`
19
-
AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"`
20
}
21
22
func LoadConfig(ctx context.Context) (*Config, error) {
···
6
"github.com/sethvargo/go-envconfig"
7
)
8
9
+
type CoreConfig struct {
10
+
CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"`
11
+
DbPath string `env:"DB_PATH, default=appview.db"`
12
+
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"`
13
+
AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"`
14
+
Dev bool `env:"DEV, default=false"`
15
+
}
16
+
17
+
type OAuthConfig struct {
18
+
Jwks string `env:"JWKS"`
19
+
}
20
+
21
+
type JetstreamConfig struct {
22
+
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
23
+
}
24
+
25
+
type ResendConfig struct {
26
+
ApiKey string `env:"API_KEY"`
27
+
}
28
+
29
+
type CamoConfig struct {
30
+
Host string `env:"HOST, default=https://camo.tangled.sh"`
31
+
SharedSecret string `env:"SHARED_SECRET"`
32
+
}
33
+
34
+
type AvatarConfig struct {
35
+
Host string `env:"HOST, default=https://avatar.tangled.sh"`
36
+
SharedSecret string `env:"SHARED_SECRET"`
37
+
}
38
+
39
type Config struct {
40
+
Core CoreConfig `env:",prefix=TANGLED_"`
41
+
Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"`
42
+
Resend ResendConfig `env:",prefix=TANGLED_RESEND_"`
43
+
Camo CamoConfig `env:",prefix=TANGLED_CAMO_"`
44
+
Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"`
45
+
OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"`
46
}
47
48
func LoadConfig(ctx context.Context) (*Config, error) {
+3
appview/consts.go
+3
appview/consts.go
-16
appview/db/artifact.go
-16
appview/db/artifact.go
···
57
return err
58
}
59
60
-
type filter struct {
61
-
key string
62
-
arg any
63
-
}
64
-
65
-
func Filter(key string, arg any) filter {
66
-
return filter{
67
-
key: key,
68
-
arg: arg,
69
-
}
70
-
}
71
-
72
-
func (f filter) Condition() string {
73
-
return fmt.Sprintf("%s = ?", f.key)
74
-
}
75
-
76
func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) {
77
var artifacts []Artifact
78
+99
appview/db/db.go
+99
appview/db/db.go
···
3
import (
4
"context"
5
"database/sql"
6
"log"
7
8
_ "github.com/mattn/go-sqlite3"
···
231
foreign key (repo_at) references repos(at_uri) on delete cascade
232
);
233
234
create table if not exists migrations (
235
id integer primary key autoincrement,
236
name text unique
···
348
349
return nil
350
}
···
3
import (
4
"context"
5
"database/sql"
6
+
"fmt"
7
"log"
8
9
_ "github.com/mattn/go-sqlite3"
···
232
foreign key (repo_at) references repos(at_uri) on delete cascade
233
);
234
235
+
create table if not exists profile (
236
+
-- id
237
+
id integer primary key autoincrement,
238
+
did text not null,
239
+
240
+
-- data
241
+
description text not null,
242
+
include_bluesky integer not null default 0,
243
+
location text,
244
+
245
+
-- constraints
246
+
unique(did)
247
+
);
248
+
create table if not exists profile_links (
249
+
-- id
250
+
id integer primary key autoincrement,
251
+
did text not null,
252
+
253
+
-- data
254
+
link text not null,
255
+
256
+
-- constraints
257
+
foreign key (did) references profile(did) on delete cascade
258
+
);
259
+
create table if not exists profile_stats (
260
+
-- id
261
+
id integer primary key autoincrement,
262
+
did text not null,
263
+
264
+
-- data
265
+
kind text not null check (kind in (
266
+
"merged-pull-request-count",
267
+
"closed-pull-request-count",
268
+
"open-pull-request-count",
269
+
"open-issue-count",
270
+
"closed-issue-count",
271
+
"repository-count"
272
+
)),
273
+
274
+
-- constraints
275
+
foreign key (did) references profile(did) on delete cascade
276
+
);
277
+
create table if not exists profile_pinned_repositories (
278
+
-- id
279
+
id integer primary key autoincrement,
280
+
did text not null,
281
+
282
+
-- data
283
+
at_uri text not null,
284
+
285
+
-- constraints
286
+
unique(did, at_uri),
287
+
foreign key (did) references profile(did) on delete cascade,
288
+
foreign key (at_uri) references repos(at_uri) on delete cascade
289
+
);
290
+
291
+
create table if not exists oauth_requests (
292
+
id integer primary key autoincrement,
293
+
auth_server_iss text not null,
294
+
state text not null,
295
+
did text not null,
296
+
handle text not null,
297
+
pds_url text not null,
298
+
pkce_verifier text not null,
299
+
dpop_auth_server_nonce text not null,
300
+
dpop_private_jwk text not null
301
+
);
302
+
303
+
create table if not exists oauth_sessions (
304
+
id integer primary key autoincrement,
305
+
did text not null,
306
+
handle text not null,
307
+
pds_url text not null,
308
+
auth_server_iss text not null,
309
+
access_jwt text not null,
310
+
refresh_jwt text not null,
311
+
dpop_pds_nonce text,
312
+
dpop_auth_server_nonce text not null,
313
+
dpop_private_jwk text not null,
314
+
expiry text not null
315
+
);
316
+
317
create table if not exists migrations (
318
id integer primary key autoincrement,
319
name text unique
···
431
432
return nil
433
}
434
+
435
+
type filter struct {
436
+
key string
437
+
arg any
438
+
}
439
+
440
+
func Filter(key string, arg any) filter {
441
+
return filter{
442
+
key: key,
443
+
arg: arg,
444
+
}
445
+
}
446
+
447
+
func (f filter) Condition() string {
448
+
return fmt.Sprintf("%s = ?", f.key)
449
+
}
+173
appview/db/oauth.go
+173
appview/db/oauth.go
···
···
1
+
package db
2
+
3
+
type OAuthRequest struct {
4
+
ID uint
5
+
AuthserverIss string
6
+
Handle string
7
+
State string
8
+
Did string
9
+
PdsUrl string
10
+
PkceVerifier string
11
+
DpopAuthserverNonce string
12
+
DpopPrivateJwk string
13
+
}
14
+
15
+
func SaveOAuthRequest(e Execer, oauthRequest OAuthRequest) error {
16
+
_, err := e.Exec(`
17
+
insert into oauth_requests (
18
+
auth_server_iss,
19
+
state,
20
+
handle,
21
+
did,
22
+
pds_url,
23
+
pkce_verifier,
24
+
dpop_auth_server_nonce,
25
+
dpop_private_jwk
26
+
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
27
+
oauthRequest.AuthserverIss,
28
+
oauthRequest.State,
29
+
oauthRequest.Handle,
30
+
oauthRequest.Did,
31
+
oauthRequest.PdsUrl,
32
+
oauthRequest.PkceVerifier,
33
+
oauthRequest.DpopAuthserverNonce,
34
+
oauthRequest.DpopPrivateJwk,
35
+
)
36
+
return err
37
+
}
38
+
39
+
func GetOAuthRequestByState(e Execer, state string) (OAuthRequest, error) {
40
+
var req OAuthRequest
41
+
err := e.QueryRow(`
42
+
select
43
+
id,
44
+
auth_server_iss,
45
+
handle,
46
+
state,
47
+
did,
48
+
pds_url,
49
+
pkce_verifier,
50
+
dpop_auth_server_nonce,
51
+
dpop_private_jwk
52
+
from oauth_requests
53
+
where state = ?`, state).Scan(
54
+
&req.ID,
55
+
&req.AuthserverIss,
56
+
&req.Handle,
57
+
&req.State,
58
+
&req.Did,
59
+
&req.PdsUrl,
60
+
&req.PkceVerifier,
61
+
&req.DpopAuthserverNonce,
62
+
&req.DpopPrivateJwk,
63
+
)
64
+
return req, err
65
+
}
66
+
67
+
func DeleteOAuthRequestByState(e Execer, state string) error {
68
+
_, err := e.Exec(`
69
+
delete from oauth_requests
70
+
where state = ?`, state)
71
+
return err
72
+
}
73
+
74
+
type OAuthSession struct {
75
+
ID uint
76
+
Handle string
77
+
Did string
78
+
PdsUrl string
79
+
AccessJwt string
80
+
RefreshJwt string
81
+
AuthServerIss string
82
+
DpopPdsNonce string
83
+
DpopAuthserverNonce string
84
+
DpopPrivateJwk string
85
+
Expiry string
86
+
}
87
+
88
+
func SaveOAuthSession(e Execer, session OAuthSession) error {
89
+
_, err := e.Exec(`
90
+
insert into oauth_sessions (
91
+
did,
92
+
handle,
93
+
pds_url,
94
+
access_jwt,
95
+
refresh_jwt,
96
+
auth_server_iss,
97
+
dpop_auth_server_nonce,
98
+
dpop_private_jwk,
99
+
expiry
100
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
101
+
session.Did,
102
+
session.Handle,
103
+
session.PdsUrl,
104
+
session.AccessJwt,
105
+
session.RefreshJwt,
106
+
session.AuthServerIss,
107
+
session.DpopAuthserverNonce,
108
+
session.DpopPrivateJwk,
109
+
session.Expiry,
110
+
)
111
+
return err
112
+
}
113
+
114
+
func RefreshOAuthSession(e Execer, did string, accessJwt, refreshJwt, expiry string) error {
115
+
_, err := e.Exec(`
116
+
update oauth_sessions
117
+
set access_jwt = ?, refresh_jwt = ?, expiry = ?
118
+
where did = ?`,
119
+
accessJwt,
120
+
refreshJwt,
121
+
expiry,
122
+
did,
123
+
)
124
+
return err
125
+
}
126
+
127
+
func GetOAuthSessionByDid(e Execer, did string) (*OAuthSession, error) {
128
+
var session OAuthSession
129
+
err := e.QueryRow(`
130
+
select
131
+
id,
132
+
did,
133
+
handle,
134
+
pds_url,
135
+
access_jwt,
136
+
refresh_jwt,
137
+
auth_server_iss,
138
+
dpop_auth_server_nonce,
139
+
dpop_private_jwk,
140
+
expiry
141
+
from oauth_sessions
142
+
where did = ?`, did).Scan(
143
+
&session.ID,
144
+
&session.Did,
145
+
&session.Handle,
146
+
&session.PdsUrl,
147
+
&session.AccessJwt,
148
+
&session.RefreshJwt,
149
+
&session.AuthServerIss,
150
+
&session.DpopAuthserverNonce,
151
+
&session.DpopPrivateJwk,
152
+
&session.Expiry,
153
+
)
154
+
return &session, err
155
+
}
156
+
157
+
func DeleteOAuthSessionByDid(e Execer, did string) error {
158
+
_, err := e.Exec(`
159
+
delete from oauth_sessions
160
+
where did = ?`, did)
161
+
return err
162
+
}
163
+
164
+
func UpdateDpopPdsNonce(e Execer, did string, dpopPdsNonce string) error {
165
+
_, err := e.Exec(`
166
+
update oauth_sessions
167
+
set dpop_pds_nonce = ?
168
+
where did = ?`,
169
+
dpopPdsNonce,
170
+
did,
171
+
)
172
+
return err
173
+
}
+366
appview/db/profile.go
+366
appview/db/profile.go
···
1
package db
2
3
import (
4
+
"database/sql"
5
"fmt"
6
+
"log"
7
+
"net/url"
8
+
"slices"
9
+
"strings"
10
"time"
11
+
12
+
"github.com/bluesky-social/indigo/atproto/syntax"
13
+
"tangled.sh/tangled.sh/core/api/tangled"
14
)
15
16
type RepoEvent struct {
···
170
171
return &timeline, nil
172
}
173
+
174
+
type Profile struct {
175
+
// ids
176
+
ID int
177
+
Did string
178
+
179
+
// data
180
+
Description string
181
+
IncludeBluesky bool
182
+
Location string
183
+
Links [5]string
184
+
Stats [2]VanityStat
185
+
PinnedRepos [6]syntax.ATURI
186
+
}
187
+
188
+
func (p Profile) IsLinksEmpty() bool {
189
+
for _, l := range p.Links {
190
+
if l != "" {
191
+
return false
192
+
}
193
+
}
194
+
return true
195
+
}
196
+
197
+
func (p Profile) IsStatsEmpty() bool {
198
+
for _, s := range p.Stats {
199
+
if s.Kind != "" {
200
+
return false
201
+
}
202
+
}
203
+
return true
204
+
}
205
+
206
+
func (p Profile) IsPinnedReposEmpty() bool {
207
+
for _, r := range p.PinnedRepos {
208
+
if r != "" {
209
+
return false
210
+
}
211
+
}
212
+
return true
213
+
}
214
+
215
+
type VanityStatKind string
216
+
217
+
const (
218
+
VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count"
219
+
VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count"
220
+
VanityStatOpenPRCount VanityStatKind = "open-pull-request-count"
221
+
VanityStatOpenIssueCount VanityStatKind = "open-issue-count"
222
+
VanityStatClosedIssueCount VanityStatKind = "closed-issue-count"
223
+
VanityStatRepositoryCount VanityStatKind = "repository-count"
224
+
)
225
+
226
+
func (v VanityStatKind) String() string {
227
+
switch v {
228
+
case VanityStatMergedPRCount:
229
+
return "Merged PRs"
230
+
case VanityStatClosedPRCount:
231
+
return "Closed PRs"
232
+
case VanityStatOpenPRCount:
233
+
return "Open PRs"
234
+
case VanityStatOpenIssueCount:
235
+
return "Open Issues"
236
+
case VanityStatClosedIssueCount:
237
+
return "Closed Issues"
238
+
case VanityStatRepositoryCount:
239
+
return "Repositories"
240
+
}
241
+
return ""
242
+
}
243
+
244
+
type VanityStat struct {
245
+
Kind VanityStatKind
246
+
Value uint64
247
+
}
248
+
249
+
func (p *Profile) ProfileAt() syntax.ATURI {
250
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self"))
251
+
}
252
+
253
+
func UpsertProfile(tx *sql.Tx, profile *Profile) error {
254
+
defer tx.Rollback()
255
+
256
+
// update links
257
+
_, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did)
258
+
if err != nil {
259
+
return err
260
+
}
261
+
// update vanity stats
262
+
_, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did)
263
+
if err != nil {
264
+
return err
265
+
}
266
+
267
+
// update pinned repos
268
+
_, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did)
269
+
if err != nil {
270
+
return err
271
+
}
272
+
273
+
includeBskyValue := 0
274
+
if profile.IncludeBluesky {
275
+
includeBskyValue = 1
276
+
}
277
+
278
+
_, err = tx.Exec(
279
+
`insert or replace into profile (
280
+
did,
281
+
description,
282
+
include_bluesky,
283
+
location
284
+
)
285
+
values (?, ?, ?, ?)`,
286
+
profile.Did,
287
+
profile.Description,
288
+
includeBskyValue,
289
+
profile.Location,
290
+
)
291
+
292
+
if err != nil {
293
+
log.Println("profile", "err", err)
294
+
return err
295
+
}
296
+
297
+
for _, link := range profile.Links {
298
+
if link == "" {
299
+
continue
300
+
}
301
+
302
+
_, err := tx.Exec(
303
+
`insert into profile_links (did, link) values (?, ?)`,
304
+
profile.Did,
305
+
link,
306
+
)
307
+
308
+
if err != nil {
309
+
log.Println("profile_links", "err", err)
310
+
return err
311
+
}
312
+
}
313
+
314
+
for _, v := range profile.Stats {
315
+
if v.Kind == "" {
316
+
continue
317
+
}
318
+
319
+
_, err := tx.Exec(
320
+
`insert into profile_stats (did, kind) values (?, ?)`,
321
+
profile.Did,
322
+
v.Kind,
323
+
)
324
+
325
+
if err != nil {
326
+
log.Println("profile_stats", "err", err)
327
+
return err
328
+
}
329
+
}
330
+
331
+
for _, pin := range profile.PinnedRepos {
332
+
if pin == "" {
333
+
continue
334
+
}
335
+
336
+
_, err := tx.Exec(
337
+
`insert into profile_pinned_repositories (did, at_uri) values (?, ?)`,
338
+
profile.Did,
339
+
pin,
340
+
)
341
+
342
+
if err != nil {
343
+
log.Println("profile_pinned_repositories", "err", err)
344
+
return err
345
+
}
346
+
}
347
+
348
+
return tx.Commit()
349
+
}
350
+
351
+
func GetProfile(e Execer, did string) (*Profile, error) {
352
+
var profile Profile
353
+
profile.Did = did
354
+
355
+
includeBluesky := 0
356
+
err := e.QueryRow(
357
+
`select description, include_bluesky, location from profile where did = ?`,
358
+
did,
359
+
).Scan(&profile.Description, &includeBluesky, &profile.Location)
360
+
if err == sql.ErrNoRows {
361
+
profile := Profile{}
362
+
profile.Did = did
363
+
return &profile, nil
364
+
}
365
+
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
if includeBluesky != 0 {
371
+
profile.IncludeBluesky = true
372
+
}
373
+
374
+
rows, err := e.Query(`select link from profile_links where did = ?`, did)
375
+
if err != nil {
376
+
return nil, err
377
+
}
378
+
defer rows.Close()
379
+
i := 0
380
+
for rows.Next() {
381
+
if err := rows.Scan(&profile.Links[i]); err != nil {
382
+
return nil, err
383
+
}
384
+
i++
385
+
}
386
+
387
+
rows, err = e.Query(`select kind from profile_stats where did = ?`, did)
388
+
if err != nil {
389
+
return nil, err
390
+
}
391
+
defer rows.Close()
392
+
i = 0
393
+
for rows.Next() {
394
+
if err := rows.Scan(&profile.Stats[i].Kind); err != nil {
395
+
return nil, err
396
+
}
397
+
value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind)
398
+
if err != nil {
399
+
return nil, err
400
+
}
401
+
profile.Stats[i].Value = value
402
+
i++
403
+
}
404
+
405
+
rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did)
406
+
if err != nil {
407
+
return nil, err
408
+
}
409
+
defer rows.Close()
410
+
i = 0
411
+
for rows.Next() {
412
+
if err := rows.Scan(&profile.PinnedRepos[i]); err != nil {
413
+
return nil, err
414
+
}
415
+
i++
416
+
}
417
+
418
+
return &profile, nil
419
+
}
420
+
421
+
func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) {
422
+
query := ""
423
+
var args []any
424
+
switch stat {
425
+
case VanityStatMergedPRCount:
426
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
427
+
args = append(args, did, PullMerged)
428
+
case VanityStatClosedPRCount:
429
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
430
+
args = append(args, did, PullClosed)
431
+
case VanityStatOpenPRCount:
432
+
query = `select count(id) from pulls where owner_did = ? and state = ?`
433
+
args = append(args, did, PullOpen)
434
+
case VanityStatOpenIssueCount:
435
+
query = `select count(id) from issues where owner_did = ? and open = 1`
436
+
args = append(args, did)
437
+
case VanityStatClosedIssueCount:
438
+
query = `select count(id) from issues where owner_did = ? and open = 0`
439
+
args = append(args, did)
440
+
case VanityStatRepositoryCount:
441
+
query = `select count(id) from repos where did = ?`
442
+
args = append(args, did)
443
+
}
444
+
445
+
var result uint64
446
+
err := e.QueryRow(query, args...).Scan(&result)
447
+
if err != nil {
448
+
return 0, err
449
+
}
450
+
451
+
return result, nil
452
+
}
453
+
454
+
func ValidateProfile(e Execer, profile *Profile) error {
455
+
// ensure description is not too long
456
+
if len(profile.Description) > 256 {
457
+
return fmt.Errorf("Entered bio is too long.")
458
+
}
459
+
460
+
// ensure description is not too long
461
+
if len(profile.Location) > 40 {
462
+
return fmt.Errorf("Entered location is too long.")
463
+
}
464
+
465
+
// ensure links are in order
466
+
err := validateLinks(profile)
467
+
if err != nil {
468
+
return err
469
+
}
470
+
471
+
// ensure all pinned repos are either own repos or collaborating repos
472
+
repos, err := GetAllReposByDid(e, profile.Did)
473
+
if err != nil {
474
+
log.Printf("getting repos for %s: %s", profile.Did, err)
475
+
}
476
+
477
+
collaboratingRepos, err := CollaboratingIn(e, profile.Did)
478
+
if err != nil {
479
+
log.Printf("getting collaborating repos for %s: %s", profile.Did, err)
480
+
}
481
+
482
+
var validRepos []syntax.ATURI
483
+
for _, r := range repos {
484
+
validRepos = append(validRepos, r.RepoAt())
485
+
}
486
+
for _, r := range collaboratingRepos {
487
+
validRepos = append(validRepos, r.RepoAt())
488
+
}
489
+
490
+
for _, pinned := range profile.PinnedRepos {
491
+
if pinned == "" {
492
+
continue
493
+
}
494
+
if !slices.Contains(validRepos, pinned) {
495
+
return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned)
496
+
}
497
+
}
498
+
499
+
return nil
500
+
}
501
+
502
+
func validateLinks(profile *Profile) error {
503
+
for i, link := range profile.Links {
504
+
if link == "" {
505
+
continue
506
+
}
507
+
508
+
parsedURL, err := url.Parse(link)
509
+
if err != nil {
510
+
return fmt.Errorf("Invalid URL '%s': %v\n", link, err)
511
+
}
512
+
513
+
if parsedURL.Scheme == "" {
514
+
if strings.HasPrefix(link, "//") {
515
+
profile.Links[i] = "https:" + link
516
+
} else {
517
+
profile.Links[i] = "https://" + link
518
+
}
519
+
continue
520
+
} else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
521
+
return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme)
522
+
}
523
+
524
+
// catch relative paths
525
+
if parsedURL.Host == "" {
526
+
return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link)
527
+
}
528
+
}
529
+
return nil
530
+
}
+1
-11
appview/db/pulls.go
+1
-11
appview/db/pulls.go
···
235
}
236
237
func NewPull(tx *sql.Tx, pull *Pull) error {
238
-
defer tx.Rollback()
239
-
240
_, err := tx.Exec(`
241
insert or ignore into repo_pull_seqs (repo_at, next_pull_id)
242
values (?, 1)
···
291
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
292
values (?, ?, ?, ?, ?)
293
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
294
-
if err != nil {
295
-
return err
296
-
}
297
-
298
-
if err := tx.Commit(); err != nil {
299
-
return err
300
-
}
301
-
302
-
return nil
303
}
304
305
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
···
235
}
236
237
func NewPull(tx *sql.Tx, pull *Pull) error {
238
_, err := tx.Exec(`
239
insert or ignore into repo_pull_seqs (repo_at, next_pull_id)
240
values (?, 1)
···
289
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
290
values (?, ?, ?, ?, ?)
291
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
292
+
return err
293
}
294
295
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+12
appview/db/repos.go
+12
appview/db/repos.go
···
2
3
import (
4
"database/sql"
5
+
"fmt"
6
"time"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
securejoin "github.com/cyphar/filepath-securejoin"
10
+
"tangled.sh/tangled.sh/core/api/tangled"
11
)
12
13
type Repo struct {
···
24
25
// optional
26
Source string
27
+
}
28
+
29
+
func (r Repo) RepoAt() syntax.ATURI {
30
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
31
+
}
32
+
33
+
func (r Repo) DidSlashRepo() string {
34
+
p, _ := securejoin.SecureJoin(r.Did, r.Name)
35
+
return p
36
}
37
38
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
+1
-1
appview/db/star.go
+1
-1
appview/db/star.go
+104
-5
appview/ingester.go
+104
-5
appview/ingester.go
···
13
"github.com/ipfs/go-cid"
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview/db"
16
)
17
18
type Ingester func(ctx context.Context, e *models.Event) error
19
20
-
func Ingest(d db.DbWrapper) Ingester {
21
return func(ctx context.Context, e *models.Event) error {
22
var err error
23
defer func() {
···
40
case tangled.PublicKeyNSID:
41
ingestPublicKey(&d, e)
42
case tangled.RepoArtifactNSID:
43
-
ingestArtifact(&d, e)
44
}
45
46
return err
···
137
return nil
138
}
139
140
-
func ingestArtifact(d *db.DbWrapper, e *models.Event) error {
141
did := e.Did
142
var err error
143
144
switch e.Commit.Operation {
145
case models.CommitOperationCreate, models.CommitOperationUpdate:
146
-
log.Println("processing add of artifact")
147
raw := json.RawMessage(e.Commit.Record)
148
record := tangled.RepoArtifact{}
149
err = json.Unmarshal(raw, &record)
···
157
return err
158
}
159
160
createdAt, err := time.Parse(time.RFC3339, record.CreatedAt)
161
if err != nil {
162
createdAt = time.Now()
···
176
177
err = db.AddArtifact(d, artifact)
178
case models.CommitOperationDelete:
179
-
log.Println("processing delete of artifact")
180
err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey))
181
}
182
···
186
187
return nil
188
}
···
13
"github.com/ipfs/go-cid"
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/rbac"
17
)
18
19
type Ingester func(ctx context.Context, e *models.Event) error
20
21
+
func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester {
22
return func(ctx context.Context, e *models.Event) error {
23
var err error
24
defer func() {
···
41
case tangled.PublicKeyNSID:
42
ingestPublicKey(&d, e)
43
case tangled.RepoArtifactNSID:
44
+
ingestArtifact(&d, e, enforcer)
45
+
case tangled.ActorProfileNSID:
46
+
ingestProfile(&d, e)
47
}
48
49
return err
···
140
return nil
141
}
142
143
+
func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error {
144
did := e.Did
145
var err error
146
147
switch e.Commit.Operation {
148
case models.CommitOperationCreate, models.CommitOperationUpdate:
149
raw := json.RawMessage(e.Commit.Record)
150
record := tangled.RepoArtifact{}
151
err = json.Unmarshal(raw, &record)
···
159
return err
160
}
161
162
+
repo, err := db.GetRepoByAtUri(d, repoAt.String())
163
+
if err != nil {
164
+
return err
165
+
}
166
+
167
+
ok, err := enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push")
168
+
if err != nil || !ok {
169
+
return err
170
+
}
171
+
172
createdAt, err := time.Parse(time.RFC3339, record.CreatedAt)
173
if err != nil {
174
createdAt = time.Now()
···
188
189
err = db.AddArtifact(d, artifact)
190
case models.CommitOperationDelete:
191
err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey))
192
}
193
···
197
198
return nil
199
}
200
+
201
+
func ingestProfile(d *db.DbWrapper, e *models.Event) error {
202
+
did := e.Did
203
+
var err error
204
+
205
+
if e.Commit.RKey != "self" {
206
+
return fmt.Errorf("ingestProfile only ingests `self` record")
207
+
}
208
+
209
+
switch e.Commit.Operation {
210
+
case models.CommitOperationCreate, models.CommitOperationUpdate:
211
+
raw := json.RawMessage(e.Commit.Record)
212
+
record := tangled.ActorProfile{}
213
+
err = json.Unmarshal(raw, &record)
214
+
if err != nil {
215
+
log.Printf("invalid record: %s", err)
216
+
return err
217
+
}
218
+
219
+
description := ""
220
+
if record.Description != nil {
221
+
description = *record.Description
222
+
}
223
+
224
+
includeBluesky := record.Bluesky
225
+
226
+
location := ""
227
+
if record.Location != nil {
228
+
location = *record.Location
229
+
}
230
+
231
+
var links [5]string
232
+
for i, l := range record.Links {
233
+
if i < 5 {
234
+
links[i] = l
235
+
}
236
+
}
237
+
238
+
var stats [2]db.VanityStat
239
+
for i, s := range record.Stats {
240
+
if i < 2 {
241
+
stats[i].Kind = db.VanityStatKind(s)
242
+
}
243
+
}
244
+
245
+
var pinned [6]syntax.ATURI
246
+
for i, r := range record.PinnedRepositories {
247
+
if i < 6 {
248
+
pinned[i] = syntax.ATURI(r)
249
+
}
250
+
}
251
+
252
+
profile := db.Profile{
253
+
Did: did,
254
+
Description: description,
255
+
IncludeBluesky: includeBluesky,
256
+
Location: location,
257
+
Links: links,
258
+
Stats: stats,
259
+
PinnedRepos: pinned,
260
+
}
261
+
262
+
ddb, ok := d.Execer.(*db.DB)
263
+
if !ok {
264
+
return fmt.Errorf("failed to index profile record, invalid db cast")
265
+
}
266
+
267
+
tx, err := ddb.Begin()
268
+
if err != nil {
269
+
return fmt.Errorf("failed to start transaction")
270
+
}
271
+
272
+
err = db.ValidateProfile(tx, &profile)
273
+
if err != nil {
274
+
return fmt.Errorf("invalid profile record")
275
+
}
276
+
277
+
err = db.UpsertProfile(tx, &profile)
278
+
case models.CommitOperationDelete:
279
+
err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey))
280
+
}
281
+
282
+
if err != nil {
283
+
return fmt.Errorf("failed to %s profile record: %w", e.Commit.Operation, err)
284
+
}
285
+
286
+
return nil
287
+
}
+489
appview/knotclient/signer.go
+489
appview/knotclient/signer.go
···
···
1
+
package knotclient
2
+
3
+
import (
4
+
"bytes"
5
+
"crypto/hmac"
6
+
"crypto/sha256"
7
+
"encoding/hex"
8
+
"encoding/json"
9
+
"fmt"
10
+
"io"
11
+
"log"
12
+
"net/http"
13
+
"net/url"
14
+
"strconv"
15
+
"time"
16
+
17
+
"tangled.sh/tangled.sh/core/types"
18
+
)
19
+
20
+
type SignerTransport struct {
21
+
Secret string
22
+
}
23
+
24
+
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
25
+
timestamp := time.Now().Format(time.RFC3339)
26
+
mac := hmac.New(sha256.New, []byte(s.Secret))
27
+
message := req.Method + req.URL.Path + timestamp
28
+
mac.Write([]byte(message))
29
+
signature := hex.EncodeToString(mac.Sum(nil))
30
+
req.Header.Set("X-Signature", signature)
31
+
req.Header.Set("X-Timestamp", timestamp)
32
+
return http.DefaultTransport.RoundTrip(req)
33
+
}
34
+
35
+
type SignedClient struct {
36
+
Secret string
37
+
Url *url.URL
38
+
client *http.Client
39
+
}
40
+
41
+
func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
42
+
client := &http.Client{
43
+
Timeout: 5 * time.Second,
44
+
Transport: SignerTransport{
45
+
Secret: secret,
46
+
},
47
+
}
48
+
49
+
scheme := "https"
50
+
if dev {
51
+
scheme = "http"
52
+
}
53
+
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
54
+
if err != nil {
55
+
return nil, err
56
+
}
57
+
58
+
signedClient := &SignedClient{
59
+
Secret: secret,
60
+
client: client,
61
+
Url: url,
62
+
}
63
+
64
+
return signedClient, nil
65
+
}
66
+
67
+
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
68
+
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
69
+
}
70
+
71
+
func (s *SignedClient) Init(did string) (*http.Response, error) {
72
+
const (
73
+
Method = "POST"
74
+
Endpoint = "/init"
75
+
)
76
+
77
+
body, _ := json.Marshal(map[string]any{
78
+
"did": did,
79
+
})
80
+
81
+
req, err := s.newRequest(Method, Endpoint, body)
82
+
if err != nil {
83
+
return nil, err
84
+
}
85
+
86
+
return s.client.Do(req)
87
+
}
88
+
89
+
func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
90
+
const (
91
+
Method = "PUT"
92
+
Endpoint = "/repo/new"
93
+
)
94
+
95
+
body, _ := json.Marshal(map[string]any{
96
+
"did": did,
97
+
"name": repoName,
98
+
"default_branch": defaultBranch,
99
+
})
100
+
101
+
req, err := s.newRequest(Method, Endpoint, body)
102
+
if err != nil {
103
+
return nil, err
104
+
}
105
+
106
+
return s.client.Do(req)
107
+
}
108
+
109
+
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
110
+
const (
111
+
Method = "POST"
112
+
Endpoint = "/repo/fork"
113
+
)
114
+
115
+
body, _ := json.Marshal(map[string]any{
116
+
"did": ownerDid,
117
+
"source": source,
118
+
"name": name,
119
+
})
120
+
121
+
req, err := s.newRequest(Method, Endpoint, body)
122
+
if err != nil {
123
+
return nil, err
124
+
}
125
+
126
+
return s.client.Do(req)
127
+
}
128
+
129
+
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
130
+
const (
131
+
Method = "DELETE"
132
+
Endpoint = "/repo"
133
+
)
134
+
135
+
body, _ := json.Marshal(map[string]any{
136
+
"did": did,
137
+
"name": repoName,
138
+
})
139
+
140
+
req, err := s.newRequest(Method, Endpoint, body)
141
+
if err != nil {
142
+
return nil, err
143
+
}
144
+
145
+
return s.client.Do(req)
146
+
}
147
+
148
+
func (s *SignedClient) AddMember(did string) (*http.Response, error) {
149
+
const (
150
+
Method = "PUT"
151
+
Endpoint = "/member/add"
152
+
)
153
+
154
+
body, _ := json.Marshal(map[string]any{
155
+
"did": did,
156
+
})
157
+
158
+
req, err := s.newRequest(Method, Endpoint, body)
159
+
if err != nil {
160
+
return nil, err
161
+
}
162
+
163
+
return s.client.Do(req)
164
+
}
165
+
166
+
func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
167
+
const (
168
+
Method = "PUT"
169
+
)
170
+
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
171
+
172
+
body, _ := json.Marshal(map[string]any{
173
+
"branch": branch,
174
+
})
175
+
176
+
req, err := s.newRequest(Method, endpoint, body)
177
+
if err != nil {
178
+
return nil, err
179
+
}
180
+
181
+
return s.client.Do(req)
182
+
}
183
+
184
+
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
185
+
const (
186
+
Method = "POST"
187
+
)
188
+
endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
189
+
190
+
body, _ := json.Marshal(map[string]any{
191
+
"did": memberDid,
192
+
})
193
+
194
+
req, err := s.newRequest(Method, endpoint, body)
195
+
if err != nil {
196
+
return nil, err
197
+
}
198
+
199
+
return s.client.Do(req)
200
+
}
201
+
202
+
func (s *SignedClient) Merge(
203
+
patch []byte,
204
+
ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
205
+
) (*http.Response, error) {
206
+
const (
207
+
Method = "POST"
208
+
)
209
+
endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
210
+
211
+
mr := types.MergeRequest{
212
+
Branch: branch,
213
+
CommitMessage: commitMessage,
214
+
CommitBody: commitBody,
215
+
AuthorName: authorName,
216
+
AuthorEmail: authorEmail,
217
+
Patch: string(patch),
218
+
}
219
+
220
+
body, _ := json.Marshal(mr)
221
+
222
+
req, err := s.newRequest(Method, endpoint, body)
223
+
if err != nil {
224
+
return nil, err
225
+
}
226
+
227
+
return s.client.Do(req)
228
+
}
229
+
230
+
func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
231
+
const (
232
+
Method = "POST"
233
+
)
234
+
endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
235
+
236
+
body, _ := json.Marshal(map[string]any{
237
+
"patch": string(patch),
238
+
"branch": branch,
239
+
})
240
+
241
+
req, err := s.newRequest(Method, endpoint, body)
242
+
if err != nil {
243
+
return nil, err
244
+
}
245
+
246
+
return s.client.Do(req)
247
+
}
248
+
249
+
func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
250
+
const (
251
+
Method = "POST"
252
+
)
253
+
endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
254
+
255
+
req, err := s.newRequest(Method, endpoint, nil)
256
+
if err != nil {
257
+
return nil, err
258
+
}
259
+
260
+
return s.client.Do(req)
261
+
}
262
+
263
+
type UnsignedClient struct {
264
+
Url *url.URL
265
+
client *http.Client
266
+
}
267
+
268
+
func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
269
+
client := &http.Client{
270
+
Timeout: 5 * time.Second,
271
+
}
272
+
273
+
scheme := "https"
274
+
if dev {
275
+
scheme = "http"
276
+
}
277
+
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
278
+
if err != nil {
279
+
return nil, err
280
+
}
281
+
282
+
unsignedClient := &UnsignedClient{
283
+
client: client,
284
+
Url: url,
285
+
}
286
+
287
+
return unsignedClient, nil
288
+
}
289
+
290
+
func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
291
+
reqUrl := us.Url.JoinPath(endpoint)
292
+
293
+
// add query parameters
294
+
if query != nil {
295
+
reqUrl.RawQuery = query.Encode()
296
+
}
297
+
298
+
return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
299
+
}
300
+
301
+
func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
302
+
const (
303
+
Method = "GET"
304
+
)
305
+
306
+
endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
307
+
if ref == "" {
308
+
endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
309
+
}
310
+
311
+
req, err := us.newRequest(Method, endpoint, nil, nil)
312
+
if err != nil {
313
+
return nil, err
314
+
}
315
+
316
+
return us.client.Do(req)
317
+
}
318
+
319
+
func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
320
+
const (
321
+
Method = "GET"
322
+
)
323
+
324
+
endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
325
+
326
+
query := url.Values{}
327
+
query.Add("page", strconv.Itoa(page))
328
+
query.Add("per_page", strconv.Itoa(60))
329
+
330
+
req, err := us.newRequest(Method, endpoint, query, nil)
331
+
if err != nil {
332
+
return nil, err
333
+
}
334
+
335
+
return us.client.Do(req)
336
+
}
337
+
338
+
func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
339
+
const (
340
+
Method = "GET"
341
+
)
342
+
343
+
endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
344
+
345
+
req, err := us.newRequest(Method, endpoint, nil, nil)
346
+
if err != nil {
347
+
return nil, err
348
+
}
349
+
350
+
return us.client.Do(req)
351
+
}
352
+
353
+
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
354
+
const (
355
+
Method = "GET"
356
+
)
357
+
358
+
endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
359
+
360
+
req, err := us.newRequest(Method, endpoint, nil, nil)
361
+
if err != nil {
362
+
return nil, err
363
+
}
364
+
365
+
resp, err := us.client.Do(req)
366
+
if err != nil {
367
+
return nil, err
368
+
}
369
+
370
+
body, err := io.ReadAll(resp.Body)
371
+
if err != nil {
372
+
return nil, err
373
+
}
374
+
375
+
var result types.RepoTagsResponse
376
+
err = json.Unmarshal(body, &result)
377
+
if err != nil {
378
+
return nil, err
379
+
}
380
+
381
+
return &result, nil
382
+
}
383
+
384
+
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
385
+
const (
386
+
Method = "GET"
387
+
)
388
+
389
+
endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
390
+
391
+
req, err := us.newRequest(Method, endpoint, nil, nil)
392
+
if err != nil {
393
+
return nil, err
394
+
}
395
+
396
+
return us.client.Do(req)
397
+
}
398
+
399
+
func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
400
+
const (
401
+
Method = "GET"
402
+
)
403
+
404
+
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
405
+
406
+
req, err := us.newRequest(Method, endpoint, nil, nil)
407
+
if err != nil {
408
+
return nil, err
409
+
}
410
+
411
+
resp, err := us.client.Do(req)
412
+
if err != nil {
413
+
return nil, err
414
+
}
415
+
defer resp.Body.Close()
416
+
417
+
var defaultBranch types.RepoDefaultBranchResponse
418
+
if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
419
+
return nil, err
420
+
}
421
+
422
+
return &defaultBranch, nil
423
+
}
424
+
425
+
func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
426
+
const (
427
+
Method = "GET"
428
+
Endpoint = "/capabilities"
429
+
)
430
+
431
+
req, err := us.newRequest(Method, Endpoint, nil, nil)
432
+
if err != nil {
433
+
return nil, err
434
+
}
435
+
436
+
resp, err := us.client.Do(req)
437
+
if err != nil {
438
+
return nil, err
439
+
}
440
+
defer resp.Body.Close()
441
+
442
+
var capabilities types.Capabilities
443
+
if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
444
+
return nil, err
445
+
}
446
+
447
+
return &capabilities, nil
448
+
}
449
+
450
+
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
451
+
const (
452
+
Method = "GET"
453
+
)
454
+
455
+
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
456
+
457
+
req, err := us.newRequest(Method, endpoint, nil, nil)
458
+
if err != nil {
459
+
return nil, fmt.Errorf("Failed to create request.")
460
+
}
461
+
462
+
compareResp, err := us.client.Do(req)
463
+
if err != nil {
464
+
return nil, fmt.Errorf("Failed to create request.")
465
+
}
466
+
defer compareResp.Body.Close()
467
+
468
+
switch compareResp.StatusCode {
469
+
case 404:
470
+
case 400:
471
+
return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
472
+
}
473
+
474
+
respBody, err := io.ReadAll(compareResp.Body)
475
+
if err != nil {
476
+
log.Println("failed to compare across branches")
477
+
return nil, fmt.Errorf("Failed to compare branches.")
478
+
}
479
+
defer compareResp.Body.Close()
480
+
481
+
var formatPatchResponse types.RepoFormatPatchResponse
482
+
err = json.Unmarshal(respBody, &formatPatchResponse)
483
+
if err != nil {
484
+
log.Println("failed to unmarshal format-patch response", err)
485
+
return nil, fmt.Errorf("failed to compare branches.")
486
+
}
487
+
488
+
return &formatPatchResponse, nil
489
+
}
+5
-58
appview/middleware/middleware.go
+5
-58
appview/middleware/middleware.go
···
5
"log"
6
"net/http"
7
"strconv"
8
-
"time"
9
10
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
"tangled.sh/tangled.sh/core/appview"
13
-
"tangled.sh/tangled.sh/core/appview/auth"
14
"tangled.sh/tangled.sh/core/appview/pagination"
15
)
16
17
type Middleware func(http.Handler) http.Handler
18
19
-
func AuthMiddleware(a *auth.Auth) Middleware {
20
return func(next http.Handler) http.Handler {
21
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
redirectFunc := func(w http.ResponseWriter, r *http.Request) {
···
29
}
30
}
31
32
-
session, err := a.GetSession(r)
33
-
if session.IsNew || err != nil {
34
log.Printf("not logged in, redirecting")
35
redirectFunc(w, r)
36
return
37
}
38
39
-
authorized, ok := session.Values[appview.SessionAuthenticated].(bool)
40
-
if !ok || !authorized {
41
log.Printf("not logged in, redirecting")
42
redirectFunc(w, r)
43
return
44
-
}
45
-
46
-
// refresh if nearing expiry
47
-
// TODO: dedup with /login
48
-
expiryStr := session.Values[appview.SessionExpiry].(string)
49
-
expiry, err := time.Parse(time.RFC3339, expiryStr)
50
-
if err != nil {
51
-
log.Println("invalid expiry time", err)
52
-
redirectFunc(w, r)
53
-
return
54
-
}
55
-
pdsUrl, ok1 := session.Values[appview.SessionPds].(string)
56
-
did, ok2 := session.Values[appview.SessionDid].(string)
57
-
refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string)
58
-
59
-
if !ok1 || !ok2 || !ok3 {
60
-
log.Println("invalid expiry time", err)
61
-
redirectFunc(w, r)
62
-
return
63
-
}
64
-
65
-
if time.Now().After(expiry) {
66
-
log.Println("token expired, refreshing ...")
67
-
68
-
client := xrpc.Client{
69
-
Host: pdsUrl,
70
-
Auth: &xrpc.AuthInfo{
71
-
Did: did,
72
-
AccessJwt: refreshJwt,
73
-
RefreshJwt: refreshJwt,
74
-
},
75
-
}
76
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
77
-
if err != nil {
78
-
log.Println("failed to refresh session", err)
79
-
redirectFunc(w, r)
80
-
return
81
-
}
82
-
83
-
sessionish := auth.RefreshSessionWrapper{atSession}
84
-
85
-
err = a.StoreSession(r, w, &sessionish, pdsUrl)
86
-
if err != nil {
87
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
88
-
return
89
-
}
90
-
91
-
log.Println("successfully refreshed token")
92
}
93
94
next.ServeHTTP(w, r)
···
5
"log"
6
"net/http"
7
"strconv"
8
9
+
"tangled.sh/tangled.sh/core/appview/oauth"
10
"tangled.sh/tangled.sh/core/appview/pagination"
11
)
12
13
type Middleware func(http.Handler) http.Handler
14
15
+
func AuthMiddleware(a *oauth.OAuth) Middleware {
16
return func(next http.Handler) http.Handler {
17
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
redirectFunc := func(w http.ResponseWriter, r *http.Request) {
···
25
}
26
}
27
28
+
_, auth, err := a.GetSession(r)
29
+
if err != nil {
30
log.Printf("not logged in, redirecting")
31
redirectFunc(w, r)
32
return
33
}
34
35
+
if !auth {
36
log.Printf("not logged in, redirecting")
37
redirectFunc(w, r)
38
return
39
}
40
41
next.ServeHTTP(w, r)
+24
appview/oauth/client/oauth_client.go
+24
appview/oauth/client/oauth_client.go
···
···
1
+
package client
2
+
3
+
import (
4
+
oauth "github.com/haileyok/atproto-oauth-golang"
5
+
"github.com/haileyok/atproto-oauth-golang/helpers"
6
+
)
7
+
8
+
type OAuthClient struct {
9
+
*oauth.Client
10
+
}
11
+
12
+
func NewClient(clientId, clientJwk, redirectUri string) (*OAuthClient, error) {
13
+
k, err := helpers.ParseJWKFromBytes([]byte(clientJwk))
14
+
if err != nil {
15
+
return nil, err
16
+
}
17
+
18
+
cli, err := oauth.NewClient(oauth.ClientArgs{
19
+
ClientId: clientId,
20
+
ClientJwk: k,
21
+
RedirectUri: redirectUri,
22
+
})
23
+
return &OAuthClient{cli}, err
24
+
}
+309
appview/oauth/handler/handler.go
+309
appview/oauth/handler/handler.go
···
···
1
+
package oauth
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"log"
7
+
"net/http"
8
+
"net/url"
9
+
"strings"
10
+
11
+
"github.com/go-chi/chi/v5"
12
+
"github.com/gorilla/sessions"
13
+
"github.com/haileyok/atproto-oauth-golang/helpers"
14
+
"github.com/lestrrat-go/jwx/v2/jwk"
15
+
"tangled.sh/tangled.sh/core/appview"
16
+
"tangled.sh/tangled.sh/core/appview/db"
17
+
"tangled.sh/tangled.sh/core/appview/knotclient"
18
+
"tangled.sh/tangled.sh/core/appview/middleware"
19
+
"tangled.sh/tangled.sh/core/appview/oauth"
20
+
"tangled.sh/tangled.sh/core/appview/oauth/client"
21
+
"tangled.sh/tangled.sh/core/appview/pages"
22
+
"tangled.sh/tangled.sh/core/rbac"
23
+
)
24
+
25
+
const (
26
+
oauthScope = "atproto transition:generic"
27
+
)
28
+
29
+
type OAuthHandler struct {
30
+
Config *appview.Config
31
+
Pages *pages.Pages
32
+
Resolver *appview.Resolver
33
+
Db *db.DB
34
+
Store *sessions.CookieStore
35
+
OAuth *oauth.OAuth
36
+
Enforcer *rbac.Enforcer
37
+
}
38
+
39
+
func (o *OAuthHandler) Router() http.Handler {
40
+
r := chi.NewRouter()
41
+
42
+
r.Get("/login", o.login)
43
+
r.Post("/login", o.login)
44
+
45
+
r.With(middleware.AuthMiddleware(o.OAuth)).Post("/logout", o.logout)
46
+
47
+
r.Get("/oauth/client-metadata.json", o.clientMetadata)
48
+
r.Get("/oauth/jwks.json", o.jwks)
49
+
r.Get("/oauth/callback", o.callback)
50
+
return r
51
+
}
52
+
53
+
func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) {
54
+
w.Header().Set("Content-Type", "application/json")
55
+
w.WriteHeader(http.StatusOK)
56
+
json.NewEncoder(w).Encode(o.OAuth.ClientMetadata())
57
+
}
58
+
59
+
func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) {
60
+
jwks := o.Config.OAuth.Jwks
61
+
pubKey, err := pubKeyFromJwk(jwks)
62
+
if err != nil {
63
+
log.Printf("error parsing public key: %v", err)
64
+
http.Error(w, err.Error(), http.StatusInternalServerError)
65
+
return
66
+
}
67
+
68
+
response := helpers.CreateJwksResponseObject(pubKey)
69
+
70
+
w.Header().Set("Content-Type", "application/json")
71
+
w.WriteHeader(http.StatusOK)
72
+
json.NewEncoder(w).Encode(response)
73
+
}
74
+
75
+
func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) {
76
+
switch r.Method {
77
+
case http.MethodGet:
78
+
o.Pages.Login(w, pages.LoginParams{})
79
+
case http.MethodPost:
80
+
handle := strings.TrimPrefix(r.FormValue("handle"), "@")
81
+
82
+
resolved, err := o.Resolver.ResolveIdent(r.Context(), handle)
83
+
if err != nil {
84
+
log.Println("failed to resolve handle:", err)
85
+
o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
86
+
return
87
+
}
88
+
self := o.OAuth.ClientMetadata()
89
+
oauthClient, err := client.NewClient(
90
+
self.ClientID,
91
+
o.Config.OAuth.Jwks,
92
+
self.RedirectURIs[0],
93
+
)
94
+
95
+
if err != nil {
96
+
log.Println("failed to create oauth client:", err)
97
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
98
+
return
99
+
}
100
+
101
+
authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint())
102
+
if err != nil {
103
+
log.Println("failed to resolve auth server:", err)
104
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
105
+
return
106
+
}
107
+
108
+
authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer)
109
+
if err != nil {
110
+
log.Println("failed to fetch auth server metadata:", err)
111
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
112
+
return
113
+
}
114
+
115
+
dpopKey, err := helpers.GenerateKey(nil)
116
+
if err != nil {
117
+
log.Println("failed to generate dpop key:", err)
118
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
119
+
return
120
+
}
121
+
122
+
dpopKeyJson, err := json.Marshal(dpopKey)
123
+
if err != nil {
124
+
log.Println("failed to marshal dpop key:", err)
125
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
126
+
return
127
+
}
128
+
129
+
parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey)
130
+
if err != nil {
131
+
log.Println("failed to send par auth request:", err)
132
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
133
+
return
134
+
}
135
+
136
+
err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{
137
+
Did: resolved.DID.String(),
138
+
PdsUrl: resolved.PDSEndpoint(),
139
+
Handle: handle,
140
+
AuthserverIss: authMeta.Issuer,
141
+
PkceVerifier: parResp.PkceVerifier,
142
+
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
143
+
DpopPrivateJwk: string(dpopKeyJson),
144
+
State: parResp.State,
145
+
})
146
+
if err != nil {
147
+
log.Println("failed to save oauth request:", err)
148
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
149
+
return
150
+
}
151
+
152
+
u, _ := url.Parse(authMeta.AuthorizationEndpoint)
153
+
query := url.Values{}
154
+
query.Add("client_id", self.ClientID)
155
+
query.Add("request_uri", parResp.RequestUri)
156
+
u.RawQuery = query.Encode()
157
+
o.Pages.HxRedirect(w, u.String())
158
+
}
159
+
}
160
+
161
+
func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
162
+
state := r.FormValue("state")
163
+
164
+
oauthRequest, err := db.GetOAuthRequestByState(o.Db, state)
165
+
if err != nil {
166
+
log.Println("failed to get oauth request:", err)
167
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
168
+
return
169
+
}
170
+
171
+
defer func() {
172
+
err := db.DeleteOAuthRequestByState(o.Db, state)
173
+
if err != nil {
174
+
log.Println("failed to delete oauth request for state:", state, err)
175
+
}
176
+
}()
177
+
178
+
error := r.FormValue("error")
179
+
errorDescription := r.FormValue("error_description")
180
+
if error != "" || errorDescription != "" {
181
+
log.Printf("error: %s, %s", error, errorDescription)
182
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
183
+
return
184
+
}
185
+
186
+
code := r.FormValue("code")
187
+
if code == "" {
188
+
log.Println("missing code for state: ", state)
189
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
190
+
return
191
+
}
192
+
193
+
iss := r.FormValue("iss")
194
+
if iss == "" {
195
+
log.Println("missing iss for state: ", state)
196
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
197
+
return
198
+
}
199
+
200
+
self := o.OAuth.ClientMetadata()
201
+
202
+
oauthClient, err := client.NewClient(
203
+
self.ClientID,
204
+
o.Config.OAuth.Jwks,
205
+
self.RedirectURIs[0],
206
+
)
207
+
208
+
if err != nil {
209
+
log.Println("failed to create oauth client:", err)
210
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
211
+
return
212
+
}
213
+
214
+
jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
215
+
if err != nil {
216
+
log.Println("failed to parse jwk:", err)
217
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
218
+
return
219
+
}
220
+
221
+
tokenResp, err := oauthClient.InitialTokenRequest(
222
+
r.Context(),
223
+
code,
224
+
oauthRequest.AuthserverIss,
225
+
oauthRequest.PkceVerifier,
226
+
oauthRequest.DpopAuthserverNonce,
227
+
jwk,
228
+
)
229
+
if err != nil {
230
+
log.Println("failed to get token:", err)
231
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
232
+
return
233
+
}
234
+
235
+
if tokenResp.Scope != oauthScope {
236
+
log.Println("scope doesn't match:", tokenResp.Scope)
237
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
238
+
return
239
+
}
240
+
241
+
err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp)
242
+
if err != nil {
243
+
log.Println("failed to save session:", err)
244
+
o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
245
+
return
246
+
}
247
+
248
+
log.Println("session saved successfully")
249
+
go o.addToDefaultKnot(oauthRequest.Did)
250
+
251
+
http.Redirect(w, r, "/", http.StatusFound)
252
+
}
253
+
254
+
func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) {
255
+
err := o.OAuth.ClearSession(r, w)
256
+
if err != nil {
257
+
log.Println("failed to clear session:", err)
258
+
http.Redirect(w, r, "/", http.StatusFound)
259
+
return
260
+
}
261
+
262
+
log.Println("session cleared successfully")
263
+
http.Redirect(w, r, "/", http.StatusFound)
264
+
}
265
+
266
+
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
267
+
k, err := helpers.ParseJWKFromBytes([]byte(jwks))
268
+
if err != nil {
269
+
return nil, err
270
+
}
271
+
pubKey, err := k.PublicKey()
272
+
if err != nil {
273
+
return nil, err
274
+
}
275
+
return pubKey, nil
276
+
}
277
+
278
+
func (o *OAuthHandler) addToDefaultKnot(did string) {
279
+
defaultKnot := "knot1.tangled.sh"
280
+
281
+
log.Printf("adding %s to default knot", did)
282
+
err := o.Enforcer.AddMember(defaultKnot, did)
283
+
if err != nil {
284
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
285
+
return
286
+
}
287
+
err = o.Enforcer.E.SavePolicy()
288
+
if err != nil {
289
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
290
+
return
291
+
}
292
+
293
+
secret, err := db.GetRegistrationKey(o.Db, defaultKnot)
294
+
if err != nil {
295
+
log.Println("failed to get registration key for knot1.tangled.sh")
296
+
return
297
+
}
298
+
signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.Config.Core.Dev)
299
+
resp, err := signedClient.AddMember(did)
300
+
if err != nil {
301
+
log.Println("failed to add user to knot1.tangled.sh: ", err)
302
+
return
303
+
}
304
+
305
+
if resp.StatusCode != http.StatusNoContent {
306
+
log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
307
+
return
308
+
}
309
+
}
+268
appview/oauth/oauth.go
+268
appview/oauth/oauth.go
···
···
1
+
package oauth
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
"net/url"
8
+
"time"
9
+
10
+
"github.com/gorilla/sessions"
11
+
oauth "github.com/haileyok/atproto-oauth-golang"
12
+
"github.com/haileyok/atproto-oauth-golang/helpers"
13
+
"tangled.sh/tangled.sh/core/appview"
14
+
"tangled.sh/tangled.sh/core/appview/db"
15
+
"tangled.sh/tangled.sh/core/appview/oauth/client"
16
+
xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient"
17
+
)
18
+
19
+
type OAuthRequest struct {
20
+
ID uint
21
+
AuthserverIss string
22
+
State string
23
+
Did string
24
+
PdsUrl string
25
+
PkceVerifier string
26
+
DpopAuthserverNonce string
27
+
DpopPrivateJwk string
28
+
}
29
+
30
+
type OAuth struct {
31
+
Store *sessions.CookieStore
32
+
Db *db.DB
33
+
Config *appview.Config
34
+
}
35
+
36
+
func NewOAuth(db *db.DB, config *appview.Config) *OAuth {
37
+
return &OAuth{
38
+
Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)),
39
+
Db: db,
40
+
Config: config,
41
+
}
42
+
}
43
+
44
+
func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error {
45
+
// first we save the did in the user session
46
+
userSession, err := o.Store.Get(r, appview.SessionName)
47
+
if err != nil {
48
+
return err
49
+
}
50
+
51
+
userSession.Values[appview.SessionDid] = oreq.Did
52
+
userSession.Values[appview.SessionHandle] = oreq.Handle
53
+
userSession.Values[appview.SessionPds] = oreq.PdsUrl
54
+
userSession.Values[appview.SessionAuthenticated] = true
55
+
err = userSession.Save(r, w)
56
+
if err != nil {
57
+
return fmt.Errorf("error saving user session: %w", err)
58
+
}
59
+
60
+
// then save the whole thing in the db
61
+
session := db.OAuthSession{
62
+
Did: oreq.Did,
63
+
Handle: oreq.Handle,
64
+
PdsUrl: oreq.PdsUrl,
65
+
DpopAuthserverNonce: oreq.DpopAuthserverNonce,
66
+
AuthServerIss: oreq.AuthserverIss,
67
+
DpopPrivateJwk: oreq.DpopPrivateJwk,
68
+
AccessJwt: oresp.AccessToken,
69
+
RefreshJwt: oresp.RefreshToken,
70
+
Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339),
71
+
}
72
+
73
+
return db.SaveOAuthSession(o.Db, session)
74
+
}
75
+
76
+
func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error {
77
+
userSession, err := o.Store.Get(r, appview.SessionName)
78
+
if err != nil || userSession.IsNew {
79
+
return fmt.Errorf("error getting user session (or new session?): %w", err)
80
+
}
81
+
82
+
did := userSession.Values[appview.SessionDid].(string)
83
+
84
+
err = db.DeleteOAuthSessionByDid(o.Db, did)
85
+
if err != nil {
86
+
return fmt.Errorf("error deleting oauth session: %w", err)
87
+
}
88
+
89
+
userSession.Options.MaxAge = -1
90
+
91
+
return userSession.Save(r, w)
92
+
}
93
+
94
+
func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) {
95
+
userSession, err := o.Store.Get(r, appview.SessionName)
96
+
if err != nil || userSession.IsNew {
97
+
return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err)
98
+
}
99
+
100
+
did := userSession.Values[appview.SessionDid].(string)
101
+
auth := userSession.Values[appview.SessionAuthenticated].(bool)
102
+
103
+
session, err := db.GetOAuthSessionByDid(o.Db, did)
104
+
if err != nil {
105
+
return nil, false, fmt.Errorf("error getting oauth session: %w", err)
106
+
}
107
+
108
+
expiry, err := time.Parse(time.RFC3339, session.Expiry)
109
+
if err != nil {
110
+
return nil, false, fmt.Errorf("error parsing expiry time: %w", err)
111
+
}
112
+
if expiry.Sub(time.Now()) <= 5*time.Minute {
113
+
privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk))
114
+
if err != nil {
115
+
return nil, false, err
116
+
}
117
+
118
+
self := o.ClientMetadata()
119
+
120
+
oauthClient, err := client.NewClient(
121
+
self.ClientID,
122
+
o.Config.OAuth.Jwks,
123
+
self.RedirectURIs[0],
124
+
)
125
+
126
+
if err != nil {
127
+
return nil, false, err
128
+
}
129
+
130
+
resp, err := oauthClient.RefreshTokenRequest(r.Context(), session.RefreshJwt, session.AuthServerIss, session.DpopAuthserverNonce, privateJwk)
131
+
if err != nil {
132
+
return nil, false, err
133
+
}
134
+
135
+
newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339)
136
+
err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry)
137
+
if err != nil {
138
+
return nil, false, fmt.Errorf("error refreshing oauth session: %w", err)
139
+
}
140
+
141
+
// update the current session
142
+
session.AccessJwt = resp.AccessToken
143
+
session.RefreshJwt = resp.RefreshToken
144
+
session.DpopAuthserverNonce = resp.DpopAuthserverNonce
145
+
session.Expiry = newExpiry
146
+
}
147
+
148
+
return session, auth, nil
149
+
}
150
+
151
+
type User struct {
152
+
Handle string
153
+
Did string
154
+
Pds string
155
+
}
156
+
157
+
func (a *OAuth) GetUser(r *http.Request) *User {
158
+
clientSession, err := a.Store.Get(r, appview.SessionName)
159
+
160
+
if err != nil || clientSession.IsNew {
161
+
return nil
162
+
}
163
+
164
+
return &User{
165
+
Handle: clientSession.Values[appview.SessionHandle].(string),
166
+
Did: clientSession.Values[appview.SessionDid].(string),
167
+
Pds: clientSession.Values[appview.SessionPds].(string),
168
+
}
169
+
}
170
+
171
+
func (a *OAuth) GetDid(r *http.Request) string {
172
+
clientSession, err := a.Store.Get(r, appview.SessionName)
173
+
174
+
if err != nil || clientSession.IsNew {
175
+
return ""
176
+
}
177
+
178
+
return clientSession.Values[appview.SessionDid].(string)
179
+
}
180
+
181
+
func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
182
+
session, auth, err := o.GetSession(r)
183
+
if err != nil {
184
+
return nil, fmt.Errorf("error getting session: %w", err)
185
+
}
186
+
if !auth {
187
+
return nil, fmt.Errorf("not authorized")
188
+
}
189
+
190
+
client := &oauth.XrpcClient{
191
+
OnDpopPdsNonceChanged: func(did, newNonce string) {
192
+
err := db.UpdateDpopPdsNonce(o.Db, did, newNonce)
193
+
if err != nil {
194
+
log.Printf("error updating dpop pds nonce: %v", err)
195
+
}
196
+
},
197
+
}
198
+
199
+
privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk))
200
+
if err != nil {
201
+
return nil, fmt.Errorf("error parsing private jwk: %w", err)
202
+
}
203
+
204
+
xrpcClient := xrpc.NewClient(client, &oauth.XrpcAuthedRequestArgs{
205
+
Did: session.Did,
206
+
PdsUrl: session.PdsUrl,
207
+
DpopPdsNonce: session.PdsUrl,
208
+
AccessToken: session.AccessJwt,
209
+
Issuer: session.AuthServerIss,
210
+
DpopPrivateJwk: privateJwk,
211
+
})
212
+
213
+
return xrpcClient, nil
214
+
}
215
+
216
+
type ClientMetadata struct {
217
+
ClientID string `json:"client_id"`
218
+
ClientName string `json:"client_name"`
219
+
SubjectType string `json:"subject_type"`
220
+
ClientURI string `json:"client_uri"`
221
+
RedirectURIs []string `json:"redirect_uris"`
222
+
GrantTypes []string `json:"grant_types"`
223
+
ResponseTypes []string `json:"response_types"`
224
+
ApplicationType string `json:"application_type"`
225
+
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
226
+
JwksURI string `json:"jwks_uri"`
227
+
Scope string `json:"scope"`
228
+
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
229
+
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
230
+
}
231
+
232
+
func (o *OAuth) ClientMetadata() ClientMetadata {
233
+
makeRedirectURIs := func(c string) []string {
234
+
return []string{fmt.Sprintf("%s/oauth/callback", c)}
235
+
}
236
+
237
+
clientURI := o.Config.Core.AppviewHost
238
+
clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI)
239
+
redirectURIs := makeRedirectURIs(clientURI)
240
+
241
+
if o.Config.Core.Dev {
242
+
clientURI = fmt.Sprintf("http://127.0.0.1:3000")
243
+
redirectURIs = makeRedirectURIs(clientURI)
244
+
245
+
query := url.Values{}
246
+
query.Add("redirect_uri", redirectURIs[0])
247
+
query.Add("scope", "atproto transition:generic")
248
+
clientID = fmt.Sprintf("http://localhost?%s", query.Encode())
249
+
}
250
+
251
+
jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI)
252
+
253
+
return ClientMetadata{
254
+
ClientID: clientID,
255
+
ClientName: "Tangled",
256
+
SubjectType: "public",
257
+
ClientURI: clientURI,
258
+
RedirectURIs: redirectURIs,
259
+
GrantTypes: []string{"authorization_code", "refresh_token"},
260
+
ResponseTypes: []string{"code"},
261
+
ApplicationType: "web",
262
+
DpopBoundAccessTokens: true,
263
+
JwksURI: jwksURI,
264
+
Scope: "atproto transition:generic",
265
+
TokenEndpointAuthMethod: "private_key_jwt",
266
+
TokenEndpointAuthSigningAlg: "ES256",
267
+
}
268
+
}
+2
-1
appview/pages/funcmap.go
+2
-1
appview/pages/funcmap.go
···
13
"time"
14
15
"github.com/dustin/go-humanize"
16
"tangled.sh/tangled.sh/core/appview/filetree"
17
"tangled.sh/tangled.sh/core/appview/pages/markup"
18
)
···
144
},
145
"markdown": func(text string) template.HTML {
146
rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault}
147
-
return template.HTML(rctx.RenderMarkdown(text))
148
},
149
"isNil": func(t any) bool {
150
// returns false for other "zero" values
···
13
"time"
14
15
"github.com/dustin/go-humanize"
16
+
"github.com/microcosm-cc/bluemonday"
17
"tangled.sh/tangled.sh/core/appview/filetree"
18
"tangled.sh/tangled.sh/core/appview/pages/markup"
19
)
···
145
},
146
"markdown": func(text string) template.HTML {
147
rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault}
148
+
return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text)))
149
},
150
"isNil": func(t any) bool {
151
// returns false for other "zero" values
+2
appview/pages/markup/markdown.go
+2
appview/pages/markup/markdown.go
···
10
"github.com/yuin/goldmark/ast"
11
"github.com/yuin/goldmark/extension"
12
"github.com/yuin/goldmark/parser"
13
"github.com/yuin/goldmark/text"
14
"github.com/yuin/goldmark/util"
15
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
41
goldmark.WithParserOptions(
42
parser.WithAutoHeadingID(),
43
),
44
)
45
46
if rctx != nil {
···
10
"github.com/yuin/goldmark/ast"
11
"github.com/yuin/goldmark/extension"
12
"github.com/yuin/goldmark/parser"
13
+
"github.com/yuin/goldmark/renderer/html"
14
"github.com/yuin/goldmark/text"
15
"github.com/yuin/goldmark/util"
16
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
42
goldmark.WithParserOptions(
43
parser.WithAutoHeadingID(),
44
),
45
+
goldmark.WithRendererOptions(html.WithUnsafe()),
46
)
47
48
if rctx != nil {
+82
-43
appview/pages/pages.go
+82
-43
appview/pages/pages.go
···
16
"strings"
17
18
"tangled.sh/tangled.sh/core/appview"
19
-
"tangled.sh/tangled.sh/core/appview/auth"
20
"tangled.sh/tangled.sh/core/appview/db"
21
"tangled.sh/tangled.sh/core/appview/pages/markup"
22
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
23
"tangled.sh/tangled.sh/core/appview/pagination"
···
48
func NewPages(config *appview.Config) *Pages {
49
// initialized with safe defaults, can be overriden per use
50
rctx := &markup.RenderContext{
51
-
IsDev: config.Dev,
52
-
CamoUrl: config.CamoHost,
53
-
CamoSecret: config.CamoSharedSecret,
54
}
55
56
p := &Pages{
57
t: make(map[string]*template.Template),
58
-
dev: config.Dev,
59
embedFS: Files,
60
rctx: rctx,
61
templateDir: "appview/pages",
···
250
}
251
252
type TimelineParams struct {
253
-
LoggedInUser *auth.User
254
Timeline []db.TimelineEvent
255
DidHandleMap map[string]string
256
}
···
260
}
261
262
type SettingsParams struct {
263
-
LoggedInUser *auth.User
264
PubKeys []db.PublicKey
265
Emails []db.Email
266
}
···
270
}
271
272
type KnotsParams struct {
273
-
LoggedInUser *auth.User
274
Registrations []db.Registration
275
}
276
···
279
}
280
281
type KnotParams struct {
282
-
LoggedInUser *auth.User
283
DidHandleMap map[string]string
284
Registration *db.Registration
285
Members []string
···
291
}
292
293
type NewRepoParams struct {
294
-
LoggedInUser *auth.User
295
Knots []string
296
}
297
···
300
}
301
302
type ForkRepoParams struct {
303
-
LoggedInUser *auth.User
304
Knots []string
305
RepoInfo repoinfo.RepoInfo
306
}
···
310
}
311
312
type ProfilePageParams struct {
313
-
LoggedInUser *auth.User
314
-
UserDid string
315
-
UserHandle string
316
Repos []db.Repo
317
CollaboratingRepos []db.Repo
318
-
ProfileStats ProfileStats
319
-
FollowStatus db.FollowStatus
320
-
AvatarUri string
321
ProfileTimeline *db.ProfileTimeline
322
323
DidHandleMap map[string]string
324
}
325
326
-
type ProfileStats struct {
327
-
Followers int
328
-
Following int
329
}
330
331
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
332
return p.execute("user/profile", w, params)
333
}
334
335
type FollowFragmentParams struct {
336
UserDid string
337
FollowStatus db.FollowStatus
···
341
return p.executePlain("user/fragments/follow", w, params)
342
}
343
344
type RepoActionsFragmentParams struct {
345
IsStarred bool
346
RepoAt syntax.ATURI
···
364
}
365
366
type RepoIndexParams struct {
367
-
LoggedInUser *auth.User
368
RepoInfo repoinfo.RepoInfo
369
Active string
370
TagMap map[string][]string
···
405
}
406
407
type RepoLogParams struct {
408
-
LoggedInUser *auth.User
409
RepoInfo repoinfo.RepoInfo
410
TagMap map[string][]string
411
types.RepoLogResponse
···
419
}
420
421
type RepoCommitParams struct {
422
-
LoggedInUser *auth.User
423
RepoInfo repoinfo.RepoInfo
424
Active string
425
EmailToDidOrHandle map[string]string
···
433
}
434
435
type RepoTreeParams struct {
436
-
LoggedInUser *auth.User
437
RepoInfo repoinfo.RepoInfo
438
Active string
439
BreadCrumbs [][]string
···
469
}
470
471
type RepoBranchesParams struct {
472
-
LoggedInUser *auth.User
473
RepoInfo repoinfo.RepoInfo
474
Active string
475
types.RepoBranchesResponse
···
481
}
482
483
type RepoTagsParams struct {
484
-
LoggedInUser *auth.User
485
RepoInfo repoinfo.RepoInfo
486
Active string
487
types.RepoTagsResponse
···
495
}
496
497
type RepoArtifactParams struct {
498
-
LoggedInUser *auth.User
499
RepoInfo repoinfo.RepoInfo
500
Artifact db.Artifact
501
}
···
505
}
506
507
type RepoBlobParams struct {
508
-
LoggedInUser *auth.User
509
RepoInfo repoinfo.RepoInfo
510
Active string
511
BreadCrumbs [][]string
···
523
case markup.FormatMarkdown:
524
p.rctx.RepoInfo = params.RepoInfo
525
p.rctx.RendererType = markup.RendererTypeRepoMarkdown
526
-
params.RenderedContents = template.HTML(p.rctx.RenderMarkdown(params.Contents))
527
}
528
}
529
···
567
}
568
569
type RepoSettingsParams struct {
570
-
LoggedInUser *auth.User
571
RepoInfo repoinfo.RepoInfo
572
Collaborators []Collaborator
573
Active string
···
583
}
584
585
type RepoIssuesParams struct {
586
-
LoggedInUser *auth.User
587
RepoInfo repoinfo.RepoInfo
588
Active string
589
Issues []db.Issue
···
598
}
599
600
type RepoSingleIssueParams struct {
601
-
LoggedInUser *auth.User
602
RepoInfo repoinfo.RepoInfo
603
Active string
604
Issue db.Issue
···
620
}
621
622
type RepoNewIssueParams struct {
623
-
LoggedInUser *auth.User
624
RepoInfo repoinfo.RepoInfo
625
Active string
626
}
···
631
}
632
633
type EditIssueCommentParams struct {
634
-
LoggedInUser *auth.User
635
RepoInfo repoinfo.RepoInfo
636
Issue *db.Issue
637
Comment *db.Comment
···
642
}
643
644
type SingleIssueCommentParams struct {
645
-
LoggedInUser *auth.User
646
DidHandleMap map[string]string
647
RepoInfo repoinfo.RepoInfo
648
Issue *db.Issue
···
654
}
655
656
type RepoNewPullParams struct {
657
-
LoggedInUser *auth.User
658
RepoInfo repoinfo.RepoInfo
659
Branches []types.Branch
660
Active string
···
666
}
667
668
type RepoPullsParams struct {
669
-
LoggedInUser *auth.User
670
RepoInfo repoinfo.RepoInfo
671
Pulls []*db.Pull
672
Active string
···
698
}
699
700
type RepoSinglePullParams struct {
701
-
LoggedInUser *auth.User
702
RepoInfo repoinfo.RepoInfo
703
Active string
704
DidHandleMap map[string]string
···
713
}
714
715
type RepoPullPatchParams struct {
716
-
LoggedInUser *auth.User
717
DidHandleMap map[string]string
718
RepoInfo repoinfo.RepoInfo
719
Pull *db.Pull
···
728
}
729
730
type RepoPullInterdiffParams struct {
731
-
LoggedInUser *auth.User
732
DidHandleMap map[string]string
733
RepoInfo repoinfo.RepoInfo
734
Pull *db.Pull
···
778
}
779
780
type PullResubmitParams struct {
781
-
LoggedInUser *auth.User
782
RepoInfo repoinfo.RepoInfo
783
Pull *db.Pull
784
SubmissionId int
···
789
}
790
791
type PullActionsParams struct {
792
-
LoggedInUser *auth.User
793
RepoInfo repoinfo.RepoInfo
794
Pull *db.Pull
795
RoundNumber int
···
802
}
803
804
type PullNewCommentParams struct {
805
-
LoggedInUser *auth.User
806
RepoInfo repoinfo.RepoInfo
807
Pull *db.Pull
808
RoundNumber int
···
16
"strings"
17
18
"tangled.sh/tangled.sh/core/appview"
19
"tangled.sh/tangled.sh/core/appview/db"
20
+
"tangled.sh/tangled.sh/core/appview/oauth"
21
"tangled.sh/tangled.sh/core/appview/pages/markup"
22
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
23
"tangled.sh/tangled.sh/core/appview/pagination"
···
48
func NewPages(config *appview.Config) *Pages {
49
// initialized with safe defaults, can be overriden per use
50
rctx := &markup.RenderContext{
51
+
IsDev: config.Core.Dev,
52
+
CamoUrl: config.Camo.Host,
53
+
CamoSecret: config.Camo.SharedSecret,
54
}
55
56
p := &Pages{
57
t: make(map[string]*template.Template),
58
+
dev: config.Core.Dev,
59
embedFS: Files,
60
rctx: rctx,
61
templateDir: "appview/pages",
···
250
}
251
252
type TimelineParams struct {
253
+
LoggedInUser *oauth.User
254
Timeline []db.TimelineEvent
255
DidHandleMap map[string]string
256
}
···
260
}
261
262
type SettingsParams struct {
263
+
LoggedInUser *oauth.User
264
PubKeys []db.PublicKey
265
Emails []db.Email
266
}
···
270
}
271
272
type KnotsParams struct {
273
+
LoggedInUser *oauth.User
274
Registrations []db.Registration
275
}
276
···
279
}
280
281
type KnotParams struct {
282
+
LoggedInUser *oauth.User
283
DidHandleMap map[string]string
284
Registration *db.Registration
285
Members []string
···
291
}
292
293
type NewRepoParams struct {
294
+
LoggedInUser *oauth.User
295
Knots []string
296
}
297
···
300
}
301
302
type ForkRepoParams struct {
303
+
LoggedInUser *oauth.User
304
Knots []string
305
RepoInfo repoinfo.RepoInfo
306
}
···
310
}
311
312
type ProfilePageParams struct {
313
+
LoggedInUser *oauth.User
314
Repos []db.Repo
315
CollaboratingRepos []db.Repo
316
ProfileTimeline *db.ProfileTimeline
317
+
Card ProfileCard
318
319
DidHandleMap map[string]string
320
}
321
322
+
type ProfileCard struct {
323
+
UserDid string
324
+
UserHandle string
325
+
FollowStatus db.FollowStatus
326
+
AvatarUri string
327
+
Followers int
328
+
Following int
329
+
330
+
Profile *db.Profile
331
}
332
333
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
334
return p.execute("user/profile", w, params)
335
}
336
337
+
type ReposPageParams struct {
338
+
LoggedInUser *oauth.User
339
+
Repos []db.Repo
340
+
Card ProfileCard
341
+
342
+
DidHandleMap map[string]string
343
+
}
344
+
345
+
func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error {
346
+
return p.execute("user/repos", w, params)
347
+
}
348
+
349
type FollowFragmentParams struct {
350
UserDid string
351
FollowStatus db.FollowStatus
···
355
return p.executePlain("user/fragments/follow", w, params)
356
}
357
358
+
type EditBioParams struct {
359
+
LoggedInUser *oauth.User
360
+
Profile *db.Profile
361
+
}
362
+
363
+
func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
364
+
return p.executePlain("user/fragments/editBio", w, params)
365
+
}
366
+
367
+
type EditPinsParams struct {
368
+
LoggedInUser *oauth.User
369
+
Profile *db.Profile
370
+
AllRepos []PinnedRepo
371
+
DidHandleMap map[string]string
372
+
}
373
+
374
+
type PinnedRepo struct {
375
+
IsPinned bool
376
+
db.Repo
377
+
}
378
+
379
+
func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
380
+
return p.executePlain("user/fragments/editPins", w, params)
381
+
}
382
+
383
type RepoActionsFragmentParams struct {
384
IsStarred bool
385
RepoAt syntax.ATURI
···
403
}
404
405
type RepoIndexParams struct {
406
+
LoggedInUser *oauth.User
407
RepoInfo repoinfo.RepoInfo
408
Active string
409
TagMap map[string][]string
···
444
}
445
446
type RepoLogParams struct {
447
+
LoggedInUser *oauth.User
448
RepoInfo repoinfo.RepoInfo
449
TagMap map[string][]string
450
types.RepoLogResponse
···
458
}
459
460
type RepoCommitParams struct {
461
+
LoggedInUser *oauth.User
462
RepoInfo repoinfo.RepoInfo
463
Active string
464
EmailToDidOrHandle map[string]string
···
472
}
473
474
type RepoTreeParams struct {
475
+
LoggedInUser *oauth.User
476
RepoInfo repoinfo.RepoInfo
477
Active string
478
BreadCrumbs [][]string
···
508
}
509
510
type RepoBranchesParams struct {
511
+
LoggedInUser *oauth.User
512
RepoInfo repoinfo.RepoInfo
513
Active string
514
types.RepoBranchesResponse
···
520
}
521
522
type RepoTagsParams struct {
523
+
LoggedInUser *oauth.User
524
RepoInfo repoinfo.RepoInfo
525
Active string
526
types.RepoTagsResponse
···
534
}
535
536
type RepoArtifactParams struct {
537
+
LoggedInUser *oauth.User
538
RepoInfo repoinfo.RepoInfo
539
Artifact db.Artifact
540
}
···
544
}
545
546
type RepoBlobParams struct {
547
+
LoggedInUser *oauth.User
548
RepoInfo repoinfo.RepoInfo
549
Active string
550
BreadCrumbs [][]string
···
562
case markup.FormatMarkdown:
563
p.rctx.RepoInfo = params.RepoInfo
564
p.rctx.RendererType = markup.RendererTypeRepoMarkdown
565
+
params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents)))
566
}
567
}
568
···
606
}
607
608
type RepoSettingsParams struct {
609
+
LoggedInUser *oauth.User
610
RepoInfo repoinfo.RepoInfo
611
Collaborators []Collaborator
612
Active string
···
622
}
623
624
type RepoIssuesParams struct {
625
+
LoggedInUser *oauth.User
626
RepoInfo repoinfo.RepoInfo
627
Active string
628
Issues []db.Issue
···
637
}
638
639
type RepoSingleIssueParams struct {
640
+
LoggedInUser *oauth.User
641
RepoInfo repoinfo.RepoInfo
642
Active string
643
Issue db.Issue
···
659
}
660
661
type RepoNewIssueParams struct {
662
+
LoggedInUser *oauth.User
663
RepoInfo repoinfo.RepoInfo
664
Active string
665
}
···
670
}
671
672
type EditIssueCommentParams struct {
673
+
LoggedInUser *oauth.User
674
RepoInfo repoinfo.RepoInfo
675
Issue *db.Issue
676
Comment *db.Comment
···
681
}
682
683
type SingleIssueCommentParams struct {
684
+
LoggedInUser *oauth.User
685
DidHandleMap map[string]string
686
RepoInfo repoinfo.RepoInfo
687
Issue *db.Issue
···
693
}
694
695
type RepoNewPullParams struct {
696
+
LoggedInUser *oauth.User
697
RepoInfo repoinfo.RepoInfo
698
Branches []types.Branch
699
Active string
···
705
}
706
707
type RepoPullsParams struct {
708
+
LoggedInUser *oauth.User
709
RepoInfo repoinfo.RepoInfo
710
Pulls []*db.Pull
711
Active string
···
737
}
738
739
type RepoSinglePullParams struct {
740
+
LoggedInUser *oauth.User
741
RepoInfo repoinfo.RepoInfo
742
Active string
743
DidHandleMap map[string]string
···
752
}
753
754
type RepoPullPatchParams struct {
755
+
LoggedInUser *oauth.User
756
DidHandleMap map[string]string
757
RepoInfo repoinfo.RepoInfo
758
Pull *db.Pull
···
767
}
768
769
type RepoPullInterdiffParams struct {
770
+
LoggedInUser *oauth.User
771
DidHandleMap map[string]string
772
RepoInfo repoinfo.RepoInfo
773
Pull *db.Pull
···
817
}
818
819
type PullResubmitParams struct {
820
+
LoggedInUser *oauth.User
821
RepoInfo repoinfo.RepoInfo
822
Pull *db.Pull
823
SubmissionId int
···
828
}
829
830
type PullActionsParams struct {
831
+
LoggedInUser *oauth.User
832
RepoInfo repoinfo.RepoInfo
833
Pull *db.Pull
834
RoundNumber int
···
841
}
842
843
type PullNewCommentParams struct {
844
+
LoggedInUser *oauth.User
845
RepoInfo repoinfo.RepoInfo
846
Pull *db.Pull
847
RoundNumber int
+1
appview/pages/templates/layouts/base.html
+1
appview/pages/templates/layouts/base.html
···
7
name="viewport"
8
content="width=device-width, initial-scale=1.0"
9
/>
10
+
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
11
<script src="/static/htmx.min.js"></script>
12
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
13
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
+1
-1
appview/pages/templates/repo/fragments/cloneInstructions.html
+1
-1
appview/pages/templates/repo/fragments/cloneInstructions.html
+6
-6
appview/pages/templates/repo/fragments/filetree.html
+6
-6
appview/pages/templates/repo/fragments/filetree.html
···
2
{{ if and .Name .IsDirectory }}
3
<details open>
4
<summary class="cursor-pointer list-none pt-1">
5
-
<span class="inline-flex items-center gap-2 ">
6
-
{{ i "folder" "w-3 h-3 fill-current" }}
7
-
<span class="text-black dark:text-white">{{ .Name }}</span>
8
</span>
9
</summary>
10
<div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700">
···
14
</div>
15
</details>
16
{{ else if .Name }}
17
-
<div class="flex items-center gap-2 pt-1">
18
-
{{ i "file" "w-3 h-3" }}
19
-
<a href="#file-{{ .Path }}" class="text-black dark:text-white no-underline hover:underline">{{ .Name }}</a>
20
</div>
21
{{ else }}
22
{{ range $child := .Children }}
···
2
{{ if and .Name .IsDirectory }}
3
<details open>
4
<summary class="cursor-pointer list-none pt-1">
5
+
<span class="tree-directory inline-flex items-center gap-2 ">
6
+
{{ i "folder" "size-4 fill-current" }}
7
+
<span class="filename text-black dark:text-white">{{ .Name }}</span>
8
</span>
9
</summary>
10
<div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700">
···
14
</div>
15
</details>
16
{{ else if .Name }}
17
+
<div class="tree-file flex items-center gap-2 pt-1">
18
+
{{ i "file" "size-4" }}
19
+
<a href="#file-{{ .Path }}" class="filename text-black dark:text-white no-underline hover:underline">{{ .Name }}</a>
20
</div>
21
{{ else }}
22
{{ range $child := .Children }}
+13
-12
appview/pages/templates/repo/fragments/repoActions.html
+13
-12
appview/pages/templates/repo/fragments/repoActions.html
···
2
<div class="flex items-center gap-2 z-auto">
3
<button
4
id="starBtn"
5
-
class="btn disabled:opacity-50 disabled:cursor-not-allowed"
6
{{ if .IsStarred }}
7
hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
8
{{ else }}
···
14
hx-swap="outerHTML"
15
hx-disabled-elt="#starBtn"
16
>
17
-
<div class="flex gap-2 items-center">
18
-
{{ if .IsStarred }}
19
-
{{ i "star" "w-4 h-4 fill-current" }}
20
-
{{ else }}
21
-
{{ i "star" "w-4 h-4" }}
22
-
{{ end }}
23
-
<span class="text-sm">
24
-
{{ .Stats.StarCount }}
25
-
</span>
26
-
</div>
27
</button>
28
{{ if .DisableFork }}
29
<button
···
36
</button>
37
{{ else }}
38
<a
39
-
class="btn text-sm no-underline hover:no-underline flex items-center gap-2"
40
href="/{{ .FullName }}/fork"
41
>
42
{{ i "git-fork" "w-4 h-4" }}
43
fork
44
</a>
45
{{ end }}
46
</div>
···
2
<div class="flex items-center gap-2 z-auto">
3
<button
4
id="starBtn"
5
+
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
6
{{ if .IsStarred }}
7
hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
8
{{ else }}
···
14
hx-swap="outerHTML"
15
hx-disabled-elt="#starBtn"
16
>
17
+
{{ if .IsStarred }}
18
+
{{ i "star" "w-4 h-4 fill-current" }}
19
+
{{ else }}
20
+
{{ i "star" "w-4 h-4" }}
21
+
{{ end }}
22
+
<span class="text-sm">
23
+
{{ .Stats.StarCount }}
24
+
</span>
25
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
26
</button>
27
{{ if .DisableFork }}
28
<button
···
35
</button>
36
{{ else }}
37
<a
38
+
class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
39
+
hx-boost="true"
40
href="/{{ .FullName }}/fork"
41
>
42
{{ i "git-fork" "w-4 h-4" }}
43
fork
44
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
45
</a>
46
{{ end }}
47
</div>
+3
-3
appview/pages/templates/repo/index.html
+3
-3
appview/pages/templates/repo/index.html
···
103
class="{{ $linkstyle }}"
104
>
105
<div class="flex items-center gap-2">
106
-
{{ i "folder" "w-3 h-3 fill-current" }}
107
{{ .Name }}
108
</div>
109
</a>
···
125
class="{{ $linkstyle }}"
126
>
127
<div class="flex items-center gap-2">
128
-
{{ i "file" "w-3 h-3" }}{{ .Name }}
129
</div>
130
</a>
131
···
320
{{ define "repoAfter" }}
321
{{- if .HTMLReadme }}
322
<section
323
-
class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }}
324
prose dark:prose-invert dark:[&_pre]:bg-gray-900
325
dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900
326
dark:[&_pre]:border dark:[&_pre]:border-gray-700
···
103
class="{{ $linkstyle }}"
104
>
105
<div class="flex items-center gap-2">
106
+
{{ i "folder" "size-4 fill-current" }}
107
{{ .Name }}
108
</div>
109
</a>
···
125
class="{{ $linkstyle }}"
126
>
127
<div class="flex items-center gap-2">
128
+
{{ i "file" "size-4" }}{{ .Name }}
129
</div>
130
</a>
131
···
320
{{ define "repoAfter" }}
321
{{- if .HTMLReadme }}
322
<section
323
+
class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto overflow-auto {{ if not .Raw }}
324
prose dark:prose-invert dark:[&_pre]:bg-gray-900
325
dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900
326
dark:[&_pre]:border dark:[&_pre]:border-gray-700
+26
-17
appview/pages/templates/repo/issues/issues.html
+26
-17
appview/pages/templates/repo/issues/issues.html
···
1
{{ define "title" }}issues · {{ .RepoInfo.FullName }}{{ end }}
2
3
{{ define "repoContent" }}
4
-
<div class="flex justify-between items-center">
5
-
<p>
6
-
filtering
7
-
<select class="border p-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value">
8
-
<option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option>
9
-
<option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option>
10
-
</select>
11
-
issues
12
-
</p>
13
-
<a
14
-
href="/{{ .RepoInfo.FullName }}/issues/new"
15
-
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline">
16
-
{{ i "circle-plus" "w-4 h-4" }}
17
-
<span>new</span>
18
-
</a>
19
-
</div>
20
-
<div class="error" id="issues"></div>
21
{{ end }}
22
23
{{ define "repoAfter" }}
···
1
{{ define "title" }}issues · {{ .RepoInfo.FullName }}{{ end }}
2
3
{{ define "repoContent" }}
4
+
<div class="flex justify-between items-center gap-4">
5
+
<div class="flex gap-4">
6
+
<a
7
+
href="?state=open"
8
+
class="flex items-center gap-2 {{ if .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
9
+
>
10
+
{{ i "circle-dot" "w-4 h-4" }}
11
+
<span>{{ .RepoInfo.Stats.IssueCount.Open }} open</span>
12
+
</a>
13
+
<a
14
+
href="?state=closed"
15
+
class="flex items-center gap-2 {{ if not .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
16
+
>
17
+
{{ i "ban" "w-4 h-4" }}
18
+
<span>{{ .RepoInfo.Stats.IssueCount.Closed }} closed</span>
19
+
</a>
20
+
</div>
21
+
<a
22
+
href="/{{ .RepoInfo.FullName }}/issues/new"
23
+
class="btn text-sm flex items-center justify-center gap-2 no-underline hover:no-underline"
24
+
>
25
+
{{ i "circle-plus" "w-4 h-4" }}
26
+
<span>new</span>
27
+
</a>
28
+
</div>
29
+
<div class="error" id="issues"></div>
30
{{ end }}
31
32
{{ define "repoAfter" }}
+7
-2
appview/pages/templates/repo/new.html
+7
-2
appview/pages/templates/repo/new.html
···
5
<p class="text-xl font-bold dark:text-white">Create a new repository</p>
6
</div>
7
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
8
-
<form hx-post="/repo/new" class="space-y-12" hx-swap="none">
9
<div class="space-y-2">
10
<label for="name" class="-mb-1 dark:text-white">Repository name</label>
11
<input
···
60
</fieldset>
61
62
<div class="space-y-2">
63
-
<button type="submit" class="btn">create repo</button>
64
<div id="repo" class="error"></div>
65
</div>
66
</form>
···
5
<p class="text-xl font-bold dark:text-white">Create a new repository</p>
6
</div>
7
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
8
+
<form hx-post="/repo/new" class="space-y-12" hx-swap="none" hx-indicator="#spinner">
9
<div class="space-y-2">
10
<label for="name" class="-mb-1 dark:text-white">Repository name</label>
11
<input
···
60
</fieldset>
61
62
<div class="space-y-2">
63
+
<button type="submit" class="btn flex gap-2 items-center">
64
+
create repo
65
+
<span id="spinner" class="group">
66
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
67
+
</span>
68
+
</button>
69
<div id="repo" class="error"></div>
70
</div>
71
</form>
+14
-9
appview/pages/templates/repo/pulls/fragments/pullActions.html
+14
-9
appview/pages/templates/repo/pulls/fragments/pullActions.html
···
17
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
18
hx-target="#actions-{{$roundNumber}}"
19
hx-swap="outerHtml"
20
-
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline">
21
{{ i "message-square-plus" "w-4 h-4" }}
22
<span>comment</span>
23
</button>
24
{{ if and $isPushAllowed $isOpen $isLastRound }}
25
{{ $disabled := "" }}
···
30
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
31
hx-swap="none"
32
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
33
-
class="btn p-2 flex items-center gap-2" {{ $disabled }}>
34
{{ i "git-merge" "w-4 h-4" }}
35
-
<span>merge</span>
36
</button>
37
{{ end }}
38
···
51
{{ end }}
52
53
hx-disabled-elt="#resubmitBtn"
54
-
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }}
55
56
{{ if $disabled }}
57
title="Update this branch to resubmit this pull request"
···
59
title="Resubmit this pull request"
60
{{ end }}
61
>
62
-
{{ i "rotate-ccw" "w-4 h-4" }}
63
<span>resubmit</span>
64
</button>
65
{{ end }}
66
···
68
<button
69
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
70
hx-swap="none"
71
-
class="btn p-2 flex items-center gap-2">
72
-
{{ i "ban" "w-4 h-4" }}
73
<span>close</span>
74
</button>
75
{{ end }}
76
···
78
<button
79
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
80
hx-swap="none"
81
-
class="btn p-2 flex items-center gap-2">
82
-
{{ i "refresh-ccw-dot" "w-4 h-4" }}
83
<span>reopen</span>
84
</button>
85
{{ end }}
86
</div>
···
17
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment"
18
hx-target="#actions-{{$roundNumber}}"
19
hx-swap="outerHtml"
20
+
class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group">
21
{{ i "message-square-plus" "w-4 h-4" }}
22
<span>comment</span>
23
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
24
</button>
25
{{ if and $isPushAllowed $isOpen $isLastRound }}
26
{{ $disabled := "" }}
···
31
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
32
hx-swap="none"
33
hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?"
34
+
class="btn p-2 flex items-center gap-2 group" {{ $disabled }}>
35
{{ i "git-merge" "w-4 h-4" }}
36
+
<span>merge</span>
37
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
38
</button>
39
{{ end }}
40
···
53
{{ end }}
54
55
hx-disabled-elt="#resubmitBtn"
56
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }}
57
58
{{ if $disabled }}
59
title="Update this branch to resubmit this pull request"
···
61
title="Resubmit this pull request"
62
{{ end }}
63
>
64
+
{{ i "rotate-ccw" "w-4 h-4" }}
65
<span>resubmit</span>
66
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
67
</button>
68
{{ end }}
69
···
71
<button
72
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close"
73
hx-swap="none"
74
+
class="btn p-2 flex items-center gap-2 group">
75
+
{{ i "ban" "w-4 h-4" }}
76
<span>close</span>
77
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
78
</button>
79
{{ end }}
80
···
82
<button
83
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen"
84
hx-swap="none"
85
+
class="btn p-2 flex items-center gap-2 group">
86
+
{{ i "refresh-ccw-dot" "w-4 h-4" }}
87
<span>reopen</span>
88
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
89
</button>
90
{{ end }}
91
</div>
+9
-11
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+9
-11
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
42
</span>
43
</span>
44
{{ if not .Pull.IsPatchBased }}
45
-
<span>from
46
-
{{ if .Pull.IsForkBased }}
47
-
{{ if .Pull.PullSource.Repo }}
48
-
<a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>
49
-
{{ else }}
50
-
<span class="italic">[deleted fork]</span>
51
-
{{ end }}
52
-
{{ end }}
53
-
54
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
55
-
{{ .Pull.PullSource.Branch }}
56
</span>
57
-
</span>
58
{{ end }}
59
</span>
60
</div>
···
42
</span>
43
</span>
44
{{ if not .Pull.IsPatchBased }}
45
+
from
46
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
47
+
{{ if .Pull.IsForkBased }}
48
+
{{ if .Pull.PullSource.Repo }}
49
+
<a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>:
50
+
{{- else -}}
51
+
<span class="italic">[deleted fork]</span>
52
+
{{- end -}}
53
+
{{- end -}}
54
+
{{- .Pull.PullSource.Branch -}}
55
</span>
56
{{ end }}
57
</span>
58
</div>
+1
-1
appview/pages/templates/repo/pulls/new.html
+1
-1
appview/pages/templates/repo/pulls/new.html
+8
-4
appview/pages/templates/repo/pulls/pull.html
+8
-4
appview/pages/templates/repo/pulls/pull.html
···
51
</span>
52
</div>
53
54
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
55
hx-boost="true"
56
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
57
-
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">view patch</span>
58
</a>
59
{{ if not (eq .RoundNumber 0) }}
60
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2"
61
hx-boost="true"
62
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
63
-
{{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span>
64
</a>
65
<span id="interdiff-error-{{.RoundNumber}}"></span>
66
{{ end }}
···
51
</span>
52
</div>
53
54
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
55
hx-boost="true"
56
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}">
57
+
{{ i "file-diff" "w-4 h-4" }}
58
+
<span class="hidden md:inline">diff</span>
59
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
60
</a>
61
{{ if not (eq .RoundNumber 0) }}
62
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
63
hx-boost="true"
64
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
65
+
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
66
+
<span class="hidden md:inline">interdiff</span>
67
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
68
</a>
69
<span id="interdiff-error-{{.RoundNumber}}"></span>
70
{{ end }}
+32
-29
appview/pages/templates/repo/pulls/pulls.html
+32
-29
appview/pages/templates/repo/pulls/pulls.html
···
2
3
{{ define "repoContent" }}
4
<div class="flex justify-between items-center">
5
-
<p class="dark:text-white">
6
-
filtering
7
-
<select
8
-
class="border p-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white"
9
-
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value"
10
>
11
-
<option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}>
12
-
open ({{ .RepoInfo.Stats.PullCount.Open }})
13
-
</option>
14
-
<option value="merged" {{ if .FilteringBy.IsMerged }}selected{{ end }}>
15
-
merged ({{ .RepoInfo.Stats.PullCount.Merged }})
16
-
</option>
17
-
<option value="closed" {{ if .FilteringBy.IsClosed }}selected{{ end }}>
18
-
closed ({{ .RepoInfo.Stats.PullCount.Closed }})
19
-
</option>
20
-
</select>
21
-
pull requests
22
-
</p>
23
<a
24
href="/{{ .RepoInfo.FullName }}/pulls/new"
25
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"
···
79
</span>
80
</span>
81
{{ if not .IsPatchBased }}
82
-
<span>from
83
-
{{ if .IsForkBased }}
84
-
{{ if .PullSource.Repo }}
85
-
<a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>
86
-
{{ else }}
87
-
<span class="italic">[deleted fork]</span>
88
-
{{ end }}
89
-
{{ end }}
90
-
91
-
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
92
-
{{ .PullSource.Branch }}
93
-
</span>
94
</span>
95
{{ end }}
96
<span class="before:content-['ยท']">
···
2
3
{{ define "repoContent" }}
4
<div class="flex justify-between items-center">
5
+
<div class="flex gap-4">
6
+
<a
7
+
href="?state=open"
8
+
class="flex items-center gap-2 {{ if .FilteringBy.IsOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
9
+
>
10
+
{{ i "git-pull-request" "w-4 h-4" }}
11
+
<span>{{ .RepoInfo.Stats.PullCount.Open }} open</span>
12
+
</a>
13
+
<a
14
+
href="?state=merged"
15
+
class="flex items-center gap-2 {{ if .FilteringBy.IsMerged }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
16
+
>
17
+
{{ i "git-merge" "w-4 h-4" }}
18
+
<span>{{ .RepoInfo.Stats.PullCount.Merged }} merged</span>
19
+
</a>
20
+
<a
21
+
href="?state=closed"
22
+
class="flex items-center gap-2 {{ if .FilteringBy.IsClosed }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}"
23
>
24
+
{{ i "ban" "w-4 h-4" }}
25
+
<span>{{ .RepoInfo.Stats.PullCount.Closed }} closed</span>
26
+
</a>
27
+
</div>
28
<a
29
href="/{{ .RepoInfo.FullName }}/pulls/new"
30
class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"
···
84
</span>
85
</span>
86
{{ if not .IsPatchBased }}
87
+
from
88
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
89
+
{{ if .IsForkBased }}
90
+
{{ if .PullSource.Repo }}
91
+
<a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>:
92
+
{{- else -}}
93
+
<span class="italic">[deleted fork]</span>
94
+
{{- end -}}
95
+
{{- end -}}
96
+
{{- .PullSource.Branch -}}
97
</span>
98
{{ end }}
99
<span class="before:content-['ยท']">
+2
-2
appview/pages/templates/repo/tree.html
+2
-2
appview/pages/templates/repo/tree.html
···
54
<div class="flex justify-between items-center">
55
<a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}">
56
<div class="flex items-center gap-2">
57
-
{{ i "folder" "w-3 h-3 fill-current" }}{{ .Name }}
58
</div>
59
</a>
60
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
···
69
<div class="flex justify-between items-center">
70
<a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}">
71
<div class="flex items-center gap-2">
72
-
{{ i "file" "w-3 h-3" }}{{ .Name }}
73
</div>
74
</a>
75
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
···
54
<div class="flex justify-between items-center">
55
<a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}">
56
<div class="flex items-center gap-2">
57
+
{{ i "folder" "size-4 fill-current" }}{{ .Name }}
58
</div>
59
</a>
60
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
···
69
<div class="flex justify-between items-center">
70
<a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}">
71
<div class="flex items-center gap-2">
72
+
{{ i "file" "size-4" }}{{ .Name }}
73
</div>
74
</a>
75
<time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
+1
-1
appview/pages/templates/timeline.html
+1
-1
appview/pages/templates/timeline.html
···
23
</div>
24
<div class="italic text-lg">
25
tightly-knit social coding, <a href="/login" class="underline inline-flex gap-1 items-center">join now {{ i "arrow-right" "w-4 h-4" }}</a>
26
-
<p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a>or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>.
27
Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p>
28
</div>
29
</div>
···
23
</div>
24
<div class="italic text-lg">
25
tightly-knit social coding, <a href="/login" class="underline inline-flex gap-1 items-center">join now {{ i "arrow-right" "w-4 h-4" }}</a>
26
+
<p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>.
27
Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p>
28
</div>
29
</div>
+6
appview/pages/templates/user/fragments/bluesky.html
+6
appview/pages/templates/user/fragments/bluesky.html
···
···
1
+
{{ define "user/fragments/bluesky" }}
2
+
<svg class="{{.}}" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="-3 -3 30 30">
3
+
<title>Bluesky</title>
4
+
<path fill="none" stroke="currentColor" d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" stroke-width="2.25"/>
5
+
</svg>
6
+
{{ end }}
+111
appview/pages/templates/user/fragments/editBio.html
+111
appview/pages/templates/user/fragments/editBio.html
···
···
1
+
{{ define "user/fragments/editBio" }}
2
+
<form
3
+
hx-post="/profile/bio"
4
+
class="flex flex-col gap-4 my-2 max-w-full"
5
+
hx-disabled-elt="#save-btn,#cancel-btn"
6
+
hx-swap="none"
7
+
hx-indicator="#spinner">
8
+
<div class="flex flex-col gap-1">
9
+
{{ $description := "" }}
10
+
{{ if and .Profile .Profile.Description }}
11
+
{{ $description = .Profile.Description }}
12
+
{{ end }}
13
+
<label class="m-0 p-0" for="description">bio</label>
14
+
<textarea
15
+
type="text"
16
+
class="py-1 px-1 w-full"
17
+
name="description"
18
+
rows="3"
19
+
placeholder="write a bio">{{ $description }}</textarea>
20
+
</div>
21
+
22
+
<div class="flex flex-col gap-1">
23
+
<label class="m-0 p-0" for="location">location</label>
24
+
<div class="flex items-center gap-2 w-full">
25
+
{{ $location := "" }}
26
+
{{ if and .Profile .Profile.Location }}
27
+
{{ $location = .Profile.Location }}
28
+
{{ end }}
29
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
30
+
<input type="text" class="py-1 px-1 w-full" name="location" value="{{ $location }}">
31
+
</div>
32
+
</div>
33
+
34
+
<div class="flex flex-col gap-1">
35
+
<label class="m-0 p-0">social links</label>
36
+
<div class="flex items-center gap-2 py-1">
37
+
{{ $includeBsky := false }}
38
+
{{ if and .Profile .Profile.IncludeBluesky }}
39
+
{{ $includeBsky = true }}
40
+
{{ end }}
41
+
<input type="checkbox" id="includeBluesky" name="includeBluesky" value="on" {{if $includeBsky}}checked{{end}}>
42
+
<label for="includeBluesky" class="my-0 py-0 normal-case font-normal">Link to Bluesky account</label>
43
+
</div>
44
+
45
+
{{ $profile := .Profile }}
46
+
{{ range $idx, $s := (sequence 5) }}
47
+
{{ $link := "" }}
48
+
{{ if and $profile $profile.Links }}
49
+
{{ if lt $idx (len $profile.Links) }}
50
+
{{ $link = index $profile.Links $idx }}
51
+
{{ end }}
52
+
{{ end }}
53
+
54
+
<div class="flex items-center gap-2 w-full">
55
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
56
+
<input type="text" class="py-1 px-1 w-full" name="link{{$idx}}" value="{{ $link }}" placeholder="social link {{add $idx 1}}">
57
+
</div>
58
+
{{ end }}
59
+
</div>
60
+
61
+
<div class="flex flex-col gap-1">
62
+
<label class="m-0 p-0">vanity stats</label>
63
+
{{ range $idx, $s := (sequence 2) }}
64
+
{{ $stat := "" }}
65
+
{{ if and $profile $profile.Stats }}
66
+
{{ if lt $idx (len $profile.Stats) }}
67
+
{{ $s := index $profile.Stats $idx }}
68
+
{{ $stat = $s.Kind }}
69
+
{{ end }}
70
+
{{ end }}
71
+
72
+
{{ block "stat" (list $idx $stat) }} {{ end }}
73
+
{{ end }}
74
+
</div>
75
+
76
+
<div class="flex items-center gap-2 justify-between">
77
+
<button id="save-btn" type="submit" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm">
78
+
{{ i "check" "size-4" }} save
79
+
<span id="spinner" class="group">
80
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
81
+
</span>
82
+
</button>
83
+
<a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline">
84
+
<button id="cancel-btn" type="button" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm">
85
+
{{ i "x" "size-4" }} cancel
86
+
</button>
87
+
</a>
88
+
</div>
89
+
</form>
90
+
{{ end }}
91
+
92
+
{{ define "stat" }}
93
+
{{ $id := index . 0 }}
94
+
{{ $stat := index . 1 }}
95
+
<select class="stat-group w-full p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700 text-sm" id="stat{{$id}}" name="stat{{$id}}">
96
+
<option value="">choose stat</option>
97
+
{{ $stats := assoc
98
+
"merged-pull-request-count" "Merged PR Count"
99
+
"closed-pull-request-count" "Closed PR Count"
100
+
"open-pull-request-count" "Open PR Count"
101
+
"open-issue-count" "Open Issue Count"
102
+
"closed-issue-count" "Closed Issue Count"
103
+
"repository-count" "Repository Count"
104
+
}}
105
+
{{ range $s := $stats }}
106
+
{{ $value := index $s 0 }}
107
+
{{ $label := index $s 1 }}
108
+
<option value="{{ $value }}"{{ if eq $stat $value }} selected{{ end }}>{{ $label }}</option>
109
+
{{ end }}
110
+
</select>
111
+
{{ end }}
+42
appview/pages/templates/user/fragments/editPins.html
+42
appview/pages/templates/user/fragments/editPins.html
···
···
1
+
{{ define "user/fragments/editPins" }}
2
+
{{ $profile := .Profile }}
3
+
<form
4
+
hx-post="/profile/pins"
5
+
hx-disabled-elt="#save-btn,#cancel-btn"
6
+
hx-swap="none"
7
+
hx-indicator="#spinner">
8
+
<div class="flex items-center justify-between mb-2">
9
+
<p class="text-sm font-bold p-2 dark:text-white">SELECT PINNED REPOS</p>
10
+
<div class="flex items-center gap-2">
11
+
<button id="save-btn" type="submit" class="btn px-2 flex items-center gap-2 no-underline text-sm">
12
+
{{ i "check" "w-3 h-3" }} save
13
+
<span id="spinner" class="group">
14
+
{{ i "loader-circle" "w-3 h-3 animate-spin hidden group-[.htmx-request]:inline" }}
15
+
</span>
16
+
</button>
17
+
<a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline">
18
+
<button id="cancel-btn" type="button" class="btn px-2 w-full flex items-center gap-2 no-underline text-sm">
19
+
{{ i "x" "w-3 h-3" }} cancel
20
+
</button>
21
+
</a>
22
+
</div>
23
+
</div>
24
+
<div id="repos" class="grid grid-cols-1 gap-1 mb-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
25
+
{{ range $idx, $r := .AllRepos }}
26
+
<div class="flex items-center gap-2 text-base p-2 border-b border-gray-200 dark:border-gray-700">
27
+
<input type="checkbox" id="repo-{{$idx}}" name="pinnedRepo{{$idx}}" value="{{.RepoAt}}" {{if .IsPinned}}checked{{end}}>
28
+
<label for="repo-{{$idx}}" class="my-0 py-0 normal-case font-normal w-full">
29
+
<div class="flex justify-between items-center w-full">
30
+
<span class="flex-shrink-0 overflow-hidden text-ellipsis ">{{ index $.DidHandleMap .Did }}/{{.Name}}</span>
31
+
<div class="flex gap-1 items-center">
32
+
{{ i "star" "size-4 fill-current" }}
33
+
<span>{{ .RepoStats.StarCount }}</span>
34
+
</div>
35
+
</div>
36
+
</label>
37
+
</div>
38
+
{{ end }}
39
+
</div>
40
+
41
+
</form>
42
+
{{ end }}
+97
appview/pages/templates/user/fragments/profileCard.html
+97
appview/pages/templates/user/fragments/profileCard.html
···
···
1
+
{{ define "user/fragments/profileCard" }}
2
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
{{ if .AvatarUri }}
6
+
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
7
+
{{ end }}
8
+
</div>
9
+
<div class="col-span-2">
10
+
<p title="{{ didOrHandle .UserDid .UserHandle }}"
11
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
12
+
{{ didOrHandle .UserDid .UserHandle }}
13
+
</p>
14
+
15
+
<div class="md:hidden">
16
+
{{ block "followerFollowing" (list .Followers .Following) }} {{ end }}
17
+
</div>
18
+
</div>
19
+
<div class="col-span-3 md:col-span-full">
20
+
<div id="profile-bio" class="text-sm">
21
+
{{ $profile := .Profile }}
22
+
{{ with .Profile }}
23
+
24
+
{{ if .Description }}
25
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
26
+
{{ end }}
27
+
28
+
<div class="hidden md:block">
29
+
{{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }}
30
+
</div>
31
+
32
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
33
+
{{ if .Location }}
34
+
<div class="flex items-center gap-2">
35
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
36
+
<span>{{ .Location }}</span>
37
+
</div>
38
+
{{ end }}
39
+
{{ if .IncludeBluesky }}
40
+
<div class="flex items-center gap-2">
41
+
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span>
42
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ didOrHandle $.UserDid $.UserHandle }}</a>
43
+
</div>
44
+
{{ end }}
45
+
{{ range $link := .Links }}
46
+
{{ if $link }}
47
+
<div class="flex items-center gap-2">
48
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
49
+
<a href="{{ $link }}">{{ $link }}</a>
50
+
</div>
51
+
{{ end }}
52
+
{{ end }}
53
+
{{ if not $profile.IsStatsEmpty }}
54
+
<div class="flex items-center justify-evenly gap-2 py-2">
55
+
{{ range $stat := .Stats }}
56
+
{{ if $stat.Kind }}
57
+
<div class="flex flex-col items-center gap-2">
58
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
59
+
<span>{{ $stat.Kind.String }}</span>
60
+
</div>
61
+
{{ end }}
62
+
{{ end }}
63
+
</div>
64
+
{{ end }}
65
+
</div>
66
+
{{ end }}
67
+
{{ if ne .FollowStatus.String "IsSelf" }}
68
+
{{ template "user/fragments/follow" . }}
69
+
{{ else }}
70
+
<button id="editBtn"
71
+
class="btn mt-2 w-full flex items-center gap-2 group"
72
+
hx-target="#profile-bio"
73
+
hx-get="/profile/edit-bio"
74
+
hx-swap="innerHTML">
75
+
{{ i "pencil" "w-4 h-4" }}
76
+
edit
77
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
78
+
</button>
79
+
{{ end }}
80
+
</div>
81
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
82
+
</div>
83
+
</div>
84
+
</div>
85
+
{{ end }}
86
+
87
+
{{ define "followerFollowing" }}
88
+
{{ $followers := index . 0 }}
89
+
{{ $following := index . 1 }}
90
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
91
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
92
+
<span id="followers">{{ $followers }} followers</span>
93
+
<span class="select-none after:content-['ยท']"></span>
94
+
<span id="following">{{ $following }} following</span>
95
+
</div>
96
+
{{ end }}
97
+
+22
-33
appview/pages/templates/user/login.html
+22
-33
appview/pages/templates/user/login.html
···
8
content="width=device-width, initial-scale=1.0"
9
/>
10
<script src="/static/htmx.min.js"></script>
11
-
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
<title>login</title>
13
</head>
14
<body class="flex items-center justify-center min-h-screen">
15
-
<main class="max-w-7xl px-6 -mt-4">
16
-
<h1 class="text-center text-2xl font-semibold italic dark:text-white">
17
tangled
18
</h1>
19
<h2 class="text-center text-xl italic dark:text-white">
20
tightly-knit social coding.
21
</h2>
22
<form
23
-
class="w-full mt-4"
24
hx-post="/login"
25
hx-swap="none"
26
-
hx-disabled-elt="this"
27
>
28
<div class="flex flex-col">
29
<label for="handle">handle</label>
30
-
<input
31
-
type="text"
32
-
id="handle"
33
-
name="handle"
34
-
tabindex="1"
35
-
required
36
-
/>
37
-
<span class="text-xs text-gray-500 mt-1">
38
-
You need to use your
39
-
<a href="https://bsky.app">Bluesky</a> handle to log
40
-
in.
41
-
</span>
42
-
</div>
43
-
44
-
<div class="flex flex-col mt-2">
45
-
<label for="app_password">app password</label>
46
<input
47
-
type="password"
48
-
id="app_password"
49
-
name="app_password"
50
-
tabindex="2"
51
required
52
/>
53
-
<span class="text-xs text-gray-500 mt-1">
54
-
Generate an app password
55
-
<a
56
-
href="https://bsky.app/settings/app-passwords"
57
-
target="_blank"
58
-
>here</a
59
-
>.
60
</span>
61
</div>
62
···
70
</button>
71
</form>
72
<p class="text-sm text-gray-500">
73
-
Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel:
74
<a href="https://web.libera.chat/#tangled"
75
><code>#tangled</code> on Libera Chat</a
76
>.
···
8
content="width=device-width, initial-scale=1.0"
9
/>
10
<script src="/static/htmx.min.js"></script>
11
+
<link
12
+
rel="stylesheet"
13
+
href="/static/tw.css?{{ cssContentHash }}"
14
+
type="text/css"
15
+
/>
16
<title>login</title>
17
</head>
18
<body class="flex items-center justify-center min-h-screen">
19
+
<main class="max-w-md px-6 -mt-4">
20
+
<h1
21
+
class="text-center text-2xl font-semibold italic dark:text-white"
22
+
>
23
tangled
24
</h1>
25
<h2 class="text-center text-xl italic dark:text-white">
26
tightly-knit social coding.
27
</h2>
28
<form
29
+
class="mt-4 max-w-sm mx-auto"
30
hx-post="/login"
31
hx-swap="none"
32
+
hx-disabled-elt="#login-button"
33
>
34
<div class="flex flex-col">
35
<label for="handle">handle</label>
36
<input
37
+
type="text"
38
+
id="handle"
39
+
name="handle"
40
+
tabindex="1"
41
required
42
/>
43
+
<span class="text-sm text-gray-500 mt-1">
44
+
Use your
45
+
<a href="https://bsky.app">Bluesky</a> handle to log
46
+
in. You will then be redirected to your PDS to
47
+
complete authentication.
48
</span>
49
</div>
50
···
58
</button>
59
</form>
60
<p class="text-sm text-gray-500">
61
+
Join our <a href="https://chat.tangled.sh">Discord</a> or
62
+
IRC channel:
63
<a href="https://web.libera.chat/#tangled"
64
><code>#tangled</code> on Libera Chat</a
65
>.
+80
-91
appview/pages/templates/user/profile.html
+80
-91
appview/pages/templates/user/profile.html
···
1
-
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
2
3
{{ define "content" }}
4
-
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
5
-
<div class="md:col-span-1 order-1 md:order-1">
6
-
{{ block "profileCard" . }}{{ end }}
7
</div>
8
-
<div class="md:col-span-2 order-2 md:order-2">
9
{{ block "ownRepos" . }}{{ end }}
10
{{ block "collaboratingRepos" . }}{{ end }}
11
</div>
12
-
<div class="md:col-span-2 order-3 md:order-3">
13
{{ block "profileTimeline" . }}{{ end }}
14
</div>
15
</div>
16
{{ end }}
17
18
{{ define "profileTimeline" }}
19
-
<p class="text-sm font-bold py-2 dark:text-white px-6">ACTIVITY</p>
20
<div class="flex flex-col gap-6 relative">
21
{{ with .ProfileTimeline }}
22
{{ range $idx, $byMonth := .ByMonth }}
···
225
{{ end }}
226
{{ end }}
227
228
-
{{ define "profileCard" }}
229
-
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
230
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-3 items-center">
231
-
<div id="avatar" class="col-span-1 md-col-span-full flex justify-center items-center">
232
-
{{ if .AvatarUri }}
233
-
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
234
-
{{ end }}
235
</div>
236
-
<div id="text" class="col-span-2 md:col-span-full">
237
-
<p
238
-
title="{{ didOrHandle .UserDid .UserHandle }}"
239
-
class="text-lg font-bold md:text-center dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
240
-
{{ didOrHandle .UserDid .UserHandle }}
241
-
</p>
242
-
<div class="text-sm md:text-center dark:text-gray-300">
243
-
<span id="followers">{{ .ProfileStats.Followers }} followers</span>
244
-
<span class="px-1 select-none after:content-['ยท']"></span>
245
-
<span id="following">{{ .ProfileStats.Following }} following</span>
246
-
</div>
247
-
248
-
{{ if ne .FollowStatus.String "IsSelf" }}
249
-
{{ template "user/fragments/follow" . }}
250
-
{{ end }}
251
-
</div>
252
-
</div>
253
</div>
254
{{ end }}
255
256
-
{{ define "ownRepos" }}
257
-
<p class="text-sm font-bold py-2 px-6 dark:text-white">REPOS</p>
258
-
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
259
-
{{ range .Repos }}
260
-
<div
261
-
id="repo-card"
262
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"
263
-
>
264
-
<div id="repo-card-name" class="font-medium dark:text-white">
265
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"
266
-
>{{ .Name }}</a
267
-
>
268
-
</div>
269
-
{{ if .Description }}
270
-
<div class="text-gray-600 dark:text-gray-300 text-sm">
271
-
{{ .Description }}
272
-
</div>
273
-
{{ end }}
274
-
<div
275
-
class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"
276
-
>
277
-
278
-
{{ if .RepoStats.StarCount }}
279
-
<div class="flex gap-1 items-center text-sm">
280
-
{{ i "star" "w-3 h-3 fill-current" }}
281
-
<span>{{ .RepoStats.StarCount }}</span>
282
-
</div>
283
-
{{ end }}
284
-
</div>
285
</div>
286
-
{{ else }}
287
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
288
-
{{ end }}
289
-
</div>
290
-
291
-
<p class="text-sm font-bold py-2 px-6 dark:text-white">COLLABORATING ON</p>
292
-
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
293
-
{{ range .CollaboratingRepos }}
294
-
<div
295
-
id="repo-card"
296
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col"
297
-
>
298
-
<div id="repo-card-name" class="font-medium dark:text-white">
299
-
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
300
-
{{ index $.DidHandleMap .Did }}/{{ .Name }}
301
-
</a>
302
</div>
303
-
{{ if .Description }}
304
-
<div class="text-gray-600 dark:text-gray-300 text-sm">
305
-
{{ .Description }}
306
</div>
307
{{ end }}
308
-
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
309
-
310
-
{{ if .RepoStats.StarCount }}
311
-
<div class="flex gap-1 items-center text-sm">
312
-
{{ i "star" "w-3 h-3 fill-current" }}
313
-
<span>{{ .RepoStats.StarCount }}</span>
314
-
</div>
315
-
{{ end }}
316
-
</div>
317
</div>
318
-
{{ else }}
319
-
<p class="px-6 dark:text-white">This user is not collaborating.</p>
320
-
{{ end }}
321
</div>
322
{{ end }}
···
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
2
3
{{ define "content" }}
4
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
+
<div class="md:col-span-2 order-1 md:order-1">
6
+
{{ template "user/fragments/profileCard" .Card }}
7
</div>
8
+
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
9
{{ block "ownRepos" . }}{{ end }}
10
{{ block "collaboratingRepos" . }}{{ end }}
11
</div>
12
+
<div class="md:col-span-3 order-3 md:order-3">
13
{{ block "profileTimeline" . }}{{ end }}
14
</div>
15
</div>
16
{{ end }}
17
18
{{ define "profileTimeline" }}
19
+
<p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p>
20
<div class="flex flex-col gap-6 relative">
21
{{ with .ProfileTimeline }}
22
{{ range $idx, $byMonth := .ByMonth }}
···
225
{{ end }}
226
{{ end }}
227
228
+
{{ define "ownRepos" }}
229
+
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
230
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
231
+
class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group">
232
+
<span>PINNED REPOS</span>
233
+
<span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
234
+
view all {{ i "chevron-right" "w-4 h-4" }}
235
+
</span>
236
+
</a>
237
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
238
+
<button
239
+
hx-get="profile/edit-pins"
240
+
hx-target="#all-repos"
241
+
class="btn font-normal text-sm flex gap-2 items-center group">
242
+
{{ i "pencil" "w-3 h-3" }}
243
+
edit
244
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
245
+
</button>
246
+
{{ end }}
247
+
</div>
248
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
249
+
{{ range .Repos }}
250
+
<div
251
+
id="repo-card"
252
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
253
+
<div id="repo-card-name" class="font-medium">
254
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
255
+
>{{ .Name }}</a
256
+
>
257
+
</div>
258
+
{{ if .Description }}
259
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
260
+
{{ .Description }}
261
+
</div>
262
+
{{ end }}
263
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
264
+
{{ if .RepoStats.StarCount }}
265
+
<div class="flex gap-1 items-center text-sm">
266
+
{{ i "star" "w-3 h-3 fill-current" }}
267
+
<span>{{ .RepoStats.StarCount }}</span>
268
+
</div>
269
+
{{ end }}
270
+
</div>
271
</div>
272
+
{{ else }}
273
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
274
+
{{ end }}
275
</div>
276
{{ end }}
277
278
+
{{ define "collaboratingRepos" }}
279
+
{{ if gt (len .CollaboratingRepos) 0 }}
280
+
<p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p>
281
+
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
282
+
{{ range .CollaboratingRepos }}
283
+
<div
284
+
id="repo-card"
285
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col">
286
+
<div id="repo-card-name" class="font-medium dark:text-white">
287
+
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
288
+
{{ index $.DidHandleMap .Did }}/{{ .Name }}
289
+
</a>
290
</div>
291
+
{{ if .Description }}
292
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
293
+
{{ .Description }}
294
</div>
295
+
{{ end }}
296
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
297
+
298
+
{{ if .RepoStats.StarCount }}
299
+
<div class="flex gap-1 items-center text-sm">
300
+
{{ i "star" "w-3 h-3 fill-current" }}
301
+
<span>{{ .RepoStats.StarCount }}</span>
302
</div>
303
{{ end }}
304
</div>
305
+
</div>
306
+
{{ else }}
307
+
<p class="px-6 dark:text-white">This user is not collaborating.</p>
308
+
{{ end }}
309
</div>
310
+
{{ end }}
311
{{ end }}
+44
appview/pages/templates/user/repos.html
+44
appview/pages/templates/user/repos.html
···
···
1
+
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
2
+
3
+
{{ define "content" }}
4
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
5
+
<div class="md:col-span-2 order-1 md:order-1">
6
+
{{ template "user/fragments/profileCard" .Card }}
7
+
</div>
8
+
<div id="all-repos" class="md:col-span-6 order-2 md:order-2">
9
+
{{ block "ownRepos" . }}{{ end }}
10
+
</div>
11
+
</div>
12
+
{{ end }}
13
+
14
+
{{ define "ownRepos" }}
15
+
<p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p>
16
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
17
+
{{ range .Repos }}
18
+
<div
19
+
id="repo-card"
20
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
21
+
<div id="repo-card-name" class="font-medium">
22
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
23
+
>{{ .Name }}</a
24
+
>
25
+
</div>
26
+
{{ if .Description }}
27
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
28
+
{{ .Description }}
29
+
</div>
30
+
{{ end }}
31
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
32
+
{{ if .RepoStats.StarCount }}
33
+
<div class="flex gap-1 items-center text-sm">
34
+
{{ i "star" "w-3 h-3 fill-current" }}
35
+
<span>{{ .RepoStats.StarCount }}</span>
36
+
</div>
37
+
{{ end }}
38
+
</div>
39
+
</div>
40
+
{{ else }}
41
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
42
+
{{ end }}
43
+
</div>
44
+
{{ end }}
+27
-18
appview/settings/settings.go
+27
-18
appview/settings/settings.go
···
13
"github.com/go-chi/chi/v5"
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview"
16
-
"tangled.sh/tangled.sh/core/appview/auth"
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/email"
19
"tangled.sh/tangled.sh/core/appview/middleware"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
22
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
27
28
type Settings struct {
29
Db *db.DB
30
-
Auth *auth.Auth
31
Pages *pages.Pages
32
Config *appview.Config
33
}
···
35
func (s *Settings) Router() http.Handler {
36
r := chi.NewRouter()
37
38
-
r.Use(middleware.AuthMiddleware(s.Auth))
39
40
r.Get("/", s.settings)
41
···
56
}
57
58
func (s *Settings) settings(w http.ResponseWriter, r *http.Request) {
59
-
user := s.Auth.GetUser(r)
60
pubKeys, err := db.GetPublicKeys(s.Db, user.Did)
61
if err != nil {
62
log.Println(err)
···
79
verifyURL := s.verifyUrl(did, emailAddr, code)
80
81
return email.Email{
82
-
APIKey: s.Config.ResendApiKey,
83
From: "noreply@notifs.tangled.sh",
84
To: emailAddr,
85
Subject: "Verify your Tangled email",
···
111
log.Println("unimplemented")
112
return
113
case http.MethodPut:
114
-
did := s.Auth.GetDid(r)
115
emAddr := r.FormValue("email")
116
emAddr = strings.TrimSpace(emAddr)
117
···
174
s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
175
return
176
case http.MethodDelete:
177
-
did := s.Auth.GetDid(r)
178
emailAddr := r.FormValue("email")
179
emailAddr = strings.TrimSpace(emailAddr)
180
···
207
208
func (s *Settings) verifyUrl(did string, email string, code string) string {
209
var appUrl string
210
-
if s.Config.Dev {
211
-
appUrl = "http://" + s.Config.ListenAddr
212
} else {
213
appUrl = "https://tangled.sh"
214
}
···
252
return
253
}
254
255
-
did := s.Auth.GetDid(r)
256
emAddr := r.FormValue("email")
257
emAddr = strings.TrimSpace(emAddr)
258
···
323
}
324
325
func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
326
-
did := s.Auth.GetDid(r)
327
emailAddr := r.FormValue("email")
328
emailAddr = strings.TrimSpace(emailAddr)
329
···
348
log.Println("unimplemented")
349
return
350
case http.MethodPut:
351
-
did := s.Auth.GetDid(r)
352
key := r.FormValue("key")
353
key = strings.TrimSpace(key)
354
name := r.FormValue("name")
355
-
client, _ := s.Auth.AuthorizedClient(r)
356
357
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
358
if err != nil {
359
log.Printf("parsing public key: %s", err)
360
s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
···
378
}
379
380
// store in pds too
381
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
382
Collection: tangled.PublicKeyNSID,
383
Repo: did,
384
Rkey: rkey,
···
409
return
410
411
case http.MethodDelete:
412
-
did := s.Auth.GetDid(r)
413
q := r.URL.Query()
414
415
name := q.Get("name")
···
420
log.Println(rkey)
421
log.Println(key)
422
423
-
client, _ := s.Auth.AuthorizedClient(r)
424
425
if err := db.DeletePublicKey(s.Db, did, name, key); err != nil {
426
log.Printf("removing public key: %s", err)
···
430
431
if rkey != "" {
432
// remove from pds too
433
-
_, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
434
Collection: tangled.PublicKeyNSID,
435
Repo: did,
436
Rkey: rkey,
···
13
"github.com/go-chi/chi/v5"
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview"
16
"tangled.sh/tangled.sh/core/appview/db"
17
"tangled.sh/tangled.sh/core/appview/email"
18
"tangled.sh/tangled.sh/core/appview/middleware"
19
+
"tangled.sh/tangled.sh/core/appview/oauth"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
22
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
27
28
type Settings struct {
29
Db *db.DB
30
+
OAuth *oauth.OAuth
31
Pages *pages.Pages
32
Config *appview.Config
33
}
···
35
func (s *Settings) Router() http.Handler {
36
r := chi.NewRouter()
37
38
+
r.Use(middleware.AuthMiddleware(s.OAuth))
39
40
r.Get("/", s.settings)
41
···
56
}
57
58
func (s *Settings) settings(w http.ResponseWriter, r *http.Request) {
59
+
user := s.OAuth.GetUser(r)
60
pubKeys, err := db.GetPublicKeys(s.Db, user.Did)
61
if err != nil {
62
log.Println(err)
···
79
verifyURL := s.verifyUrl(did, emailAddr, code)
80
81
return email.Email{
82
+
APIKey: s.Config.Resend.ApiKey,
83
From: "noreply@notifs.tangled.sh",
84
To: emailAddr,
85
Subject: "Verify your Tangled email",
···
111
log.Println("unimplemented")
112
return
113
case http.MethodPut:
114
+
did := s.OAuth.GetDid(r)
115
emAddr := r.FormValue("email")
116
emAddr = strings.TrimSpace(emAddr)
117
···
174
s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.")
175
return
176
case http.MethodDelete:
177
+
did := s.OAuth.GetDid(r)
178
emailAddr := r.FormValue("email")
179
emailAddr = strings.TrimSpace(emailAddr)
180
···
207
208
func (s *Settings) verifyUrl(did string, email string, code string) string {
209
var appUrl string
210
+
if s.Config.Core.Dev {
211
+
appUrl = "http://" + s.Config.Core.ListenAddr
212
} else {
213
appUrl = "https://tangled.sh"
214
}
···
252
return
253
}
254
255
+
did := s.OAuth.GetDid(r)
256
emAddr := r.FormValue("email")
257
emAddr = strings.TrimSpace(emAddr)
258
···
323
}
324
325
func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) {
326
+
did := s.OAuth.GetDid(r)
327
emailAddr := r.FormValue("email")
328
emailAddr = strings.TrimSpace(emailAddr)
329
···
348
log.Println("unimplemented")
349
return
350
case http.MethodPut:
351
+
did := s.OAuth.GetDid(r)
352
key := r.FormValue("key")
353
key = strings.TrimSpace(key)
354
name := r.FormValue("name")
355
+
client, err := s.OAuth.AuthorizedClient(r)
356
+
if err != nil {
357
+
s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.")
358
+
return
359
+
}
360
361
+
_, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key))
362
if err != nil {
363
log.Printf("parsing public key: %s", err)
364
s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.")
···
382
}
383
384
// store in pds too
385
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
386
Collection: tangled.PublicKeyNSID,
387
Repo: did,
388
Rkey: rkey,
···
413
return
414
415
case http.MethodDelete:
416
+
did := s.OAuth.GetDid(r)
417
q := r.URL.Query()
418
419
name := q.Get("name")
···
424
log.Println(rkey)
425
log.Println(key)
426
427
+
client, err := s.OAuth.AuthorizedClient(r)
428
+
if err != nil {
429
+
log.Printf("failed to authorize client: %s", err)
430
+
s.Pages.Notice(w, "settings-keys", "Failed to authorize client.")
431
+
return
432
+
}
433
434
if err := db.DeletePublicKey(s.Db, did, name, key); err != nil {
435
log.Printf("removing public key: %s", err)
···
439
440
if rkey != "" {
441
// remove from pds too
442
+
_, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
443
Collection: tangled.PublicKeyNSID,
444
Repo: did,
445
Rkey: rkey,
+20
-10
appview/state/artifact.go
+20
-10
appview/state/artifact.go
···
16
"tangled.sh/tangled.sh/core/api/tangled"
17
"tangled.sh/tangled.sh/core/appview"
18
"tangled.sh/tangled.sh/core/appview/db"
19
"tangled.sh/tangled.sh/core/appview/pages"
20
"tangled.sh/tangled.sh/core/types"
21
)
22
23
// TODO: proper statuses here on early exit
24
func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) {
25
-
user := s.auth.GetUser(r)
26
tagParam := chi.URLParam(r, "tag")
27
f, err := s.fullyResolvedRepo(r)
28
if err != nil {
···
46
}
47
defer file.Close()
48
49
-
client, _ := s.auth.AuthorizedClient(r)
50
51
-
uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file)
52
if err != nil {
53
log.Println("failed to upload blob", err)
54
s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
···
60
rkey := appview.TID()
61
createdAt := time.Now()
62
63
-
putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
64
Collection: tangled.RepoArtifactNSID,
65
Repo: user.Did,
66
Rkey: rkey,
···
140
return
141
}
142
143
-
client, _ := s.auth.AuthorizedClient(r)
144
145
artifacts, err := db.GetArtifact(
146
s.db,
···
159
160
artifact := artifacts[0]
161
162
-
getBlobResp, err := comatproto.SyncGetBlob(r.Context(), client, artifact.BlobCid.String(), artifact.Did)
163
if err != nil {
164
log.Println("failed to get blob from pds", err)
165
return
···
171
172
// TODO: proper statuses here on early exit
173
func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
174
-
user := s.auth.GetUser(r)
175
tagParam := chi.URLParam(r, "tag")
176
filename := chi.URLParam(r, "file")
177
f, err := s.fullyResolvedRepo(r)
···
180
return
181
}
182
183
-
client, _ := s.auth.AuthorizedClient(r)
184
185
tag := plumbing.NewHash(tagParam)
186
···
208
return
209
}
210
211
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
212
Collection: tangled.RepoArtifactNSID,
213
Repo: user.Did,
214
Rkey: artifact.Rkey,
···
254
return nil, err
255
}
256
257
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
258
if err != nil {
259
return nil, err
260
}
···
16
"tangled.sh/tangled.sh/core/api/tangled"
17
"tangled.sh/tangled.sh/core/appview"
18
"tangled.sh/tangled.sh/core/appview/db"
19
+
"tangled.sh/tangled.sh/core/appview/knotclient"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
"tangled.sh/tangled.sh/core/types"
22
)
23
24
// TODO: proper statuses here on early exit
25
func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) {
26
+
user := s.oauth.GetUser(r)
27
tagParam := chi.URLParam(r, "tag")
28
f, err := s.fullyResolvedRepo(r)
29
if err != nil {
···
47
}
48
defer file.Close()
49
50
+
client, err := s.oauth.AuthorizedClient(r)
51
+
if err != nil {
52
+
log.Println("failed to get authorized client", err)
53
+
s.pages.Notice(w, "upload", "failed to get authorized client")
54
+
return
55
+
}
56
57
+
uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file)
58
if err != nil {
59
log.Println("failed to upload blob", err)
60
s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
···
66
rkey := appview.TID()
67
createdAt := time.Now()
68
69
+
putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
70
Collection: tangled.RepoArtifactNSID,
71
Repo: user.Did,
72
Rkey: rkey,
···
146
return
147
}
148
149
+
client, err := s.oauth.AuthorizedClient(r)
150
+
if err != nil {
151
+
log.Println("failed to get authorized client", err)
152
+
return
153
+
}
154
155
artifacts, err := db.GetArtifact(
156
s.db,
···
169
170
artifact := artifacts[0]
171
172
+
getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did)
173
if err != nil {
174
log.Println("failed to get blob from pds", err)
175
return
···
181
182
// TODO: proper statuses here on early exit
183
func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
184
+
user := s.oauth.GetUser(r)
185
tagParam := chi.URLParam(r, "tag")
186
filename := chi.URLParam(r, "file")
187
f, err := s.fullyResolvedRepo(r)
···
190
return
191
}
192
193
+
client, _ := s.oauth.AuthorizedClient(r)
194
195
tag := plumbing.NewHash(tagParam)
196
···
218
return
219
}
220
221
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
222
Collection: tangled.RepoArtifactNSID,
223
Repo: user.Did,
224
Rkey: artifact.Rkey,
···
264
return nil, err
265
}
266
267
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
268
if err != nil {
269
return nil, err
270
}
+8
-4
appview/state/follow.go
+8
-4
appview/state/follow.go
···
14
)
15
16
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
17
-
currentUser := s.auth.GetUser(r)
18
19
subject := r.URL.Query().Get("subject")
20
if subject == "" {
···
32
return
33
}
34
35
-
client, _ := s.auth.AuthorizedClient(r)
36
37
switch r.Method {
38
case http.MethodPost:
39
createdAt := time.Now().Format(time.RFC3339)
40
rkey := appview.TID()
41
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
42
Collection: tangled.GraphFollowNSID,
43
Repo: currentUser.Did,
44
Rkey: rkey,
···
75
return
76
}
77
78
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
79
Collection: tangled.GraphFollowNSID,
80
Repo: currentUser.Did,
81
Rkey: follow.Rkey,
···
14
)
15
16
func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
17
+
currentUser := s.oauth.GetUser(r)
18
19
subject := r.URL.Query().Get("subject")
20
if subject == "" {
···
32
return
33
}
34
35
+
client, err := s.oauth.AuthorizedClient(r)
36
+
if err != nil {
37
+
log.Println("failed to authorize client")
38
+
return
39
+
}
40
41
switch r.Method {
42
case http.MethodPost:
43
createdAt := time.Now().Format(time.RFC3339)
44
rkey := appview.TID()
45
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
46
Collection: tangled.GraphFollowNSID,
47
Repo: currentUser.Did,
48
Rkey: rkey,
···
79
return
80
}
81
82
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
83
Collection: tangled.GraphFollowNSID,
84
Repo: currentUser.Did,
85
Rkey: follow.Rkey,
+2
-2
appview/state/git_http.go
+2
-2
appview/state/git_http.go
···
15
repo := chi.URLParam(r, "repo")
16
17
scheme := "https"
18
-
if s.config.Dev {
19
scheme = "http"
20
}
21
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
···
52
repo := chi.URLParam(r, "repo")
53
54
scheme := "https"
55
-
if s.config.Dev {
56
scheme = "http"
57
}
58
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
···
15
repo := chi.URLParam(r, "repo")
16
17
scheme := "https"
18
+
if s.config.Core.Dev {
19
scheme = "http"
20
}
21
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
···
52
repo := chi.URLParam(r, "repo")
53
54
scheme := "https"
55
+
if s.config.Core.Dev {
56
scheme = "http"
57
}
58
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
+36
-3
appview/state/middleware.go
+36
-3
appview/state/middleware.go
···
2
3
import (
4
"context"
5
"log"
6
"net/http"
7
"strconv"
···
20
return func(next http.Handler) http.Handler {
21
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
// requires auth also
23
-
actor := s.auth.GetUser(r)
24
if actor == nil {
25
// we need a logged in user
26
log.Printf("not logged in, redirecting")
···
54
return func(next http.Handler) http.Handler {
55
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56
// requires auth also
57
-
actor := s.auth.GetUser(r)
58
if actor == nil {
59
// we need a logged in user
60
log.Printf("not logged in, redirecting")
···
131
if err != nil {
132
// invalid did or handle
133
log.Println("failed to resolve repo")
134
-
w.WriteHeader(http.StatusNotFound)
135
return
136
}
137
···
175
})
176
}
177
}
···
2
3
import (
4
"context"
5
+
"fmt"
6
"log"
7
"net/http"
8
"strconv"
···
21
return func(next http.Handler) http.Handler {
22
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23
// requires auth also
24
+
actor := s.oauth.GetUser(r)
25
if actor == nil {
26
// we need a logged in user
27
log.Printf("not logged in, redirecting")
···
55
return func(next http.Handler) http.Handler {
56
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57
// requires auth also
58
+
actor := s.oauth.GetUser(r)
59
if actor == nil {
60
// we need a logged in user
61
log.Printf("not logged in, redirecting")
···
132
if err != nil {
133
// invalid did or handle
134
log.Println("failed to resolve repo")
135
+
s.pages.Error404(w)
136
return
137
}
138
···
176
})
177
}
178
}
179
+
180
+
// this should serve the go-import meta tag even if the path is technically
181
+
// a 404 like tangled.sh/oppi.li/go-git/v5
182
+
func GoImport(s *State) middleware.Middleware {
183
+
return func(next http.Handler) http.Handler {
184
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
185
+
f, err := s.fullyResolvedRepo(r)
186
+
if err != nil {
187
+
log.Println("failed to fully resolve repo", err)
188
+
http.Error(w, "invalid repo url", http.StatusNotFound)
189
+
return
190
+
}
191
+
192
+
fullName := f.OwnerHandle() + "/" + f.RepoName
193
+
194
+
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
195
+
if r.URL.Query().Get("go-get") == "1" {
196
+
html := fmt.Sprintf(
197
+
`<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`,
198
+
fullName,
199
+
fullName,
200
+
)
201
+
w.Header().Set("Content-Type", "text/html")
202
+
w.Write([]byte(html))
203
+
return
204
+
}
205
+
}
206
+
207
+
next.ServeHTTP(w, r)
208
+
})
209
+
}
210
+
}
+328
-14
appview/state/profile.go
+328
-14
appview/state/profile.go
···
7
"fmt"
8
"log"
9
"net/http"
10
11
"github.com/bluesky-social/indigo/atproto/identity"
12
"github.com/go-chi/chi/v5"
13
"tangled.sh/tangled.sh/core/appview/db"
14
"tangled.sh/tangled.sh/core/appview/pages"
15
)
16
17
-
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
18
didOrHandle := chi.URLParam(r, "user")
19
if didOrHandle == "" {
20
http.Error(w, "Bad request", http.StatusBadRequest)
···
27
return
28
}
29
30
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
31
if err != nil {
32
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
33
}
34
35
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
36
if err != nil {
37
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
38
}
39
40
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
41
if err != nil {
42
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
···
76
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
77
}
78
79
-
loggedInUser := s.auth.GetUser(r)
80
followStatus := db.IsNotFollowing
81
if loggedInUser != nil {
82
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
···
85
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
86
s.pages.ProfilePage(w, pages.ProfilePageParams{
87
LoggedInUser: loggedInUser,
88
-
UserDid: ident.DID.String(),
89
-
UserHandle: ident.Handle.String(),
90
-
Repos: repos,
91
-
CollaboratingRepos: collaboratingRepos,
92
-
ProfileStats: pages.ProfileStats{
93
-
Followers: followers,
94
-
Following: following,
95
},
96
-
FollowStatus: db.FollowStatus(followStatus),
97
-
DidHandleMap: didHandleMap,
98
-
AvatarUri: profileAvatarUri,
99
ProfileTimeline: timeline,
100
})
101
}
102
103
func (s *State) GetAvatarUri(handle string) string {
104
-
secret := s.config.AvatarSharedSecret
105
h := hmac.New(sha256.New, []byte(secret))
106
h.Write([]byte(handle))
107
signature := hex.EncodeToString(h.Sum(nil))
108
-
return fmt.Sprintf("%s/%s/%s", s.config.AvatarHost, signature, handle)
109
}
···
7
"fmt"
8
"log"
9
"net/http"
10
+
"slices"
11
+
"strings"
12
13
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
14
"github.com/bluesky-social/indigo/atproto/identity"
15
+
"github.com/bluesky-social/indigo/atproto/syntax"
16
+
lexutil "github.com/bluesky-social/indigo/lex/util"
17
"github.com/go-chi/chi/v5"
18
+
"tangled.sh/tangled.sh/core/api/tangled"
19
"tangled.sh/tangled.sh/core/appview/db"
20
"tangled.sh/tangled.sh/core/appview/pages"
21
)
22
23
+
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
24
+
tabVal := r.URL.Query().Get("tab")
25
+
switch tabVal {
26
+
case "":
27
+
s.profilePage(w, r)
28
+
case "repos":
29
+
s.reposPage(w, r)
30
+
}
31
+
}
32
+
33
+
func (s *State) profilePage(w http.ResponseWriter, r *http.Request) {
34
didOrHandle := chi.URLParam(r, "user")
35
if didOrHandle == "" {
36
http.Error(w, "Bad request", http.StatusBadRequest)
···
43
return
44
}
45
46
+
profile, err := db.GetProfile(s.db, ident.DID.String())
47
+
if err != nil {
48
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
49
+
}
50
+
51
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
52
if err != nil {
53
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
54
}
55
56
+
// filter out ones that are pinned
57
+
pinnedRepos := []db.Repo{}
58
+
for i, r := range repos {
59
+
// if this is a pinned repo, add it
60
+
if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) {
61
+
pinnedRepos = append(pinnedRepos, r)
62
+
}
63
+
64
+
// if there are no saved pins, add the first 4 repos
65
+
if profile.IsPinnedReposEmpty() && i < 4 {
66
+
pinnedRepos = append(pinnedRepos, r)
67
+
}
68
+
}
69
+
70
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
71
if err != nil {
72
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
73
}
74
75
+
pinnedCollaboratingRepos := []db.Repo{}
76
+
for _, r := range collaboratingRepos {
77
+
// if this is a pinned repo, add it
78
+
if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) {
79
+
pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r)
80
+
}
81
+
}
82
+
83
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
84
if err != nil {
85
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
···
119
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
120
}
121
122
+
loggedInUser := s.oauth.GetUser(r)
123
followStatus := db.IsNotFollowing
124
if loggedInUser != nil {
125
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
···
128
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
129
s.pages.ProfilePage(w, pages.ProfilePageParams{
130
LoggedInUser: loggedInUser,
131
+
Repos: pinnedRepos,
132
+
CollaboratingRepos: pinnedCollaboratingRepos,
133
+
DidHandleMap: didHandleMap,
134
+
Card: pages.ProfileCard{
135
+
UserDid: ident.DID.String(),
136
+
UserHandle: ident.Handle.String(),
137
+
AvatarUri: profileAvatarUri,
138
+
Profile: profile,
139
+
FollowStatus: followStatus,
140
+
Followers: followers,
141
+
Following: following,
142
},
143
ProfileTimeline: timeline,
144
})
145
}
146
147
+
func (s *State) reposPage(w http.ResponseWriter, r *http.Request) {
148
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
149
+
if !ok {
150
+
s.pages.Error404(w)
151
+
return
152
+
}
153
+
154
+
profile, err := db.GetProfile(s.db, ident.DID.String())
155
+
if err != nil {
156
+
log.Printf("getting profile data for %s: %s", ident.DID.String(), err)
157
+
}
158
+
159
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
160
+
if err != nil {
161
+
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
162
+
}
163
+
164
+
loggedInUser := s.oauth.GetUser(r)
165
+
followStatus := db.IsNotFollowing
166
+
if loggedInUser != nil {
167
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
168
+
}
169
+
170
+
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
171
+
if err != nil {
172
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
173
+
}
174
+
175
+
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
176
+
177
+
s.pages.ReposPage(w, pages.ReposPageParams{
178
+
LoggedInUser: loggedInUser,
179
+
Repos: repos,
180
+
Card: pages.ProfileCard{
181
+
UserDid: ident.DID.String(),
182
+
UserHandle: ident.Handle.String(),
183
+
AvatarUri: profileAvatarUri,
184
+
Profile: profile,
185
+
FollowStatus: followStatus,
186
+
Followers: followers,
187
+
Following: following,
188
+
},
189
+
})
190
+
}
191
+
192
func (s *State) GetAvatarUri(handle string) string {
193
+
secret := s.config.Avatar.SharedSecret
194
h := hmac.New(sha256.New, []byte(secret))
195
h.Write([]byte(handle))
196
signature := hex.EncodeToString(h.Sum(nil))
197
+
return fmt.Sprintf("%s/%s/%s", s.config.Avatar.Host, signature, handle)
198
+
}
199
+
200
+
func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
201
+
user := s.oauth.GetUser(r)
202
+
203
+
err := r.ParseForm()
204
+
if err != nil {
205
+
log.Println("invalid profile update form", err)
206
+
s.pages.Notice(w, "update-profile", "Invalid form.")
207
+
return
208
+
}
209
+
210
+
profile, err := db.GetProfile(s.db, user.Did)
211
+
if err != nil {
212
+
log.Printf("getting profile data for %s: %s", user.Did, err)
213
+
}
214
+
215
+
profile.Description = r.FormValue("description")
216
+
profile.IncludeBluesky = r.FormValue("includeBluesky") == "on"
217
+
profile.Location = r.FormValue("location")
218
+
219
+
var links [5]string
220
+
for i := range 5 {
221
+
iLink := r.FormValue(fmt.Sprintf("link%d", i))
222
+
links[i] = iLink
223
+
}
224
+
profile.Links = links
225
+
226
+
// Parse stats (exactly 2)
227
+
stat0 := r.FormValue("stat0")
228
+
stat1 := r.FormValue("stat1")
229
+
230
+
if stat0 != "" {
231
+
profile.Stats[0].Kind = db.VanityStatKind(stat0)
232
+
}
233
+
234
+
if stat1 != "" {
235
+
profile.Stats[1].Kind = db.VanityStatKind(stat1)
236
+
}
237
+
238
+
if err := db.ValidateProfile(s.db, profile); err != nil {
239
+
log.Println("invalid profile", err)
240
+
s.pages.Notice(w, "update-profile", err.Error())
241
+
return
242
+
}
243
+
244
+
s.updateProfile(profile, w, r)
245
+
return
246
+
}
247
+
248
+
func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
249
+
user := s.oauth.GetUser(r)
250
+
251
+
err := r.ParseForm()
252
+
if err != nil {
253
+
log.Println("invalid profile update form", err)
254
+
s.pages.Notice(w, "update-profile", "Invalid form.")
255
+
return
256
+
}
257
+
258
+
profile, err := db.GetProfile(s.db, user.Did)
259
+
if err != nil {
260
+
log.Printf("getting profile data for %s: %s", user.Did, err)
261
+
}
262
+
263
+
i := 0
264
+
var pinnedRepos [6]syntax.ATURI
265
+
for key, values := range r.Form {
266
+
if i >= 6 {
267
+
log.Println("invalid pin update form", err)
268
+
s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.")
269
+
return
270
+
}
271
+
if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 {
272
+
aturi, err := syntax.ParseATURI(values[0])
273
+
if err != nil {
274
+
log.Println("invalid profile update form", err)
275
+
s.pages.Notice(w, "update-profile", "Invalid form.")
276
+
return
277
+
}
278
+
pinnedRepos[i] = aturi
279
+
i++
280
+
}
281
+
}
282
+
profile.PinnedRepos = pinnedRepos
283
+
284
+
s.updateProfile(profile, w, r)
285
+
return
286
+
}
287
+
288
+
func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *http.Request) {
289
+
user := s.oauth.GetUser(r)
290
+
tx, err := s.db.BeginTx(r.Context(), nil)
291
+
if err != nil {
292
+
log.Println("failed to start transaction", err)
293
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
294
+
return
295
+
}
296
+
297
+
client, err := s.oauth.AuthorizedClient(r)
298
+
if err != nil {
299
+
log.Println("failed to get authorized client", err)
300
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
301
+
return
302
+
}
303
+
304
+
// yeah... lexgen dose not support syntax.ATURI in the record for some reason,
305
+
// nor does it support exact size arrays
306
+
var pinnedRepoStrings []string
307
+
for _, r := range profile.PinnedRepos {
308
+
pinnedRepoStrings = append(pinnedRepoStrings, r.String())
309
+
}
310
+
311
+
var vanityStats []string
312
+
for _, v := range profile.Stats {
313
+
vanityStats = append(vanityStats, string(v.Kind))
314
+
}
315
+
316
+
ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self")
317
+
var cid *string
318
+
if ex != nil {
319
+
cid = ex.Cid
320
+
}
321
+
322
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
323
+
Collection: tangled.ActorProfileNSID,
324
+
Repo: user.Did,
325
+
Rkey: "self",
326
+
Record: &lexutil.LexiconTypeDecoder{
327
+
Val: &tangled.ActorProfile{
328
+
Bluesky: profile.IncludeBluesky,
329
+
Description: &profile.Description,
330
+
Links: profile.Links[:],
331
+
Location: &profile.Location,
332
+
PinnedRepositories: pinnedRepoStrings,
333
+
Stats: vanityStats[:],
334
+
}},
335
+
SwapRecord: cid,
336
+
})
337
+
if err != nil {
338
+
log.Println("failed to update profile", err)
339
+
s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.")
340
+
return
341
+
}
342
+
343
+
err = db.UpsertProfile(tx, profile)
344
+
if err != nil {
345
+
log.Println("failed to update profile", err)
346
+
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
347
+
return
348
+
}
349
+
350
+
s.pages.HxRedirect(w, "/"+user.Did)
351
+
return
352
+
}
353
+
354
+
func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
355
+
user := s.oauth.GetUser(r)
356
+
357
+
profile, err := db.GetProfile(s.db, user.Did)
358
+
if err != nil {
359
+
log.Printf("getting profile data for %s: %s", user.Did, err)
360
+
}
361
+
362
+
s.pages.EditBioFragment(w, pages.EditBioParams{
363
+
LoggedInUser: user,
364
+
Profile: profile,
365
+
})
366
+
}
367
+
368
+
func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
369
+
user := s.oauth.GetUser(r)
370
+
371
+
profile, err := db.GetProfile(s.db, user.Did)
372
+
if err != nil {
373
+
log.Printf("getting profile data for %s: %s", user.Did, err)
374
+
}
375
+
376
+
repos, err := db.GetAllReposByDid(s.db, user.Did)
377
+
if err != nil {
378
+
log.Printf("getting repos for %s: %s", user.Did, err)
379
+
}
380
+
381
+
collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did)
382
+
if err != nil {
383
+
log.Printf("getting collaborating repos for %s: %s", user.Did, err)
384
+
}
385
+
386
+
allRepos := []pages.PinnedRepo{}
387
+
388
+
for _, r := range repos {
389
+
isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt())
390
+
allRepos = append(allRepos, pages.PinnedRepo{
391
+
IsPinned: isPinned,
392
+
Repo: r,
393
+
})
394
+
}
395
+
for _, r := range collaboratingRepos {
396
+
isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt())
397
+
allRepos = append(allRepos, pages.PinnedRepo{
398
+
IsPinned: isPinned,
399
+
Repo: r,
400
+
})
401
+
}
402
+
403
+
var didsToResolve []string
404
+
for _, r := range allRepos {
405
+
didsToResolve = append(didsToResolve, r.Did)
406
+
}
407
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
408
+
didHandleMap := make(map[string]string)
409
+
for _, identity := range resolvedIds {
410
+
if !identity.Handle.IsInvalidHandle() {
411
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
412
+
} else {
413
+
didHandleMap[identity.DID.String()] = identity.DID.String()
414
+
}
415
+
}
416
+
417
+
s.pages.EditPinsFragment(w, pages.EditPinsParams{
418
+
LoggedInUser: user,
419
+
Profile: profile,
420
+
AllRepos: allRepos,
421
+
DidHandleMap: didHandleMap,
422
+
})
423
}
+84
-53
appview/state/pull.go
+84
-53
appview/state/pull.go
···
13
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview"
16
-
"tangled.sh/tangled.sh/core/appview/auth"
17
"tangled.sh/tangled.sh/core/appview/db"
18
"tangled.sh/tangled.sh/core/appview/pages"
19
"tangled.sh/tangled.sh/core/patchutil"
20
"tangled.sh/tangled.sh/core/types"
···
29
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
30
switch r.Method {
31
case http.MethodGet:
32
-
user := s.auth.GetUser(r)
33
f, err := s.fullyResolvedRepo(r)
34
if err != nil {
35
log.Println("failed to get repo and knot", err)
···
73
}
74
75
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
76
-
user := s.auth.GetUser(r)
77
f, err := s.fullyResolvedRepo(r)
78
if err != nil {
79
log.Println("failed to get repo and knot", err)
···
143
}
144
}
145
146
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
147
if err != nil {
148
log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
149
return types.MergeCheckResponse{
···
215
repoName = f.RepoName
216
}
217
218
-
us, err := NewUnsignedClient(knot, s.config.Dev)
219
if err != nil {
220
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
221
return pages.Unknown
···
250
}
251
252
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
253
-
user := s.auth.GetUser(r)
254
f, err := s.fullyResolvedRepo(r)
255
if err != nil {
256
log.Println("failed to get repo and knot", err)
···
298
}
299
300
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
301
-
user := s.auth.GetUser(r)
302
303
f, err := s.fullyResolvedRepo(r)
304
if err != nil {
···
355
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
356
357
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
358
-
LoggedInUser: s.auth.GetUser(r),
359
RepoInfo: f.RepoInfo(s, user),
360
Pull: pull,
361
Round: roundIdInt,
···
397
}
398
399
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
400
-
user := s.auth.GetUser(r)
401
params := r.URL.Query()
402
403
state := db.PullOpen
···
451
}
452
453
s.pages.RepoPulls(w, pages.RepoPullsParams{
454
-
LoggedInUser: s.auth.GetUser(r),
455
RepoInfo: f.RepoInfo(s, user),
456
Pulls: pulls,
457
DidHandleMap: didHandleMap,
···
461
}
462
463
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
464
-
user := s.auth.GetUser(r)
465
f, err := s.fullyResolvedRepo(r)
466
if err != nil {
467
log.Println("failed to get repo and knot", err)
···
519
}
520
521
atUri := f.RepoAt.String()
522
-
client, _ := s.auth.AuthorizedClient(r)
523
-
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
524
Collection: tangled.RepoPullCommentNSID,
525
Repo: user.Did,
526
Rkey: appview.TID(),
···
568
}
569
570
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
571
-
user := s.auth.GetUser(r)
572
f, err := s.fullyResolvedRepo(r)
573
if err != nil {
574
log.Println("failed to get repo and knot", err)
···
577
578
switch r.Method {
579
case http.MethodGet:
580
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
581
if err != nil {
582
log.Printf("failed to create unsigned client for %s", f.Knot)
583
s.pages.Error503(w)
···
646
return
647
}
648
649
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
650
if err != nil {
651
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
652
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
···
689
}
690
}
691
692
-
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) {
693
pullSource := &db.PullSource{
694
Branch: sourceBranch,
695
}
···
698
}
699
700
// Generate a patch using /compare
701
-
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
702
if err != nil {
703
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
704
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
723
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
724
}
725
726
-
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) {
727
if !patchutil.IsPatchValid(patch) {
728
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
729
return
···
732
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil)
733
}
734
735
-
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
736
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
737
if errors.Is(err, sql.ErrNoRows) {
738
s.pages.Notice(w, "pull", "No such fork.")
···
750
return
751
}
752
753
-
sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev)
754
if err != nil {
755
log.Println("failed to create signed client:", err)
756
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
757
return
758
}
759
760
-
us, err := NewUnsignedClient(fork.Knot, s.config.Dev)
761
if err != nil {
762
log.Println("failed to create unsigned client:", err)
763
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
816
w http.ResponseWriter,
817
r *http.Request,
818
f *FullyResolvedRepo,
819
-
user *auth.User,
820
title, body, targetBranch string,
821
patch string,
822
sourceRev string,
···
870
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
871
return
872
}
873
-
client, _ := s.auth.AuthorizedClient(r)
874
-
pullId, err := db.NextPullId(s.db, f.RepoAt)
875
if err != nil {
876
log.Println("failed to get pull id", err)
877
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
878
return
879
}
880
881
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
882
Collection: tangled.RepoPullNSID,
883
Repo: user.Did,
884
Rkey: rkey,
···
893
},
894
},
895
})
896
897
-
if err != nil {
898
log.Println("failed to create pull request", err)
899
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
900
return
···
929
}
930
931
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
932
-
user := s.auth.GetUser(r)
933
f, err := s.fullyResolvedRepo(r)
934
if err != nil {
935
log.Println("failed to get repo and knot", err)
···
942
}
943
944
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
945
-
user := s.auth.GetUser(r)
946
f, err := s.fullyResolvedRepo(r)
947
if err != nil {
948
log.Println("failed to get repo and knot", err)
949
return
950
}
951
952
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
953
if err != nil {
954
log.Printf("failed to create unsigned client for %s", f.Knot)
955
s.pages.Error503(w)
···
982
}
983
984
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
985
-
user := s.auth.GetUser(r)
986
f, err := s.fullyResolvedRepo(r)
987
if err != nil {
988
log.Println("failed to get repo and knot", err)
···
1002
}
1003
1004
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1005
-
user := s.auth.GetUser(r)
1006
1007
f, err := s.fullyResolvedRepo(r)
1008
if err != nil {
···
1019
return
1020
}
1021
1022
-
sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev)
1023
if err != nil {
1024
log.Printf("failed to create unsigned client for %s", repo.Knot)
1025
s.pages.Error503(w)
···
1046
return
1047
}
1048
1049
-
targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
1050
if err != nil {
1051
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1052
s.pages.Error503(w)
···
1081
}
1082
1083
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1084
-
user := s.auth.GetUser(r)
1085
f, err := s.fullyResolvedRepo(r)
1086
if err != nil {
1087
log.Println("failed to get repo and knot", err)
···
1117
}
1118
1119
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1120
-
user := s.auth.GetUser(r)
1121
1122
pull, ok := r.Context().Value("pull").(*db.Pull)
1123
if !ok {
···
1159
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
1160
return
1161
}
1162
-
client, _ := s.auth.AuthorizedClient(r)
1163
1164
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1165
if err != nil {
1166
// failed to get record
1167
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1168
return
1169
}
1170
1171
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1172
Collection: tangled.RepoPullNSID,
1173
Repo: user.Did,
1174
Rkey: pull.Rkey,
···
1200
}
1201
1202
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1203
-
user := s.auth.GetUser(r)
1204
1205
pull, ok := r.Context().Value("pull").(*db.Pull)
1206
if !ok {
···
1227
return
1228
}
1229
1230
-
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
1231
if err != nil {
1232
log.Printf("failed to create client for %s: %s", f.Knot, err)
1233
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1268
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1269
return
1270
}
1271
-
client, _ := s.auth.AuthorizedClient(r)
1272
1273
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1274
if err != nil {
1275
// failed to get record
1276
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1280
recordPullSource := &tangled.RepoPull_Source{
1281
Branch: pull.PullSource.Branch,
1282
}
1283
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1284
Collection: tangled.RepoPullNSID,
1285
Repo: user.Did,
1286
Rkey: pull.Rkey,
···
1313
}
1314
1315
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
1316
-
user := s.auth.GetUser(r)
1317
1318
pull, ok := r.Context().Value("pull").(*db.Pull)
1319
if !ok {
···
1342
}
1343
1344
// extract patch by performing compare
1345
-
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
1346
if err != nil {
1347
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
1348
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1357
}
1358
1359
// update the hidden tracking branch to latest
1360
-
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
1361
if err != nil {
1362
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
1363
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1406
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1407
return
1408
}
1409
-
client, _ := s.auth.AuthorizedClient(r)
1410
1411
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1412
if err != nil {
1413
// failed to get record
1414
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1420
Branch: pull.PullSource.Branch,
1421
Repo: &repoAt,
1422
}
1423
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1424
Collection: tangled.RepoPullNSID,
1425
Repo: user.Did,
1426
Rkey: pull.Rkey,
···
1503
log.Printf("failed to get primary email: %s", err)
1504
}
1505
1506
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
1507
if err != nil {
1508
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
1509
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1533
}
1534
1535
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1536
-
user := s.auth.GetUser(r)
1537
1538
f, err := s.fullyResolvedRepo(r)
1539
if err != nil {
···
1587
}
1588
1589
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1590
-
user := s.auth.GetUser(r)
1591
1592
f, err := s.fullyResolvedRepo(r)
1593
if err != nil {
···
13
14
"tangled.sh/tangled.sh/core/api/tangled"
15
"tangled.sh/tangled.sh/core/appview"
16
"tangled.sh/tangled.sh/core/appview/db"
17
+
"tangled.sh/tangled.sh/core/appview/knotclient"
18
+
"tangled.sh/tangled.sh/core/appview/oauth"
19
"tangled.sh/tangled.sh/core/appview/pages"
20
"tangled.sh/tangled.sh/core/patchutil"
21
"tangled.sh/tangled.sh/core/types"
···
30
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
31
switch r.Method {
32
case http.MethodGet:
33
+
user := s.oauth.GetUser(r)
34
f, err := s.fullyResolvedRepo(r)
35
if err != nil {
36
log.Println("failed to get repo and knot", err)
···
74
}
75
76
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
77
+
user := s.oauth.GetUser(r)
78
f, err := s.fullyResolvedRepo(r)
79
if err != nil {
80
log.Println("failed to get repo and knot", err)
···
144
}
145
}
146
147
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
148
if err != nil {
149
log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
150
return types.MergeCheckResponse{
···
216
repoName = f.RepoName
217
}
218
219
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
220
if err != nil {
221
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
222
return pages.Unknown
···
251
}
252
253
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
254
+
user := s.oauth.GetUser(r)
255
f, err := s.fullyResolvedRepo(r)
256
if err != nil {
257
log.Println("failed to get repo and knot", err)
···
299
}
300
301
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
302
+
user := s.oauth.GetUser(r)
303
304
f, err := s.fullyResolvedRepo(r)
305
if err != nil {
···
356
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
357
358
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
359
+
LoggedInUser: s.oauth.GetUser(r),
360
RepoInfo: f.RepoInfo(s, user),
361
Pull: pull,
362
Round: roundIdInt,
···
398
}
399
400
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
401
+
user := s.oauth.GetUser(r)
402
params := r.URL.Query()
403
404
state := db.PullOpen
···
452
}
453
454
s.pages.RepoPulls(w, pages.RepoPullsParams{
455
+
LoggedInUser: s.oauth.GetUser(r),
456
RepoInfo: f.RepoInfo(s, user),
457
Pulls: pulls,
458
DidHandleMap: didHandleMap,
···
462
}
463
464
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
465
+
user := s.oauth.GetUser(r)
466
f, err := s.fullyResolvedRepo(r)
467
if err != nil {
468
log.Println("failed to get repo and knot", err)
···
520
}
521
522
atUri := f.RepoAt.String()
523
+
client, err := s.oauth.AuthorizedClient(r)
524
+
if err != nil {
525
+
log.Println("failed to get authorized client", err)
526
+
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
527
+
return
528
+
}
529
+
atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
530
Collection: tangled.RepoPullCommentNSID,
531
Repo: user.Did,
532
Rkey: appview.TID(),
···
574
}
575
576
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
577
+
user := s.oauth.GetUser(r)
578
f, err := s.fullyResolvedRepo(r)
579
if err != nil {
580
log.Println("failed to get repo and knot", err)
···
583
584
switch r.Method {
585
case http.MethodGet:
586
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
587
if err != nil {
588
log.Printf("failed to create unsigned client for %s", f.Knot)
589
s.pages.Error503(w)
···
652
return
653
}
654
655
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
656
if err != nil {
657
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
658
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
···
695
}
696
}
697
698
+
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, sourceBranch string) {
699
pullSource := &db.PullSource{
700
Branch: sourceBranch,
701
}
···
704
}
705
706
// Generate a patch using /compare
707
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
708
if err != nil {
709
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
710
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
729
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
730
}
731
732
+
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string) {
733
if !patchutil.IsPatchValid(patch) {
734
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
735
return
···
738
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil)
739
}
740
741
+
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
742
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
743
if errors.Is(err, sql.ErrNoRows) {
744
s.pages.Notice(w, "pull", "No such fork.")
···
756
return
757
}
758
759
+
sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev)
760
if err != nil {
761
log.Println("failed to create signed client:", err)
762
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
763
return
764
}
765
766
+
us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev)
767
if err != nil {
768
log.Println("failed to create unsigned client:", err)
769
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
822
w http.ResponseWriter,
823
r *http.Request,
824
f *FullyResolvedRepo,
825
+
user *oauth.User,
826
title, body, targetBranch string,
827
patch string,
828
sourceRev string,
···
876
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
877
return
878
}
879
+
client, err := s.oauth.AuthorizedClient(r)
880
+
if err != nil {
881
+
log.Println("failed to get authorized client", err)
882
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
883
+
return
884
+
}
885
+
pullId, err := db.NextPullId(tx, f.RepoAt)
886
if err != nil {
887
log.Println("failed to get pull id", err)
888
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
889
return
890
}
891
892
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
893
Collection: tangled.RepoPullNSID,
894
Repo: user.Did,
895
Rkey: rkey,
···
904
},
905
},
906
})
907
+
if err != nil {
908
+
log.Println("failed to create pull request", err)
909
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
910
+
return
911
+
}
912
913
+
if err = tx.Commit(); err != nil {
914
log.Println("failed to create pull request", err)
915
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
916
return
···
945
}
946
947
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
948
+
user := s.oauth.GetUser(r)
949
f, err := s.fullyResolvedRepo(r)
950
if err != nil {
951
log.Println("failed to get repo and knot", err)
···
958
}
959
960
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
961
+
user := s.oauth.GetUser(r)
962
f, err := s.fullyResolvedRepo(r)
963
if err != nil {
964
log.Println("failed to get repo and knot", err)
965
return
966
}
967
968
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
969
if err != nil {
970
log.Printf("failed to create unsigned client for %s", f.Knot)
971
s.pages.Error503(w)
···
998
}
999
1000
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1001
+
user := s.oauth.GetUser(r)
1002
f, err := s.fullyResolvedRepo(r)
1003
if err != nil {
1004
log.Println("failed to get repo and knot", err)
···
1018
}
1019
1020
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
1021
+
user := s.oauth.GetUser(r)
1022
1023
f, err := s.fullyResolvedRepo(r)
1024
if err != nil {
···
1035
return
1036
}
1037
1038
+
sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev)
1039
if err != nil {
1040
log.Printf("failed to create unsigned client for %s", repo.Knot)
1041
s.pages.Error503(w)
···
1062
return
1063
}
1064
1065
+
targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1066
if err != nil {
1067
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
1068
s.pages.Error503(w)
···
1097
}
1098
1099
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1100
+
user := s.oauth.GetUser(r)
1101
f, err := s.fullyResolvedRepo(r)
1102
if err != nil {
1103
log.Println("failed to get repo and knot", err)
···
1133
}
1134
1135
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
1136
+
user := s.oauth.GetUser(r)
1137
1138
pull, ok := r.Context().Value("pull").(*db.Pull)
1139
if !ok {
···
1175
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
1176
return
1177
}
1178
+
client, err := s.oauth.AuthorizedClient(r)
1179
+
if err != nil {
1180
+
log.Println("failed to get authorized client", err)
1181
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1182
+
return
1183
+
}
1184
1185
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1186
if err != nil {
1187
// failed to get record
1188
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
1189
return
1190
}
1191
1192
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1193
Collection: tangled.RepoPullNSID,
1194
Repo: user.Did,
1195
Rkey: pull.Rkey,
···
1221
}
1222
1223
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
1224
+
user := s.oauth.GetUser(r)
1225
1226
pull, ok := r.Context().Value("pull").(*db.Pull)
1227
if !ok {
···
1248
return
1249
}
1250
1251
+
ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
1252
if err != nil {
1253
log.Printf("failed to create client for %s: %s", f.Knot, err)
1254
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1289
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1290
return
1291
}
1292
+
client, err := s.oauth.AuthorizedClient(r)
1293
+
if err != nil {
1294
+
log.Println("failed to authorize client")
1295
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1296
+
return
1297
+
}
1298
1299
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1300
if err != nil {
1301
// failed to get record
1302
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1306
recordPullSource := &tangled.RepoPull_Source{
1307
Branch: pull.PullSource.Branch,
1308
}
1309
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1310
Collection: tangled.RepoPullNSID,
1311
Repo: user.Did,
1312
Rkey: pull.Rkey,
···
1339
}
1340
1341
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
1342
+
user := s.oauth.GetUser(r)
1343
1344
pull, ok := r.Context().Value("pull").(*db.Pull)
1345
if !ok {
···
1368
}
1369
1370
// extract patch by performing compare
1371
+
ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev)
1372
if err != nil {
1373
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
1374
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1383
}
1384
1385
// update the hidden tracking branch to latest
1386
+
signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev)
1387
if err != nil {
1388
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
1389
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
1432
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1433
return
1434
}
1435
+
client, err := s.oauth.AuthorizedClient(r)
1436
+
if err != nil {
1437
+
log.Println("failed to get client")
1438
+
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1439
+
return
1440
+
}
1441
1442
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey)
1443
if err != nil {
1444
// failed to get record
1445
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
···
1451
Branch: pull.PullSource.Branch,
1452
Repo: &repoAt,
1453
}
1454
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1455
Collection: tangled.RepoPullNSID,
1456
Repo: user.Did,
1457
Rkey: pull.Rkey,
···
1534
log.Printf("failed to get primary email: %s", err)
1535
}
1536
1537
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
1538
if err != nil {
1539
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
1540
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
1564
}
1565
1566
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
1567
+
user := s.oauth.GetUser(r)
1568
1569
f, err := s.fullyResolvedRepo(r)
1570
if err != nil {
···
1618
}
1619
1620
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
1621
+
user := s.oauth.GetUser(r)
1622
1623
f, err := s.fullyResolvedRepo(r)
1624
if err != nil {
+99
-60
appview/state/repo.go
+99
-60
appview/state/repo.go
···
18
19
"tangled.sh/tangled.sh/core/api/tangled"
20
"tangled.sh/tangled.sh/core/appview"
21
-
"tangled.sh/tangled.sh/core/appview/auth"
22
"tangled.sh/tangled.sh/core/appview/db"
23
"tangled.sh/tangled.sh/core/appview/pages"
24
"tangled.sh/tangled.sh/core/appview/pages/markup"
25
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
45
return
46
}
47
48
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
49
if err != nil {
50
log.Printf("failed to create unsigned client for %s", f.Knot)
51
s.pages.Error503(w)
···
119
120
emails := uniqueEmails(commitsTrunc)
121
122
-
user := s.auth.GetUser(r)
123
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
124
LoggedInUser: user,
125
RepoInfo: f.RepoInfo(s, user),
···
150
151
ref := chi.URLParam(r, "ref")
152
153
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
154
if err != nil {
155
log.Println("failed to create unsigned client", err)
156
return
···
190
tagMap[hash] = append(tagMap[hash], tag.Name)
191
}
192
193
-
user := s.auth.GetUser(r)
194
s.pages.RepoLog(w, pages.RepoLogParams{
195
LoggedInUser: user,
196
TagMap: tagMap,
···
209
return
210
}
211
212
-
user := s.auth.GetUser(r)
213
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
214
RepoInfo: f.RepoInfo(s, user),
215
})
···
232
return
233
}
234
235
-
user := s.auth.GetUser(r)
236
237
switch r.Method {
238
case http.MethodGet:
···
241
})
242
return
243
case http.MethodPut:
244
-
user := s.auth.GetUser(r)
245
newDescription := r.FormValue("description")
246
-
client, _ := s.auth.AuthorizedClient(r)
247
248
// optimistic update
249
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
···
256
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
257
//
258
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
259
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
260
if err != nil {
261
// failed to get record
262
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
263
return
264
}
265
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
266
Collection: tangled.RepoNSID,
267
Repo: user.Did,
268
Rkey: rkey,
···
303
}
304
ref := chi.URLParam(r, "ref")
305
protocol := "http"
306
-
if !s.config.Dev {
307
protocol = "https"
308
}
309
···
331
return
332
}
333
334
-
user := s.auth.GetUser(r)
335
s.pages.RepoCommit(w, pages.RepoCommitParams{
336
LoggedInUser: user,
337
RepoInfo: f.RepoInfo(s, user),
···
351
ref := chi.URLParam(r, "ref")
352
treePath := chi.URLParam(r, "*")
353
protocol := "http"
354
-
if !s.config.Dev {
355
protocol = "https"
356
}
357
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
···
380
return
381
}
382
383
-
user := s.auth.GetUser(r)
384
385
var breadcrumbs [][]string
386
breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
411
return
412
}
413
414
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
415
if err != nil {
416
log.Println("failed to create unsigned client", err)
417
return
···
451
}
452
}
453
454
-
user := s.auth.GetUser(r)
455
s.pages.RepoTags(w, pages.RepoTagsParams{
456
LoggedInUser: user,
457
RepoInfo: f.RepoInfo(s, user),
···
469
return
470
}
471
472
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
473
if err != nil {
474
log.Println("failed to create unsigned client", err)
475
return
···
511
return strings.Compare(a.Name, b.Name) * -1
512
})
513
514
-
user := s.auth.GetUser(r)
515
s.pages.RepoBranches(w, pages.RepoBranchesParams{
516
LoggedInUser: user,
517
RepoInfo: f.RepoInfo(s, user),
···
530
ref := chi.URLParam(r, "ref")
531
filePath := chi.URLParam(r, "*")
532
protocol := "http"
533
-
if !s.config.Dev {
534
protocol = "https"
535
}
536
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
568
showRendered = r.URL.Query().Get("code") != "true"
569
}
570
571
-
user := s.auth.GetUser(r)
572
s.pages.RepoBlob(w, pages.RepoBlobParams{
573
LoggedInUser: user,
574
RepoInfo: f.RepoInfo(s, user),
···
591
filePath := chi.URLParam(r, "*")
592
593
protocol := "http"
594
-
if !s.config.Dev {
595
protocol = "https"
596
}
597
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
652
return
653
}
654
655
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
656
if err != nil {
657
log.Println("failed to create client to ", f.Knot)
658
return
···
714
}
715
716
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
717
-
user := s.auth.GetUser(r)
718
719
f, err := s.fullyResolvedRepo(r)
720
if err != nil {
···
723
}
724
725
// remove record from pds
726
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
727
repoRkey := f.RepoAt.RecordKey().String()
728
-
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
729
Collection: tangled.RepoNSID,
730
Repo: user.Did,
731
Rkey: repoRkey,
···
743
return
744
}
745
746
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
747
if err != nil {
748
log.Println("failed to create client to ", f.Knot)
749
return
···
838
return
839
}
840
841
-
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
842
if err != nil {
843
log.Println("failed to create client to ", f.Knot)
844
return
···
868
switch r.Method {
869
case http.MethodGet:
870
// for now, this is just pubkeys
871
-
user := s.auth.GetUser(r)
872
repoCollaborators, err := f.Collaborators(r.Context(), s)
873
if err != nil {
874
log.Println("failed to get collaborators", err)
···
884
885
var branchNames []string
886
var defaultBranch string
887
-
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
888
if err != nil {
889
log.Println("failed to create unsigned client", err)
890
} else {
···
1008
return collaborators, nil
1009
}
1010
1011
-
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
1012
isStarred := false
1013
if u != nil {
1014
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
···
1051
1052
knot := f.Knot
1053
var disableFork bool
1054
-
us, err := NewUnsignedClient(knot, s.config.Dev)
1055
if err != nil {
1056
log.Printf("failed to create unsigned client for %s: %v", knot, err)
1057
} else {
···
1105
}
1106
1107
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1108
-
user := s.auth.GetUser(r)
1109
f, err := s.fullyResolvedRepo(r)
1110
if err != nil {
1111
log.Println("failed to get repo and knot", err)
···
1159
}
1160
1161
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1162
-
user := s.auth.GetUser(r)
1163
f, err := s.fullyResolvedRepo(r)
1164
if err != nil {
1165
log.Println("failed to get repo and knot", err)
···
1195
1196
closed := tangled.RepoIssueStateClosed
1197
1198
-
client, _ := s.auth.AuthorizedClient(r)
1199
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1200
Collection: tangled.RepoIssueStateNSID,
1201
Repo: user.Did,
1202
Rkey: appview.TID(),
···
1214
return
1215
}
1216
1217
-
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1218
if err != nil {
1219
log.Println("failed to close issue", err)
1220
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
1231
}
1232
1233
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1234
-
user := s.auth.GetUser(r)
1235
f, err := s.fullyResolvedRepo(r)
1236
if err != nil {
1237
log.Println("failed to get repo and knot", err)
···
1279
}
1280
1281
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
1282
-
user := s.auth.GetUser(r)
1283
f, err := s.fullyResolvedRepo(r)
1284
if err != nil {
1285
log.Println("failed to get repo and knot", err)
···
1330
}
1331
1332
atUri := f.RepoAt.String()
1333
-
client, _ := s.auth.AuthorizedClient(r)
1334
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1335
Collection: tangled.RepoIssueCommentNSID,
1336
Repo: user.Did,
1337
Rkey: rkey,
···
1358
}
1359
1360
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1361
-
user := s.auth.GetUser(r)
1362
f, err := s.fullyResolvedRepo(r)
1363
if err != nil {
1364
log.Println("failed to get repo and knot", err)
···
1417
}
1418
1419
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
1420
-
user := s.auth.GetUser(r)
1421
f, err := s.fullyResolvedRepo(r)
1422
if err != nil {
1423
log.Println("failed to get repo and knot", err)
···
1469
case http.MethodPost:
1470
// extract form value
1471
newBody := r.FormValue("body")
1472
-
client, _ := s.auth.AuthorizedClient(r)
1473
rkey := comment.Rkey
1474
1475
// optimistic update
···
1484
// rkey is optional, it was introduced later
1485
if comment.Rkey != "" {
1486
// update the record on pds
1487
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
1488
if err != nil {
1489
// failed to get record
1490
log.Println(err, rkey)
···
1499
createdAt := record["createdAt"].(string)
1500
commentIdInt64 := int64(commentIdInt)
1501
1502
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1503
Collection: tangled.RepoIssueCommentNSID,
1504
Repo: user.Did,
1505
Rkey: rkey,
···
1542
}
1543
1544
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
1545
-
user := s.auth.GetUser(r)
1546
f, err := s.fullyResolvedRepo(r)
1547
if err != nil {
1548
log.Println("failed to get repo and knot", err)
···
1599
1600
// delete from pds
1601
if comment.Rkey != "" {
1602
-
client, _ := s.auth.AuthorizedClient(r)
1603
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
1604
Collection: tangled.GraphFollowNSID,
1605
Repo: user.Did,
1606
Rkey: comment.Rkey,
···
1647
page = pagination.FirstPage()
1648
}
1649
1650
-
user := s.auth.GetUser(r)
1651
f, err := s.fullyResolvedRepo(r)
1652
if err != nil {
1653
log.Println("failed to get repo and knot", err)
···
1676
}
1677
1678
s.pages.RepoIssues(w, pages.RepoIssuesParams{
1679
-
LoggedInUser: s.auth.GetUser(r),
1680
RepoInfo: f.RepoInfo(s, user),
1681
Issues: issues,
1682
DidHandleMap: didHandleMap,
···
1687
}
1688
1689
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1690
-
user := s.auth.GetUser(r)
1691
1692
f, err := s.fullyResolvedRepo(r)
1693
if err != nil {
···
1735
return
1736
}
1737
1738
-
client, _ := s.auth.AuthorizedClient(r)
1739
atUri := f.RepoAt.String()
1740
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1741
Collection: tangled.RepoIssueNSID,
1742
Repo: user.Did,
1743
Rkey: appview.TID(),
···
1770
}
1771
1772
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
1773
-
user := s.auth.GetUser(r)
1774
f, err := s.fullyResolvedRepo(r)
1775
if err != nil {
1776
log.Printf("failed to resolve source repo: %v", err)
···
1779
1780
switch r.Method {
1781
case http.MethodGet:
1782
-
user := s.auth.GetUser(r)
1783
knots, err := s.enforcer.GetDomainsForUser(user.Did)
1784
if err != nil {
1785
s.pages.Notice(w, "repo", "Invalid user account.")
···
1829
return
1830
}
1831
1832
-
client, err := NewSignedClient(knot, secret, s.config.Dev)
1833
if err != nil {
1834
s.pages.Notice(w, "repo", "Failed to reach knot server.")
1835
return
1836
}
1837
1838
var uri string
1839
-
if s.config.Dev {
1840
uri = "http"
1841
} else {
1842
uri = "https"
···
1883
// continue
1884
}
1885
1886
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
1887
1888
createdAt := time.Now().Format(time.RFC3339)
1889
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
1890
Collection: tangled.RepoNSID,
1891
Repo: user.Did,
1892
Rkey: rkey,
···
18
19
"tangled.sh/tangled.sh/core/api/tangled"
20
"tangled.sh/tangled.sh/core/appview"
21
"tangled.sh/tangled.sh/core/appview/db"
22
+
"tangled.sh/tangled.sh/core/appview/knotclient"
23
+
"tangled.sh/tangled.sh/core/appview/oauth"
24
"tangled.sh/tangled.sh/core/appview/pages"
25
"tangled.sh/tangled.sh/core/appview/pages/markup"
26
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
···
46
return
47
}
48
49
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
50
if err != nil {
51
log.Printf("failed to create unsigned client for %s", f.Knot)
52
s.pages.Error503(w)
···
120
121
emails := uniqueEmails(commitsTrunc)
122
123
+
user := s.oauth.GetUser(r)
124
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
125
LoggedInUser: user,
126
RepoInfo: f.RepoInfo(s, user),
···
151
152
ref := chi.URLParam(r, "ref")
153
154
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
155
if err != nil {
156
log.Println("failed to create unsigned client", err)
157
return
···
191
tagMap[hash] = append(tagMap[hash], tag.Name)
192
}
193
194
+
user := s.oauth.GetUser(r)
195
s.pages.RepoLog(w, pages.RepoLogParams{
196
LoggedInUser: user,
197
TagMap: tagMap,
···
210
return
211
}
212
213
+
user := s.oauth.GetUser(r)
214
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
215
RepoInfo: f.RepoInfo(s, user),
216
})
···
233
return
234
}
235
236
+
user := s.oauth.GetUser(r)
237
238
switch r.Method {
239
case http.MethodGet:
···
242
})
243
return
244
case http.MethodPut:
245
+
user := s.oauth.GetUser(r)
246
newDescription := r.FormValue("description")
247
+
client, err := s.oauth.AuthorizedClient(r)
248
+
if err != nil {
249
+
log.Println("failed to get client")
250
+
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
251
+
return
252
+
}
253
254
// optimistic update
255
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
···
262
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
263
//
264
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
265
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
266
if err != nil {
267
// failed to get record
268
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
269
return
270
}
271
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
272
Collection: tangled.RepoNSID,
273
Repo: user.Did,
274
Rkey: rkey,
···
309
}
310
ref := chi.URLParam(r, "ref")
311
protocol := "http"
312
+
if !s.config.Core.Dev {
313
protocol = "https"
314
}
315
···
337
return
338
}
339
340
+
user := s.oauth.GetUser(r)
341
s.pages.RepoCommit(w, pages.RepoCommitParams{
342
LoggedInUser: user,
343
RepoInfo: f.RepoInfo(s, user),
···
357
ref := chi.URLParam(r, "ref")
358
treePath := chi.URLParam(r, "*")
359
protocol := "http"
360
+
if !s.config.Core.Dev {
361
protocol = "https"
362
}
363
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
···
386
return
387
}
388
389
+
user := s.oauth.GetUser(r)
390
391
var breadcrumbs [][]string
392
breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
417
return
418
}
419
420
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
421
if err != nil {
422
log.Println("failed to create unsigned client", err)
423
return
···
457
}
458
}
459
460
+
user := s.oauth.GetUser(r)
461
s.pages.RepoTags(w, pages.RepoTagsParams{
462
LoggedInUser: user,
463
RepoInfo: f.RepoInfo(s, user),
···
475
return
476
}
477
478
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
479
if err != nil {
480
log.Println("failed to create unsigned client", err)
481
return
···
517
return strings.Compare(a.Name, b.Name) * -1
518
})
519
520
+
user := s.oauth.GetUser(r)
521
s.pages.RepoBranches(w, pages.RepoBranchesParams{
522
LoggedInUser: user,
523
RepoInfo: f.RepoInfo(s, user),
···
536
ref := chi.URLParam(r, "ref")
537
filePath := chi.URLParam(r, "*")
538
protocol := "http"
539
+
if !s.config.Core.Dev {
540
protocol = "https"
541
}
542
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
574
showRendered = r.URL.Query().Get("code") != "true"
575
}
576
577
+
user := s.oauth.GetUser(r)
578
s.pages.RepoBlob(w, pages.RepoBlobParams{
579
LoggedInUser: user,
580
RepoInfo: f.RepoInfo(s, user),
···
597
filePath := chi.URLParam(r, "*")
598
599
protocol := "http"
600
+
if !s.config.Core.Dev {
601
protocol = "https"
602
}
603
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
658
return
659
}
660
661
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
662
if err != nil {
663
log.Println("failed to create client to ", f.Knot)
664
return
···
720
}
721
722
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
723
+
user := s.oauth.GetUser(r)
724
725
f, err := s.fullyResolvedRepo(r)
726
if err != nil {
···
729
}
730
731
// remove record from pds
732
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
733
+
if err != nil {
734
+
log.Println("failed to get authorized client", err)
735
+
return
736
+
}
737
repoRkey := f.RepoAt.RecordKey().String()
738
+
_, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
739
Collection: tangled.RepoNSID,
740
Repo: user.Did,
741
Rkey: repoRkey,
···
753
return
754
}
755
756
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
757
if err != nil {
758
log.Println("failed to create client to ", f.Knot)
759
return
···
848
return
849
}
850
851
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
852
if err != nil {
853
log.Println("failed to create client to ", f.Knot)
854
return
···
878
switch r.Method {
879
case http.MethodGet:
880
// for now, this is just pubkeys
881
+
user := s.oauth.GetUser(r)
882
repoCollaborators, err := f.Collaborators(r.Context(), s)
883
if err != nil {
884
log.Println("failed to get collaborators", err)
···
894
895
var branchNames []string
896
var defaultBranch string
897
+
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
898
if err != nil {
899
log.Println("failed to create unsigned client", err)
900
} else {
···
1018
return collaborators, nil
1019
}
1020
1021
+
func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo {
1022
isStarred := false
1023
if u != nil {
1024
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
···
1061
1062
knot := f.Knot
1063
var disableFork bool
1064
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
1065
if err != nil {
1066
log.Printf("failed to create unsigned client for %s: %v", knot, err)
1067
} else {
···
1115
}
1116
1117
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
1118
+
user := s.oauth.GetUser(r)
1119
f, err := s.fullyResolvedRepo(r)
1120
if err != nil {
1121
log.Println("failed to get repo and knot", err)
···
1169
}
1170
1171
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
1172
+
user := s.oauth.GetUser(r)
1173
f, err := s.fullyResolvedRepo(r)
1174
if err != nil {
1175
log.Println("failed to get repo and knot", err)
···
1205
1206
closed := tangled.RepoIssueStateClosed
1207
1208
+
client, err := s.oauth.AuthorizedClient(r)
1209
+
if err != nil {
1210
+
log.Println("failed to get authorized client", err)
1211
+
return
1212
+
}
1213
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1214
Collection: tangled.RepoIssueStateNSID,
1215
Repo: user.Did,
1216
Rkey: appview.TID(),
···
1228
return
1229
}
1230
1231
+
err = db.CloseIssue(s.db, f.RepoAt, issueIdInt)
1232
if err != nil {
1233
log.Println("failed to close issue", err)
1234
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
1245
}
1246
1247
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
1248
+
user := s.oauth.GetUser(r)
1249
f, err := s.fullyResolvedRepo(r)
1250
if err != nil {
1251
log.Println("failed to get repo and knot", err)
···
1293
}
1294
1295
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
1296
+
user := s.oauth.GetUser(r)
1297
f, err := s.fullyResolvedRepo(r)
1298
if err != nil {
1299
log.Println("failed to get repo and knot", err)
···
1344
}
1345
1346
atUri := f.RepoAt.String()
1347
+
client, err := s.oauth.AuthorizedClient(r)
1348
+
if err != nil {
1349
+
log.Println("failed to get authorized client", err)
1350
+
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1351
+
return
1352
+
}
1353
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1354
Collection: tangled.RepoIssueCommentNSID,
1355
Repo: user.Did,
1356
Rkey: rkey,
···
1377
}
1378
1379
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
1380
+
user := s.oauth.GetUser(r)
1381
f, err := s.fullyResolvedRepo(r)
1382
if err != nil {
1383
log.Println("failed to get repo and knot", err)
···
1436
}
1437
1438
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
1439
+
user := s.oauth.GetUser(r)
1440
f, err := s.fullyResolvedRepo(r)
1441
if err != nil {
1442
log.Println("failed to get repo and knot", err)
···
1488
case http.MethodPost:
1489
// extract form value
1490
newBody := r.FormValue("body")
1491
+
client, err := s.oauth.AuthorizedClient(r)
1492
+
if err != nil {
1493
+
log.Println("failed to get authorized client", err)
1494
+
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
1495
+
return
1496
+
}
1497
rkey := comment.Rkey
1498
1499
// optimistic update
···
1508
// rkey is optional, it was introduced later
1509
if comment.Rkey != "" {
1510
// update the record on pds
1511
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey)
1512
if err != nil {
1513
// failed to get record
1514
log.Println(err, rkey)
···
1523
createdAt := record["createdAt"].(string)
1524
commentIdInt64 := int64(commentIdInt)
1525
1526
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1527
Collection: tangled.RepoIssueCommentNSID,
1528
Repo: user.Did,
1529
Rkey: rkey,
···
1566
}
1567
1568
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
1569
+
user := s.oauth.GetUser(r)
1570
f, err := s.fullyResolvedRepo(r)
1571
if err != nil {
1572
log.Println("failed to get repo and knot", err)
···
1623
1624
// delete from pds
1625
if comment.Rkey != "" {
1626
+
client, err := s.oauth.AuthorizedClient(r)
1627
+
if err != nil {
1628
+
log.Println("failed to get authorized client", err)
1629
+
s.pages.Notice(w, "issue-comment", "Failed to delete comment.")
1630
+
return
1631
+
}
1632
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
1633
Collection: tangled.GraphFollowNSID,
1634
Repo: user.Did,
1635
Rkey: comment.Rkey,
···
1676
page = pagination.FirstPage()
1677
}
1678
1679
+
user := s.oauth.GetUser(r)
1680
f, err := s.fullyResolvedRepo(r)
1681
if err != nil {
1682
log.Println("failed to get repo and knot", err)
···
1705
}
1706
1707
s.pages.RepoIssues(w, pages.RepoIssuesParams{
1708
+
LoggedInUser: s.oauth.GetUser(r),
1709
RepoInfo: f.RepoInfo(s, user),
1710
Issues: issues,
1711
DidHandleMap: didHandleMap,
···
1716
}
1717
1718
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
1719
+
user := s.oauth.GetUser(r)
1720
1721
f, err := s.fullyResolvedRepo(r)
1722
if err != nil {
···
1764
return
1765
}
1766
1767
+
client, err := s.oauth.AuthorizedClient(r)
1768
+
if err != nil {
1769
+
log.Println("failed to get authorized client", err)
1770
+
s.pages.Notice(w, "issues", "Failed to create issue.")
1771
+
return
1772
+
}
1773
atUri := f.RepoAt.String()
1774
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1775
Collection: tangled.RepoIssueNSID,
1776
Repo: user.Did,
1777
Rkey: appview.TID(),
···
1804
}
1805
1806
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
1807
+
user := s.oauth.GetUser(r)
1808
f, err := s.fullyResolvedRepo(r)
1809
if err != nil {
1810
log.Printf("failed to resolve source repo: %v", err)
···
1813
1814
switch r.Method {
1815
case http.MethodGet:
1816
+
user := s.oauth.GetUser(r)
1817
knots, err := s.enforcer.GetDomainsForUser(user.Did)
1818
if err != nil {
1819
s.pages.Notice(w, "repo", "Invalid user account.")
···
1863
return
1864
}
1865
1866
+
client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev)
1867
if err != nil {
1868
s.pages.Notice(w, "repo", "Failed to reach knot server.")
1869
return
1870
}
1871
1872
var uri string
1873
+
if s.config.Core.Dev {
1874
uri = "http"
1875
} else {
1876
uri = "https"
···
1917
// continue
1918
}
1919
1920
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
1921
+
if err != nil {
1922
+
log.Println("failed to get authorized client", err)
1923
+
s.pages.Notice(w, "repo", "Failed to create repository.")
1924
+
return
1925
+
}
1926
1927
createdAt := time.Now().Format(time.RFC3339)
1928
+
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
1929
Collection: tangled.RepoNSID,
1930
Repo: user.Did,
1931
Rkey: rkey,
+4
-3
appview/state/repo_util.go
+4
-3
appview/state/repo_util.go
···
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"github.com/go-chi/chi/v5"
14
"github.com/go-git/go-git/v5/plumbing/object"
15
-
"tangled.sh/tangled.sh/core/appview/auth"
16
"tangled.sh/tangled.sh/core/appview/db"
17
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
18
)
19
···
45
ref := chi.URLParam(r, "ref")
46
47
if ref == "" {
48
-
us, err := NewUnsignedClient(knot, s.config.Dev)
49
if err != nil {
50
return nil, err
51
}
···
73
}, nil
74
}
75
76
-
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
77
if u != nil {
78
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
79
return repoinfo.RolesInRepo{r}
···
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"github.com/go-chi/chi/v5"
14
"github.com/go-git/go-git/v5/plumbing/object"
15
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/appview/knotclient"
17
+
"tangled.sh/tangled.sh/core/appview/oauth"
18
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
19
)
20
···
46
ref := chi.URLParam(r, "ref")
47
48
if ref == "" {
49
+
us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev)
50
if err != nil {
51
return nil, err
52
}
···
74
}, nil
75
}
76
77
+
func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo {
78
if u != nil {
79
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
80
return repoinfo.RolesInRepo{r}
+42
-20
appview/state/router.go
+42
-20
appview/state/router.go
···
5
"strings"
6
7
"github.com/go-chi/chi/v5"
8
"tangled.sh/tangled.sh/core/appview/middleware"
9
"tangled.sh/tangled.sh/core/appview/settings"
10
"tangled.sh/tangled.sh/core/appview/state/userutil"
11
)
···
53
r.Use(StripLeadingAt)
54
55
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
56
-
r.Get("/", s.ProfilePage)
57
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
58
r.Get("/", s.RepoIndex)
59
r.Get("/commits/{ref}", s.RepoLog)
60
r.Route("/tree/{ref}", func(r chi.Router) {
···
66
r.Route("/tags", func(r chi.Router) {
67
r.Get("/", s.RepoTags)
68
r.Route("/{tag}", func(r chi.Router) {
69
-
r.Use(middleware.AuthMiddleware(s.auth))
70
// require auth to download for now
71
r.Get("/download/{file}", s.DownloadArtifact)
72
···
89
r.Get("/{issue}", s.RepoSingleIssue)
90
91
r.Group(func(r chi.Router) {
92
-
r.Use(middleware.AuthMiddleware(s.auth))
93
r.Get("/new", s.NewIssue)
94
r.Post("/new", s.NewIssue)
95
r.Post("/{issue}/comment", s.NewIssueComment)
···
105
})
106
107
r.Route("/fork", func(r chi.Router) {
108
-
r.Use(middleware.AuthMiddleware(s.auth))
109
r.Get("/", s.ForkRepo)
110
r.Post("/", s.ForkRepo)
111
})
112
113
r.Route("/pulls", func(r chi.Router) {
114
r.Get("/", s.RepoPulls)
115
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) {
116
r.Get("/", s.NewPull)
117
r.Get("/patch-upload", s.PatchUploadFragment)
118
r.Post("/validate-patch", s.ValidatePatch)
···
130
r.Get("/", s.RepoPullPatch)
131
r.Get("/interdiff", s.RepoPullInterdiff)
132
r.Get("/actions", s.PullActions)
133
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) {
134
r.Get("/", s.PullComment)
135
r.Post("/", s.PullComment)
136
})
···
141
})
142
143
r.Group(func(r chi.Router) {
144
-
r.Use(middleware.AuthMiddleware(s.auth))
145
r.Route("/resubmit", func(r chi.Router) {
146
r.Get("/", s.ResubmitPull)
147
r.Post("/", s.ResubmitPull)
···
164
165
// settings routes, needs auth
166
r.Group(func(r chi.Router) {
167
-
r.Use(middleware.AuthMiddleware(s.auth))
168
// repo description can only be edited by owner
169
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
170
r.Put("/", s.RepoDescription)
···
195
196
r.Get("/", s.Timeline)
197
198
-
r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout)
199
-
200
-
r.Route("/login", func(r chi.Router) {
201
-
r.Get("/", s.Login)
202
-
r.Post("/", s.Login)
203
-
})
204
205
r.Route("/knots", func(r chi.Router) {
206
-
r.Use(middleware.AuthMiddleware(s.auth))
207
r.Get("/", s.Knots)
208
r.Post("/key", s.RegistrationKey)
209
···
221
222
r.Route("/repo", func(r chi.Router) {
223
r.Route("/new", func(r chi.Router) {
224
-
r.Use(middleware.AuthMiddleware(s.auth))
225
r.Get("/", s.NewRepo)
226
r.Post("/", s.NewRepo)
227
})
228
// r.Post("/import", s.ImportRepo)
229
})
230
231
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) {
232
r.Post("/", s.Follow)
233
r.Delete("/", s.Follow)
234
})
235
236
-
r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) {
237
r.Post("/", s.Star)
238
r.Delete("/", s.Star)
239
})
240
241
-
r.Mount("/settings", s.SettingsRouter())
242
243
r.Get("/keys/{user}", s.Keys)
244
245
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
···
248
return r
249
}
250
251
func (s *State) SettingsRouter() http.Handler {
252
settings := &settings.Settings{
253
Db: s.db,
254
-
Auth: s.auth,
255
Pages: s.pages,
256
Config: s.config,
257
}
···
5
"strings"
6
7
"github.com/go-chi/chi/v5"
8
+
"github.com/gorilla/sessions"
9
"tangled.sh/tangled.sh/core/appview/middleware"
10
+
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
11
"tangled.sh/tangled.sh/core/appview/settings"
12
"tangled.sh/tangled.sh/core/appview/state/userutil"
13
)
···
55
r.Use(StripLeadingAt)
56
57
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
58
+
r.Get("/", s.Profile)
59
+
60
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
61
+
r.Use(GoImport(s))
62
+
63
r.Get("/", s.RepoIndex)
64
r.Get("/commits/{ref}", s.RepoLog)
65
r.Route("/tree/{ref}", func(r chi.Router) {
···
71
r.Route("/tags", func(r chi.Router) {
72
r.Get("/", s.RepoTags)
73
r.Route("/{tag}", func(r chi.Router) {
74
+
r.Use(middleware.AuthMiddleware(s.oauth))
75
// require auth to download for now
76
r.Get("/download/{file}", s.DownloadArtifact)
77
···
94
r.Get("/{issue}", s.RepoSingleIssue)
95
96
r.Group(func(r chi.Router) {
97
+
r.Use(middleware.AuthMiddleware(s.oauth))
98
r.Get("/new", s.NewIssue)
99
r.Post("/new", s.NewIssue)
100
r.Post("/{issue}/comment", s.NewIssueComment)
···
110
})
111
112
r.Route("/fork", func(r chi.Router) {
113
+
r.Use(middleware.AuthMiddleware(s.oauth))
114
r.Get("/", s.ForkRepo)
115
r.Post("/", s.ForkRepo)
116
})
117
118
r.Route("/pulls", func(r chi.Router) {
119
r.Get("/", s.RepoPulls)
120
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) {
121
r.Get("/", s.NewPull)
122
r.Get("/patch-upload", s.PatchUploadFragment)
123
r.Post("/validate-patch", s.ValidatePatch)
···
135
r.Get("/", s.RepoPullPatch)
136
r.Get("/interdiff", s.RepoPullInterdiff)
137
r.Get("/actions", s.PullActions)
138
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
139
r.Get("/", s.PullComment)
140
r.Post("/", s.PullComment)
141
})
···
146
})
147
148
r.Group(func(r chi.Router) {
149
+
r.Use(middleware.AuthMiddleware(s.oauth))
150
r.Route("/resubmit", func(r chi.Router) {
151
r.Get("/", s.ResubmitPull)
152
r.Post("/", s.ResubmitPull)
···
169
170
// settings routes, needs auth
171
r.Group(func(r chi.Router) {
172
+
r.Use(middleware.AuthMiddleware(s.oauth))
173
// repo description can only be edited by owner
174
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
175
r.Put("/", s.RepoDescription)
···
200
201
r.Get("/", s.Timeline)
202
203
+
r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout)
204
205
r.Route("/knots", func(r chi.Router) {
206
+
r.Use(middleware.AuthMiddleware(s.oauth))
207
r.Get("/", s.Knots)
208
r.Post("/key", s.RegistrationKey)
209
···
221
222
r.Route("/repo", func(r chi.Router) {
223
r.Route("/new", func(r chi.Router) {
224
+
r.Use(middleware.AuthMiddleware(s.oauth))
225
r.Get("/", s.NewRepo)
226
r.Post("/", s.NewRepo)
227
})
228
// r.Post("/import", s.ImportRepo)
229
})
230
231
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) {
232
r.Post("/", s.Follow)
233
r.Delete("/", s.Follow)
234
})
235
236
+
r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) {
237
r.Post("/", s.Star)
238
r.Delete("/", s.Star)
239
})
240
241
+
r.Route("/profile", func(r chi.Router) {
242
+
r.Use(middleware.AuthMiddleware(s.oauth))
243
+
r.Get("/edit-bio", s.EditBioFragment)
244
+
r.Get("/edit-pins", s.EditPinsFragment)
245
+
r.Post("/bio", s.UpdateProfileBio)
246
+
r.Post("/pins", s.UpdateProfilePins)
247
+
})
248
249
+
r.Mount("/settings", s.SettingsRouter())
250
+
r.Mount("/", s.OAuthRouter())
251
r.Get("/keys/{user}", s.Keys)
252
253
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
···
256
return r
257
}
258
259
+
func (s *State) OAuthRouter() http.Handler {
260
+
oauth := &oauthhandler.OAuthHandler{
261
+
Config: s.config,
262
+
Pages: s.pages,
263
+
Resolver: s.resolver,
264
+
Db: s.db,
265
+
Store: sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)),
266
+
OAuth: s.oauth,
267
+
Enforcer: s.enforcer,
268
+
}
269
+
270
+
return oauth.Router()
271
+
}
272
+
273
func (s *State) SettingsRouter() http.Handler {
274
settings := &settings.Settings{
275
Db: s.db,
276
+
OAuth: s.oauth,
277
Pages: s.pages,
278
Config: s.config,
279
}
-489
appview/state/signer.go
-489
appview/state/signer.go
···
1
-
package state
2
-
3
-
import (
4
-
"bytes"
5
-
"crypto/hmac"
6
-
"crypto/sha256"
7
-
"encoding/hex"
8
-
"encoding/json"
9
-
"fmt"
10
-
"io"
11
-
"log"
12
-
"net/http"
13
-
"net/url"
14
-
"strconv"
15
-
"time"
16
-
17
-
"tangled.sh/tangled.sh/core/types"
18
-
)
19
-
20
-
type SignerTransport struct {
21
-
Secret string
22
-
}
23
-
24
-
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
25
-
timestamp := time.Now().Format(time.RFC3339)
26
-
mac := hmac.New(sha256.New, []byte(s.Secret))
27
-
message := req.Method + req.URL.Path + timestamp
28
-
mac.Write([]byte(message))
29
-
signature := hex.EncodeToString(mac.Sum(nil))
30
-
req.Header.Set("X-Signature", signature)
31
-
req.Header.Set("X-Timestamp", timestamp)
32
-
return http.DefaultTransport.RoundTrip(req)
33
-
}
34
-
35
-
type SignedClient struct {
36
-
Secret string
37
-
Url *url.URL
38
-
client *http.Client
39
-
}
40
-
41
-
func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
42
-
client := &http.Client{
43
-
Timeout: 5 * time.Second,
44
-
Transport: SignerTransport{
45
-
Secret: secret,
46
-
},
47
-
}
48
-
49
-
scheme := "https"
50
-
if dev {
51
-
scheme = "http"
52
-
}
53
-
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
54
-
if err != nil {
55
-
return nil, err
56
-
}
57
-
58
-
signedClient := &SignedClient{
59
-
Secret: secret,
60
-
client: client,
61
-
Url: url,
62
-
}
63
-
64
-
return signedClient, nil
65
-
}
66
-
67
-
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
68
-
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
69
-
}
70
-
71
-
func (s *SignedClient) Init(did string) (*http.Response, error) {
72
-
const (
73
-
Method = "POST"
74
-
Endpoint = "/init"
75
-
)
76
-
77
-
body, _ := json.Marshal(map[string]any{
78
-
"did": did,
79
-
})
80
-
81
-
req, err := s.newRequest(Method, Endpoint, body)
82
-
if err != nil {
83
-
return nil, err
84
-
}
85
-
86
-
return s.client.Do(req)
87
-
}
88
-
89
-
func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
90
-
const (
91
-
Method = "PUT"
92
-
Endpoint = "/repo/new"
93
-
)
94
-
95
-
body, _ := json.Marshal(map[string]any{
96
-
"did": did,
97
-
"name": repoName,
98
-
"default_branch": defaultBranch,
99
-
})
100
-
101
-
req, err := s.newRequest(Method, Endpoint, body)
102
-
if err != nil {
103
-
return nil, err
104
-
}
105
-
106
-
return s.client.Do(req)
107
-
}
108
-
109
-
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
110
-
const (
111
-
Method = "POST"
112
-
Endpoint = "/repo/fork"
113
-
)
114
-
115
-
body, _ := json.Marshal(map[string]any{
116
-
"did": ownerDid,
117
-
"source": source,
118
-
"name": name,
119
-
})
120
-
121
-
req, err := s.newRequest(Method, Endpoint, body)
122
-
if err != nil {
123
-
return nil, err
124
-
}
125
-
126
-
return s.client.Do(req)
127
-
}
128
-
129
-
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
130
-
const (
131
-
Method = "DELETE"
132
-
Endpoint = "/repo"
133
-
)
134
-
135
-
body, _ := json.Marshal(map[string]any{
136
-
"did": did,
137
-
"name": repoName,
138
-
})
139
-
140
-
req, err := s.newRequest(Method, Endpoint, body)
141
-
if err != nil {
142
-
return nil, err
143
-
}
144
-
145
-
return s.client.Do(req)
146
-
}
147
-
148
-
func (s *SignedClient) AddMember(did string) (*http.Response, error) {
149
-
const (
150
-
Method = "PUT"
151
-
Endpoint = "/member/add"
152
-
)
153
-
154
-
body, _ := json.Marshal(map[string]any{
155
-
"did": did,
156
-
})
157
-
158
-
req, err := s.newRequest(Method, Endpoint, body)
159
-
if err != nil {
160
-
return nil, err
161
-
}
162
-
163
-
return s.client.Do(req)
164
-
}
165
-
166
-
func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
167
-
const (
168
-
Method = "PUT"
169
-
)
170
-
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
171
-
172
-
body, _ := json.Marshal(map[string]any{
173
-
"branch": branch,
174
-
})
175
-
176
-
req, err := s.newRequest(Method, endpoint, body)
177
-
if err != nil {
178
-
return nil, err
179
-
}
180
-
181
-
return s.client.Do(req)
182
-
}
183
-
184
-
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
185
-
const (
186
-
Method = "POST"
187
-
)
188
-
endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
189
-
190
-
body, _ := json.Marshal(map[string]any{
191
-
"did": memberDid,
192
-
})
193
-
194
-
req, err := s.newRequest(Method, endpoint, body)
195
-
if err != nil {
196
-
return nil, err
197
-
}
198
-
199
-
return s.client.Do(req)
200
-
}
201
-
202
-
func (s *SignedClient) Merge(
203
-
patch []byte,
204
-
ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
205
-
) (*http.Response, error) {
206
-
const (
207
-
Method = "POST"
208
-
)
209
-
endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
210
-
211
-
mr := types.MergeRequest{
212
-
Branch: branch,
213
-
CommitMessage: commitMessage,
214
-
CommitBody: commitBody,
215
-
AuthorName: authorName,
216
-
AuthorEmail: authorEmail,
217
-
Patch: string(patch),
218
-
}
219
-
220
-
body, _ := json.Marshal(mr)
221
-
222
-
req, err := s.newRequest(Method, endpoint, body)
223
-
if err != nil {
224
-
return nil, err
225
-
}
226
-
227
-
return s.client.Do(req)
228
-
}
229
-
230
-
func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
231
-
const (
232
-
Method = "POST"
233
-
)
234
-
endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
235
-
236
-
body, _ := json.Marshal(map[string]any{
237
-
"patch": string(patch),
238
-
"branch": branch,
239
-
})
240
-
241
-
req, err := s.newRequest(Method, endpoint, body)
242
-
if err != nil {
243
-
return nil, err
244
-
}
245
-
246
-
return s.client.Do(req)
247
-
}
248
-
249
-
func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
250
-
const (
251
-
Method = "POST"
252
-
)
253
-
endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
254
-
255
-
req, err := s.newRequest(Method, endpoint, nil)
256
-
if err != nil {
257
-
return nil, err
258
-
}
259
-
260
-
return s.client.Do(req)
261
-
}
262
-
263
-
type UnsignedClient struct {
264
-
Url *url.URL
265
-
client *http.Client
266
-
}
267
-
268
-
func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) {
269
-
client := &http.Client{
270
-
Timeout: 5 * time.Second,
271
-
}
272
-
273
-
scheme := "https"
274
-
if dev {
275
-
scheme = "http"
276
-
}
277
-
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
278
-
if err != nil {
279
-
return nil, err
280
-
}
281
-
282
-
unsignedClient := &UnsignedClient{
283
-
client: client,
284
-
Url: url,
285
-
}
286
-
287
-
return unsignedClient, nil
288
-
}
289
-
290
-
func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) {
291
-
reqUrl := us.Url.JoinPath(endpoint)
292
-
293
-
// add query parameters
294
-
if query != nil {
295
-
reqUrl.RawQuery = query.Encode()
296
-
}
297
-
298
-
return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body))
299
-
}
300
-
301
-
func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) {
302
-
const (
303
-
Method = "GET"
304
-
)
305
-
306
-
endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref)
307
-
if ref == "" {
308
-
endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName)
309
-
}
310
-
311
-
req, err := us.newRequest(Method, endpoint, nil, nil)
312
-
if err != nil {
313
-
return nil, err
314
-
}
315
-
316
-
return us.client.Do(req)
317
-
}
318
-
319
-
func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) {
320
-
const (
321
-
Method = "GET"
322
-
)
323
-
324
-
endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref))
325
-
326
-
query := url.Values{}
327
-
query.Add("page", strconv.Itoa(page))
328
-
query.Add("per_page", strconv.Itoa(60))
329
-
330
-
req, err := us.newRequest(Method, endpoint, query, nil)
331
-
if err != nil {
332
-
return nil, err
333
-
}
334
-
335
-
return us.client.Do(req)
336
-
}
337
-
338
-
func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) {
339
-
const (
340
-
Method = "GET"
341
-
)
342
-
343
-
endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName)
344
-
345
-
req, err := us.newRequest(Method, endpoint, nil, nil)
346
-
if err != nil {
347
-
return nil, err
348
-
}
349
-
350
-
return us.client.Do(req)
351
-
}
352
-
353
-
func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) {
354
-
const (
355
-
Method = "GET"
356
-
)
357
-
358
-
endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName)
359
-
360
-
req, err := us.newRequest(Method, endpoint, nil, nil)
361
-
if err != nil {
362
-
return nil, err
363
-
}
364
-
365
-
resp, err := us.client.Do(req)
366
-
if err != nil {
367
-
return nil, err
368
-
}
369
-
370
-
body, err := io.ReadAll(resp.Body)
371
-
if err != nil {
372
-
return nil, err
373
-
}
374
-
375
-
var result types.RepoTagsResponse
376
-
err = json.Unmarshal(body, &result)
377
-
if err != nil {
378
-
return nil, err
379
-
}
380
-
381
-
return &result, nil
382
-
}
383
-
384
-
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
385
-
const (
386
-
Method = "GET"
387
-
)
388
-
389
-
endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch))
390
-
391
-
req, err := us.newRequest(Method, endpoint, nil, nil)
392
-
if err != nil {
393
-
return nil, err
394
-
}
395
-
396
-
return us.client.Do(req)
397
-
}
398
-
399
-
func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) {
400
-
const (
401
-
Method = "GET"
402
-
)
403
-
404
-
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
405
-
406
-
req, err := us.newRequest(Method, endpoint, nil, nil)
407
-
if err != nil {
408
-
return nil, err
409
-
}
410
-
411
-
resp, err := us.client.Do(req)
412
-
if err != nil {
413
-
return nil, err
414
-
}
415
-
defer resp.Body.Close()
416
-
417
-
var defaultBranch types.RepoDefaultBranchResponse
418
-
if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil {
419
-
return nil, err
420
-
}
421
-
422
-
return &defaultBranch, nil
423
-
}
424
-
425
-
func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) {
426
-
const (
427
-
Method = "GET"
428
-
Endpoint = "/capabilities"
429
-
)
430
-
431
-
req, err := us.newRequest(Method, Endpoint, nil, nil)
432
-
if err != nil {
433
-
return nil, err
434
-
}
435
-
436
-
resp, err := us.client.Do(req)
437
-
if err != nil {
438
-
return nil, err
439
-
}
440
-
defer resp.Body.Close()
441
-
442
-
var capabilities types.Capabilities
443
-
if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil {
444
-
return nil, err
445
-
}
446
-
447
-
return &capabilities, nil
448
-
}
449
-
450
-
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) {
451
-
const (
452
-
Method = "GET"
453
-
)
454
-
455
-
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2))
456
-
457
-
req, err := us.newRequest(Method, endpoint, nil, nil)
458
-
if err != nil {
459
-
return nil, fmt.Errorf("Failed to create request.")
460
-
}
461
-
462
-
compareResp, err := us.client.Do(req)
463
-
if err != nil {
464
-
return nil, fmt.Errorf("Failed to create request.")
465
-
}
466
-
defer compareResp.Body.Close()
467
-
468
-
switch compareResp.StatusCode {
469
-
case 404:
470
-
case 400:
471
-
return nil, fmt.Errorf("Branch comparisons not supported on this knot.")
472
-
}
473
-
474
-
respBody, err := io.ReadAll(compareResp.Body)
475
-
if err != nil {
476
-
log.Println("failed to compare across branches")
477
-
return nil, fmt.Errorf("Failed to compare branches.")
478
-
}
479
-
defer compareResp.Body.Close()
480
-
481
-
var formatPatchResponse types.RepoFormatPatchResponse
482
-
err = json.Unmarshal(respBody, &formatPatchResponse)
483
-
if err != nil {
484
-
log.Println("failed to unmarshal format-patch response", err)
485
-
return nil, fmt.Errorf("failed to compare branches.")
486
-
}
487
-
488
-
return &formatPatchResponse, nil
489
-
}
···
+9
-4
appview/state/star.go
+9
-4
appview/state/star.go
···
15
)
16
17
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
18
-
currentUser := s.auth.GetUser(r)
19
20
subject := r.URL.Query().Get("subject")
21
if subject == "" {
···
29
return
30
}
31
32
-
client, _ := s.auth.AuthorizedClient(r)
33
34
switch r.Method {
35
case http.MethodPost:
36
createdAt := time.Now().Format(time.RFC3339)
37
rkey := appview.TID()
38
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
39
Collection: tangled.FeedStarNSID,
40
Repo: currentUser.Did,
41
Rkey: rkey,
···
80
return
81
}
82
83
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
84
Collection: tangled.FeedStarNSID,
85
Repo: currentUser.Did,
86
Rkey: star.Rkey,
···
100
starCount, err := db.GetStarCount(s.db, subjectUri)
101
if err != nil {
102
log.Println("failed to get star count for ", subjectUri)
103
}
104
105
s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
···
15
)
16
17
func (s *State) Star(w http.ResponseWriter, r *http.Request) {
18
+
currentUser := s.oauth.GetUser(r)
19
20
subject := r.URL.Query().Get("subject")
21
if subject == "" {
···
29
return
30
}
31
32
+
client, err := s.oauth.AuthorizedClient(r)
33
+
if err != nil {
34
+
log.Println("failed to authorize client", err)
35
+
return
36
+
}
37
38
switch r.Method {
39
case http.MethodPost:
40
createdAt := time.Now().Format(time.RFC3339)
41
rkey := appview.TID()
42
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
43
Collection: tangled.FeedStarNSID,
44
Repo: currentUser.Did,
45
Rkey: rkey,
···
84
return
85
}
86
87
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
88
Collection: tangled.FeedStarNSID,
89
Repo: currentUser.Did,
90
Rkey: star.Rkey,
···
104
starCount, err := db.GetStarCount(s.db, subjectUri)
105
if err != nil {
106
log.Println("failed to get star count for ", subjectUri)
107
+
return
108
}
109
110
s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
+141
-104
appview/state/state.go
+141
-104
appview/state/state.go
···
19
"github.com/go-chi/chi/v5"
20
"tangled.sh/tangled.sh/core/api/tangled"
21
"tangled.sh/tangled.sh/core/appview"
22
-
"tangled.sh/tangled.sh/core/appview/auth"
23
"tangled.sh/tangled.sh/core/appview/db"
24
"tangled.sh/tangled.sh/core/appview/pages"
25
"tangled.sh/tangled.sh/core/jetstream"
26
"tangled.sh/tangled.sh/core/rbac"
···
28
29
type State struct {
30
db *db.DB
31
-
auth *auth.Auth
32
enforcer *rbac.Enforcer
33
-
tidClock *syntax.TIDClock
34
pages *pages.Pages
35
resolver *appview.Resolver
36
jc *jetstream.JetstreamClient
···
38
}
39
40
func Make(config *appview.Config) (*State, error) {
41
-
d, err := db.Make(config.DbPath)
42
-
if err != nil {
43
-
return nil, err
44
-
}
45
-
46
-
auth, err := auth.Make(config.CookieSecret)
47
if err != nil {
48
return nil, err
49
}
50
51
-
enforcer, err := rbac.NewEnforcer(config.DbPath)
52
if err != nil {
53
return nil, err
54
}
···
59
60
resolver := appview.NewResolver()
61
62
wrapper := db.DbWrapper{d}
63
jc, err := jetstream.NewJetstreamClient(
64
-
config.JetstreamEndpoint,
65
"appview",
66
-
[]string{tangled.GraphFollowNSID, tangled.FeedStarNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID},
67
nil,
68
slog.Default(),
69
wrapper,
···
72
if err != nil {
73
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
74
}
75
-
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
76
if err != nil {
77
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
78
}
79
80
state := &State{
81
d,
82
-
auth,
83
enforcer,
84
clock,
85
pgs,
···
95
return c.Next().String()
96
}
97
98
-
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
99
-
ctx := r.Context()
100
101
-
switch r.Method {
102
-
case http.MethodGet:
103
-
err := s.pages.Login(w, pages.LoginParams{})
104
-
if err != nil {
105
-
log.Printf("rendering login page: %s", err)
106
-
}
107
108
-
return
109
-
case http.MethodPost:
110
-
handle := strings.TrimPrefix(r.FormValue("handle"), "@")
111
-
appPassword := r.FormValue("app_password")
112
113
-
resolved, err := s.resolver.ResolveIdent(ctx, handle)
114
-
if err != nil {
115
-
log.Println("failed to resolve handle:", err)
116
-
s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
117
-
return
118
-
}
119
120
-
atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
121
-
if err != nil {
122
-
s.pages.Notice(w, "login-msg", "Invalid handle or password.")
123
-
return
124
-
}
125
-
sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
126
127
-
err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
128
-
if err != nil {
129
-
s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
130
-
return
131
-
}
132
133
-
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
134
135
-
did := resolved.DID.String()
136
-
defaultKnot := "knot1.tangled.sh"
137
138
-
go func() {
139
-
log.Printf("adding %s to default knot", did)
140
-
err = s.enforcer.AddMember(defaultKnot, did)
141
-
if err != nil {
142
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
143
-
return
144
-
}
145
-
err = s.enforcer.E.SavePolicy()
146
-
if err != nil {
147
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
148
-
return
149
-
}
150
151
-
secret, err := db.GetRegistrationKey(s.db, defaultKnot)
152
-
if err != nil {
153
-
log.Println("failed to get registration key for knot1.tangled.sh")
154
-
return
155
-
}
156
-
signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev)
157
-
resp, err := signedClient.AddMember(did)
158
-
if err != nil {
159
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
160
-
return
161
-
}
162
163
-
if resp.StatusCode != http.StatusNoContent {
164
-
log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
165
-
return
166
-
}
167
-
}()
168
169
-
s.pages.HxRedirect(w, "/")
170
-
return
171
-
}
172
-
}
173
174
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
175
-
s.auth.ClearSession(r, w)
176
w.Header().Set("HX-Redirect", "/login")
177
w.WriteHeader(http.StatusSeeOther)
178
}
179
180
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
181
-
user := s.auth.GetUser(r)
182
183
timeline, err := db.MakeTimeline(s.db)
184
if err != nil {
···
229
230
return
231
case http.MethodPost:
232
-
session, err := s.auth.Store.Get(r, appview.SessionName)
233
if err != nil || session.IsNew {
234
log.Println("unauthorized attempt to generate registration key")
235
http.Error(w, "Forbidden", http.StatusUnauthorized)
···
291
292
// create a signed request and check if a node responds to that
293
func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
294
-
user := s.auth.GetUser(r)
295
296
domain := chi.URLParam(r, "domain")
297
if domain == "" {
···
306
return
307
}
308
309
-
client, err := NewSignedClient(domain, secret, s.config.Dev)
310
if err != nil {
311
log.Println("failed to create client to ", domain)
312
}
···
415
return
416
}
417
418
-
user := s.auth.GetUser(r)
419
reg, err := db.RegistrationByDomain(s.db, domain)
420
if err != nil {
421
w.Write([]byte("failed to pull up registration info"))
···
463
// get knots registered by this user
464
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
465
// for now, this is just pubkeys
466
-
user := s.auth.GetUser(r)
467
registrations, err := db.RegistrationsByDid(s.db, user.Did)
468
if err != nil {
469
log.Println(err)
···
516
log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
517
518
// announce this relation into the firehose, store into owners' pds
519
-
client, _ := s.auth.AuthorizedClient(r)
520
-
currentUser := s.auth.GetUser(r)
521
createdAt := time.Now().Format(time.RFC3339)
522
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
523
Collection: tangled.KnotMemberNSID,
524
Repo: currentUser.Did,
525
Rkey: appview.TID(),
···
544
return
545
}
546
547
-
ksClient, err := NewSignedClient(domain, secret, s.config.Dev)
548
if err != nil {
549
log.Println("failed to create client to ", domain)
550
return
···
573
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
574
}
575
576
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
577
switch r.Method {
578
case http.MethodGet:
579
-
user := s.auth.GetUser(r)
580
knots, err := s.enforcer.GetDomainsForUser(user.Did)
581
if err != nil {
582
s.pages.Notice(w, "repo", "Invalid user account.")
···
589
})
590
591
case http.MethodPost:
592
-
user := s.auth.GetUser(r)
593
594
domain := r.FormValue("domain")
595
if domain == "" {
···
603
return
604
}
605
606
-
// Check for valid repository name (GitHub-like rules)
607
-
// No spaces, only alphanumeric characters, dashes, and underscores
608
-
for _, char := range repoName {
609
-
if !((char >= 'a' && char <= 'z') ||
610
-
(char >= 'A' && char <= 'Z') ||
611
-
(char >= '0' && char <= '9') ||
612
-
char == '-' || char == '_' || char == '.') {
613
-
s.pages.Notice(w, "repo", "Repository name can only contain alphanumeric characters, periods, hyphens, and underscores.")
614
-
return
615
-
}
616
}
617
618
defaultBranch := r.FormValue("branch")
···
640
return
641
}
642
643
-
client, err := NewSignedClient(domain, secret, s.config.Dev)
644
if err != nil {
645
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
646
return
···
655
Description: description,
656
}
657
658
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
659
660
createdAt := time.Now().Format(time.RFC3339)
661
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
662
Collection: tangled.RepoNSID,
663
Repo: user.Did,
664
Rkey: rkey,
···
19
"github.com/go-chi/chi/v5"
20
"tangled.sh/tangled.sh/core/api/tangled"
21
"tangled.sh/tangled.sh/core/appview"
22
"tangled.sh/tangled.sh/core/appview/db"
23
+
"tangled.sh/tangled.sh/core/appview/knotclient"
24
+
"tangled.sh/tangled.sh/core/appview/oauth"
25
"tangled.sh/tangled.sh/core/appview/pages"
26
"tangled.sh/tangled.sh/core/jetstream"
27
"tangled.sh/tangled.sh/core/rbac"
···
29
30
type State struct {
31
db *db.DB
32
+
oauth *oauth.OAuth
33
enforcer *rbac.Enforcer
34
+
tidClock syntax.TIDClock
35
pages *pages.Pages
36
resolver *appview.Resolver
37
jc *jetstream.JetstreamClient
···
39
}
40
41
func Make(config *appview.Config) (*State, error) {
42
+
d, err := db.Make(config.Core.DbPath)
43
if err != nil {
44
return nil, err
45
}
46
47
+
enforcer, err := rbac.NewEnforcer(config.Core.DbPath)
48
if err != nil {
49
return nil, err
50
}
···
55
56
resolver := appview.NewResolver()
57
58
+
oauth := oauth.NewOAuth(d, config)
59
+
60
wrapper := db.DbWrapper{d}
61
jc, err := jetstream.NewJetstreamClient(
62
+
config.Jetstream.Endpoint,
63
"appview",
64
+
[]string{
65
+
tangled.GraphFollowNSID,
66
+
tangled.FeedStarNSID,
67
+
tangled.PublicKeyNSID,
68
+
tangled.RepoArtifactNSID,
69
+
tangled.ActorProfileNSID,
70
+
},
71
nil,
72
slog.Default(),
73
wrapper,
···
76
if err != nil {
77
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
78
}
79
+
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer))
80
if err != nil {
81
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
82
}
83
84
state := &State{
85
d,
86
+
oauth,
87
enforcer,
88
clock,
89
pgs,
···
99
return c.Next().String()
100
}
101
102
+
// func (s *State) Login(w http.ResponseWriter, r *http.Request) {
103
+
// ctx := r.Context()
104
105
+
// switch r.Method {
106
+
// case http.MethodGet:
107
+
// err := s.pages.Login(w, pages.LoginParams{})
108
+
// if err != nil {
109
+
// log.Printf("rendering login page: %s", err)
110
+
// }
111
112
+
// return
113
+
// case http.MethodPost:
114
+
// handle := strings.TrimPrefix(r.FormValue("handle"), "@")
115
+
// appPassword := r.FormValue("app_password")
116
117
+
// resolved, err := s.resolver.ResolveIdent(ctx, handle)
118
+
// if err != nil {
119
+
// log.Println("failed to resolve handle:", err)
120
+
// s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
121
+
// return
122
+
// }
123
124
+
// atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword)
125
+
// if err != nil {
126
+
// s.pages.Notice(w, "login-msg", "Invalid handle or password.")
127
+
// return
128
+
// }
129
+
// sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
130
131
+
// err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
132
+
// if err != nil {
133
+
// s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
134
+
// return
135
+
// }
136
137
+
// log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
138
139
+
// did := resolved.DID.String()
140
+
// defaultKnot := "knot1.tangled.sh"
141
142
+
// go func() {
143
+
// log.Printf("adding %s to default knot", did)
144
+
// err = s.enforcer.AddMember(defaultKnot, did)
145
+
// if err != nil {
146
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
147
+
// return
148
+
// }
149
+
// err = s.enforcer.E.SavePolicy()
150
+
// if err != nil {
151
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
152
+
// return
153
+
// }
154
155
+
// secret, err := db.GetRegistrationKey(s.db, defaultKnot)
156
+
// if err != nil {
157
+
// log.Println("failed to get registration key for knot1.tangled.sh")
158
+
// return
159
+
// }
160
+
// signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev)
161
+
// resp, err := signedClient.AddMember(did)
162
+
// if err != nil {
163
+
// log.Println("failed to add user to knot1.tangled.sh: ", err)
164
+
// return
165
+
// }
166
167
+
// if resp.StatusCode != http.StatusNoContent {
168
+
// log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
169
+
// return
170
+
// }
171
+
// }()
172
173
+
// s.pages.HxRedirect(w, "/")
174
+
// return
175
+
// }
176
+
// }
177
178
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
179
+
s.oauth.ClearSession(r, w)
180
w.Header().Set("HX-Redirect", "/login")
181
w.WriteHeader(http.StatusSeeOther)
182
}
183
184
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
185
+
user := s.oauth.GetUser(r)
186
187
timeline, err := db.MakeTimeline(s.db)
188
if err != nil {
···
233
234
return
235
case http.MethodPost:
236
+
session, err := s.oauth.Store.Get(r, appview.SessionName)
237
if err != nil || session.IsNew {
238
log.Println("unauthorized attempt to generate registration key")
239
http.Error(w, "Forbidden", http.StatusUnauthorized)
···
295
296
// create a signed request and check if a node responds to that
297
func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
298
+
user := s.oauth.GetUser(r)
299
300
domain := chi.URLParam(r, "domain")
301
if domain == "" {
···
310
return
311
}
312
313
+
client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
314
if err != nil {
315
log.Println("failed to create client to ", domain)
316
}
···
419
return
420
}
421
422
+
user := s.oauth.GetUser(r)
423
reg, err := db.RegistrationByDomain(s.db, domain)
424
if err != nil {
425
w.Write([]byte("failed to pull up registration info"))
···
467
// get knots registered by this user
468
func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
469
// for now, this is just pubkeys
470
+
user := s.oauth.GetUser(r)
471
registrations, err := db.RegistrationsByDid(s.db, user.Did)
472
if err != nil {
473
log.Println(err)
···
520
log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain)
521
522
// announce this relation into the firehose, store into owners' pds
523
+
client, err := s.oauth.AuthorizedClient(r)
524
+
if err != nil {
525
+
http.Error(w, "failed to authorize client", http.StatusInternalServerError)
526
+
return
527
+
}
528
+
currentUser := s.oauth.GetUser(r)
529
createdAt := time.Now().Format(time.RFC3339)
530
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
531
Collection: tangled.KnotMemberNSID,
532
Repo: currentUser.Did,
533
Rkey: appview.TID(),
···
552
return
553
}
554
555
+
ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
556
if err != nil {
557
log.Println("failed to create client to ", domain)
558
return
···
581
func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
582
}
583
584
+
func validateRepoName(name string) error {
585
+
// check for path traversal attempts
586
+
if name == "." || name == ".." ||
587
+
strings.Contains(name, "/") || strings.Contains(name, "\\") {
588
+
return fmt.Errorf("Repository name contains invalid path characters")
589
+
}
590
+
591
+
// check for sequences that could be used for traversal when normalized
592
+
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
593
+
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
594
+
return fmt.Errorf("Repository name contains invalid path sequence")
595
+
}
596
+
597
+
// then continue with character validation
598
+
for _, char := range name {
599
+
if !((char >= 'a' && char <= 'z') ||
600
+
(char >= 'A' && char <= 'Z') ||
601
+
(char >= '0' && char <= '9') ||
602
+
char == '-' || char == '_' || char == '.') {
603
+
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
604
+
}
605
+
}
606
+
607
+
// additional check to prevent multiple sequential dots
608
+
if strings.Contains(name, "..") {
609
+
return fmt.Errorf("Repository name cannot contain sequential dots")
610
+
}
611
+
612
+
// if all checks pass
613
+
return nil
614
+
}
615
+
616
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
617
switch r.Method {
618
case http.MethodGet:
619
+
user := s.oauth.GetUser(r)
620
knots, err := s.enforcer.GetDomainsForUser(user.Did)
621
if err != nil {
622
s.pages.Notice(w, "repo", "Invalid user account.")
···
629
})
630
631
case http.MethodPost:
632
+
user := s.oauth.GetUser(r)
633
634
domain := r.FormValue("domain")
635
if domain == "" {
···
643
return
644
}
645
646
+
if err := validateRepoName(repoName); err != nil {
647
+
s.pages.Notice(w, "repo", err.Error())
648
+
return
649
}
650
651
defaultBranch := r.FormValue("branch")
···
673
return
674
}
675
676
+
client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev)
677
if err != nil {
678
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
679
return
···
688
Description: description,
689
}
690
691
+
xrpcClient, err := s.oauth.AuthorizedClient(r)
692
+
if err != nil {
693
+
s.pages.Notice(w, "repo", "Failed to write record to PDS.")
694
+
return
695
+
}
696
697
createdAt := time.Now().Format(time.RFC3339)
698
+
atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
699
Collection: tangled.RepoNSID,
700
Repo: user.Did,
701
Rkey: rkey,
+1
-1
appview/tid.go
+1
-1
appview/tid.go
+80
appview/xrpcclient/xrpc.go
+80
appview/xrpcclient/xrpc.go
···
···
1
+
package xrpcclient
2
+
3
+
import (
4
+
"bytes"
5
+
"context"
6
+
"io"
7
+
8
+
"github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/xrpc"
10
+
oauth "github.com/haileyok/atproto-oauth-golang"
11
+
)
12
+
13
+
type Client struct {
14
+
*oauth.XrpcClient
15
+
authArgs *oauth.XrpcAuthedRequestArgs
16
+
}
17
+
18
+
func NewClient(client *oauth.XrpcClient, authArgs *oauth.XrpcAuthedRequestArgs) *Client {
19
+
return &Client{
20
+
XrpcClient: client,
21
+
authArgs: authArgs,
22
+
}
23
+
}
24
+
25
+
func (c *Client) RepoPutRecord(ctx context.Context, input *atproto.RepoPutRecord_Input) (*atproto.RepoPutRecord_Output, error) {
26
+
var out atproto.RepoPutRecord_Output
27
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil {
28
+
return nil, err
29
+
}
30
+
31
+
return &out, nil
32
+
}
33
+
34
+
func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) {
35
+
var out atproto.RepoGetRecord_Output
36
+
37
+
params := map[string]interface{}{
38
+
"cid": cid,
39
+
"collection": collection,
40
+
"repo": repo,
41
+
"rkey": rkey,
42
+
}
43
+
if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil {
44
+
return nil, err
45
+
}
46
+
47
+
return &out, nil
48
+
}
49
+
50
+
func (c *Client) RepoUploadBlob(ctx context.Context, input io.Reader) (*atproto.RepoUploadBlob_Output, error) {
51
+
var out atproto.RepoUploadBlob_Output
52
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "*/*", "com.atproto.repo.uploadBlob", nil, input, &out); err != nil {
53
+
return nil, err
54
+
}
55
+
56
+
return &out, nil
57
+
}
58
+
59
+
func (c *Client) SyncGetBlob(ctx context.Context, cid string, did string) ([]byte, error) {
60
+
buf := new(bytes.Buffer)
61
+
62
+
params := map[string]interface{}{
63
+
"cid": cid,
64
+
"did": did,
65
+
}
66
+
if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.sync.getBlob", params, nil, buf); err != nil {
67
+
return nil, err
68
+
}
69
+
70
+
return buf.Bytes(), nil
71
+
}
72
+
73
+
func (c *Client) RepoDeleteRecord(ctx context.Context, input *atproto.RepoDeleteRecord_Input) (*atproto.RepoDeleteRecord_Output, error) {
74
+
var out atproto.RepoDeleteRecord_Output
75
+
if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil {
76
+
return nil, err
77
+
}
78
+
79
+
return &out, nil
80
+
}
+2
-2
cmd/appview/main.go
+2
-2
cmd/appview/main.go
+1
cmd/gen.go
+1
cmd/gen.go
+39
cmd/genjwks/main.go
+39
cmd/genjwks/main.go
···
···
1
+
// adapted from https://github.com/haileyok/atproto-oauth-golang
2
+
3
+
package main
4
+
5
+
import (
6
+
"crypto/ecdsa"
7
+
"crypto/elliptic"
8
+
"crypto/rand"
9
+
"encoding/json"
10
+
"fmt"
11
+
"time"
12
+
13
+
"github.com/lestrrat-go/jwx/v2/jwk"
14
+
)
15
+
16
+
func main() {
17
+
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
18
+
if err != nil {
19
+
panic(err)
20
+
}
21
+
22
+
key, err := jwk.FromRaw(privKey)
23
+
if err != nil {
24
+
panic(err)
25
+
}
26
+
27
+
kid := fmt.Sprintf("%d", time.Now().Unix())
28
+
29
+
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
30
+
panic(err)
31
+
}
32
+
33
+
b, err := json.Marshal(key)
34
+
if err != nil {
35
+
panic(err)
36
+
}
37
+
38
+
fmt.Println(string(b))
39
+
}
+2
-4
docker/Dockerfile
+2
-4
docker/Dockerfile
···
42
COPY docker/rootfs/ .
43
44
RUN chown root:root /usr/local/libexec/tangled-keyfetch && \
45
-
chmod 755 /usr/local/libexec/tangled-keyfetch && \
46
-
chown git:git /home/git/repoguard && \
47
-
chown git:git /app && chown git:git /home/git/repositories
48
49
EXPOSE 22
50
EXPOSE 5555
51
52
-
ENTRYPOINT ["/init"]
···
42
COPY docker/rootfs/ .
43
44
RUN chown root:root /usr/local/libexec/tangled-keyfetch && \
45
+
chmod 755 /usr/local/libexec/tangled-keyfetch
46
47
EXPOSE 22
48
EXPOSE 5555
49
50
+
ENTRYPOINT ["/bin/sh", "-c", "chown git:git /home/git/repoguard && chown git:git /app && chown git:git /home/git/repositories && /init"]
+17
-1
docker/docker-compose.yml
+17
-1
docker/docker-compose.yml
···
13
- "./repositories:/home/git/repositories"
14
- "./server:/app"
15
ports:
16
- "2222:22"
17
+
frontend:
18
+
image: caddy:2-alpine
19
+
command: >
20
+
caddy
21
+
reverse-proxy
22
+
--from ${KNOT_SERVER_HOSTNAME}
23
+
--to knot:5555
24
+
depends_on:
25
+
- knot
26
+
ports:
27
+
- "443:443"
28
+
- "443:443/udp"
29
+
volumes:
30
+
- caddy_data:/data
31
+
restart: always
32
+
volumes:
33
+
caddy_data:
+72
docs/hacking.md
+72
docs/hacking.md
···
···
1
+
# hacking on tangled
2
+
3
+
We highly recommend [installing
4
+
nix](https://nixos.org/download/) (the package manager)
5
+
before working on the codebase. The nix flake provides a lot
6
+
of helpers to get started and most importantly, builds and
7
+
dev shells are entirely deterministic.
8
+
9
+
To set up your dev environment:
10
+
11
+
```bash
12
+
nix develop
13
+
```
14
+
15
+
Non-nix users can look at the `devShell` attribute in the
16
+
`flake.nix` file to determine necessary dependencies.
17
+
18
+
## running the appview
19
+
20
+
The nix flake also exposes a few `app` attributes (run `nix
21
+
flake show` to see a full list of what the flake provides),
22
+
one of the apps runs the appview with the `air`
23
+
live-reloader:
24
+
25
+
```bash
26
+
TANGLED_DEV=true nix run .#watch-appview
27
+
28
+
# TANGLED_DB_PATH might be of interest to point to
29
+
# different sqlite DBs
30
+
31
+
# in a separate shell, you can live-reload tailwind
32
+
nix run .#watch-tailwind
33
+
```
34
+
35
+
## running a knotserver
36
+
37
+
An end-to-end knotserver setup requires setting up a machine
38
+
with `sshd`, `repoguard`, `keyfetch`, a git user, which is
39
+
quite cumbersome and so the nix flake provides a
40
+
`nixosConfiguration` to do so.
41
+
42
+
To begin, head to `http://localhost:3000` in the browser and
43
+
generate a knotserver secret. Replace the existing secret in
44
+
`flake.nix` with the newly generated secret.
45
+
46
+
You can now start a lightweight NixOS VM using
47
+
`nixos-shell` like so:
48
+
49
+
```bash
50
+
QEMU_NET_OPTS="hostfwd=tcp::6000-:6000,hostfwd=tcp::2222-:22" nixos-shell --flake .#knotVM
51
+
52
+
# hit Ctrl-a + c + q to exit the VM
53
+
```
54
+
55
+
This starts a knotserver on port 6000 with `ssh` exposed on
56
+
port 2222. You can push repositories to this VM with this
57
+
ssh config block on your main machine:
58
+
59
+
```bash
60
+
Host nixos-shell
61
+
Hostname localhost
62
+
Port 2222
63
+
User git
64
+
IdentityFile ~/.ssh/my_tangled_key
65
+
```
66
+
67
+
Set up a remote called `local-dev` on a git repo:
68
+
69
+
```bash
70
+
git remote add local-dev git@nixos-shell:user/repo
71
+
git push local-dev main
72
+
```
+5
-5
flake.lock
+5
-5
flake.lock
···
64
"inter-fonts-src": {
65
"flake": false,
66
"locked": {
67
-
"lastModified": 1731680160,
68
"narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=",
69
"type": "tarball",
70
"url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip"
···
89
},
90
"nixpkgs": {
91
"locked": {
92
-
"lastModified": 1746055187,
93
-
"narHash": "sha256-3dqArYSMP9hM7Qpy5YWhnSjiqniSaT2uc5h2Po7tmg0=",
94
"owner": "nixos",
95
"repo": "nixpkgs",
96
-
"rev": "3e362ce63e16b9572d8c2297c04f7c19ab6725a5",
97
"type": "github"
98
},
99
"original": {
100
"owner": "nixos",
101
-
"ref": "nixos-24.11",
102
"repo": "nixpkgs",
103
"type": "github"
104
}
···
64
"inter-fonts-src": {
65
"flake": false,
66
"locked": {
67
+
"lastModified": 1731687360,
68
"narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=",
69
"type": "tarball",
70
"url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip"
···
89
},
90
"nixpkgs": {
91
"locked": {
92
+
"lastModified": 1746904237,
93
+
"narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=",
94
"owner": "nixos",
95
"repo": "nixpkgs",
96
+
"rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956",
97
"type": "github"
98
},
99
"original": {
100
"owner": "nixos",
101
+
"ref": "nixos-unstable",
102
"repo": "nixpkgs",
103
"type": "github"
104
}
+12
-9
flake.nix
+12
-9
flake.nix
···
2
description = "atproto github";
3
4
inputs = {
5
-
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
6
indigo = {
7
url = "github:oppiliappan/indigo";
8
flake = false;
···
49
inherit (gitignore.lib) gitignoreSource;
50
in {
51
overlays.default = final: prev: let
52
-
goModHash = "sha256-CmBuvv3duQQoc8iTW4244w1rYLGeqMQS+qQ3wwReZZg=";
53
buildCmdPackage = name:
54
final.buildGoModule {
55
pname = name;
···
57
src = gitignoreSource ./.;
58
subPackages = ["cmd/${name}"];
59
vendorHash = goModHash;
60
-
CGO_ENABLED = 0;
61
};
62
in {
63
indigo-lexgen = final.buildGoModule {
···
88
doCheck = false;
89
subPackages = ["cmd/appview"];
90
vendorHash = goModHash;
91
-
CGO_ENABLED = 1;
92
stdenv = pkgsStatic.stdenv;
93
};
94
···
111
112
runHook postInstall
113
'';
114
-
CGO_ENABLED = 1;
115
};
116
knotserver-unwrapped = final.pkgsStatic.buildGoModule {
117
pname = "knotserver";
···
119
src = gitignoreSource ./.;
120
subPackages = ["cmd/knotserver"];
121
vendorHash = goModHash;
122
-
CGO_ENABLED = 1;
123
};
124
repoguard = buildCmdPackage "repoguard";
125
keyfetch = buildCmdPackage "keyfetch";
126
};
127
packages = forAllSystems (system: {
128
inherit
···
133
knotserver-unwrapped
134
repoguard
135
keyfetch
136
;
137
});
138
defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview);
···
162
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
163
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
164
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/
165
'';
166
};
167
});
168
apps = forAllSystems (system: let
···
170
air-watcher = name:
171
pkgs.writeShellScriptBin "run"
172
''
173
-
TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \
174
-
-build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
175
-build.bin "./out/${name}.out" \
176
-build.stop_on_error "true" \
177
-build.include_ext "go"
···
446
};
447
};
448
}
449
-
···
2
description = "atproto github";
3
4
inputs = {
5
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
6
indigo = {
7
url = "github:oppiliappan/indigo";
8
flake = false;
···
49
inherit (gitignore.lib) gitignoreSource;
50
in {
51
overlays.default = final: prev: let
52
+
goModHash = "sha256-zcfTNo7QsiihzLa4qHEX8uGGtbcmBn8TlSm0YHBRNw8=";
53
buildCmdPackage = name:
54
final.buildGoModule {
55
pname = name;
···
57
src = gitignoreSource ./.;
58
subPackages = ["cmd/${name}"];
59
vendorHash = goModHash;
60
+
env.CGO_ENABLED = 0;
61
};
62
in {
63
indigo-lexgen = final.buildGoModule {
···
88
doCheck = false;
89
subPackages = ["cmd/appview"];
90
vendorHash = goModHash;
91
+
env.CGO_ENABLED = 1;
92
stdenv = pkgsStatic.stdenv;
93
};
94
···
111
112
runHook postInstall
113
'';
114
+
env.CGO_ENABLED = 1;
115
};
116
knotserver-unwrapped = final.pkgsStatic.buildGoModule {
117
pname = "knotserver";
···
119
src = gitignoreSource ./.;
120
subPackages = ["cmd/knotserver"];
121
vendorHash = goModHash;
122
+
env.CGO_ENABLED = 1;
123
};
124
repoguard = buildCmdPackage "repoguard";
125
keyfetch = buildCmdPackage "keyfetch";
126
+
genjwks = buildCmdPackage "genjwks";
127
};
128
packages = forAllSystems (system: {
129
inherit
···
134
knotserver-unwrapped
135
repoguard
136
keyfetch
137
+
genjwks
138
;
139
});
140
defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview);
···
164
cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/
165
cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/
166
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/
167
+
export TANGLED_OAUTH_JWKS="$(${pkgs.genjwks}/bin/genjwks)"
168
'';
169
+
env.CGO_ENABLED = 1;
170
};
171
});
172
apps = forAllSystems (system: let
···
174
air-watcher = name:
175
pkgs.writeShellScriptBin "run"
176
''
177
+
${pkgs.air}/bin/air -c /dev/null \
178
+
-build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \
179
-build.bin "./out/${name}.out" \
180
-build.stop_on_error "true" \
181
-build.include_ext "go"
···
450
};
451
};
452
}
+18
-12
go.mod
+18
-12
go.mod
···
1
module tangled.sh/tangled.sh/core
2
3
-
go 1.23.0
4
5
-
toolchain go1.23.6
6
7
require (
8
github.com/Blank-Xu/sql-adapter v1.1.1
9
github.com/alecthomas/chroma/v2 v2.15.0
10
github.com/bluekeyes/go-gitdiff v0.8.1
11
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20
12
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1
13
github.com/casbin/casbin/v2 v2.103.0
14
github.com/cyphar/filepath-securejoin v0.4.1
···
19
github.com/go-git/go-git/v5 v5.14.0
20
github.com/google/uuid v1.6.0
21
github.com/gorilla/sessions v1.4.0
22
github.com/ipfs/go-cid v0.5.0
23
github.com/mattn/go-sqlite3 v1.14.24
24
github.com/microcosm-cc/bluemonday v1.0.27
25
github.com/resend/resend-go/v2 v2.15.0
···
41
github.com/casbin/govaluate v1.3.0 // indirect
42
github.com/cespare/xxhash/v2 v2.3.0 // indirect
43
github.com/cloudflare/circl v1.6.0 // indirect
44
-
github.com/davecgh/go-spew v1.1.1 // indirect
45
github.com/dlclark/regexp2 v1.11.5 // indirect
46
github.com/emirpasic/gods v1.18.1 // indirect
47
github.com/felixge/httpsnoop v1.0.4 // indirect
48
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
49
github.com/go-git/go-billy/v5 v5.6.2 // indirect
50
-
github.com/go-logr/logr v1.4.1 // indirect
51
github.com/go-logr/stdr v1.2.2 // indirect
52
github.com/goccy/go-json v0.10.2 // indirect
53
github.com/gogo/protobuf v1.3.2 // indirect
54
github.com/gorilla/css v1.0.1 // indirect
55
github.com/gorilla/securecookie v1.1.2 // indirect
56
github.com/gorilla/websocket v1.5.1 // indirect
···
75
github.com/kevinburke/ssh_config v1.2.0 // indirect
76
github.com/klauspost/compress v1.17.9 // indirect
77
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
78
github.com/mattn/go-isatty v0.0.20 // indirect
79
github.com/minio/sha256-simd v1.0.1 // indirect
80
github.com/mr-tron/base58 v1.2.0 // indirect
···
86
github.com/opentracing/opentracing-go v1.2.0 // indirect
87
github.com/pjbgf/sha1cd v0.3.2 // indirect
88
github.com/pkg/errors v0.9.1 // indirect
89
-
github.com/pmezard/go-difflib v1.0.0 // indirect
90
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
91
github.com/prometheus/client_golang v1.19.1 // indirect
92
github.com/prometheus/client_model v0.6.1 // indirect
93
github.com/prometheus/common v0.54.0 // indirect
94
github.com/prometheus/procfs v0.15.1 // indirect
95
github.com/sergi/go-diff v1.3.1 // indirect
96
github.com/skeema/knownhosts v1.3.1 // indirect
97
github.com/spaolacci/murmur3 v1.1.0 // indirect
98
-
github.com/stretchr/testify v1.10.0 // indirect
99
github.com/xanzy/ssh-agent v0.3.3 // indirect
100
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
101
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
102
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
103
-
go.opentelemetry.io/otel v1.21.0 // indirect
104
-
go.opentelemetry.io/otel/metric v1.21.0 // indirect
105
-
go.opentelemetry.io/otel/trace v1.21.0 // indirect
106
go.uber.org/atomic v1.11.0 // indirect
107
go.uber.org/multierr v1.11.0 // indirect
108
go.uber.org/zap v1.26.0 // indirect
109
golang.org/x/crypto v0.37.0 // indirect
110
golang.org/x/net v0.39.0 // indirect
111
golang.org/x/sys v0.32.0 // indirect
112
-
golang.org/x/time v0.5.0 // indirect
113
google.golang.org/protobuf v1.34.2 // indirect
114
gopkg.in/warnings.v0 v0.1.2 // indirect
115
-
gopkg.in/yaml.v3 v3.0.1 // indirect
116
lukechampine.com/blake3 v1.2.1 // indirect
117
)
118
···
1
module tangled.sh/tangled.sh/core
2
3
+
go 1.24.0
4
5
+
toolchain go1.24.3
6
7
require (
8
github.com/Blank-Xu/sql-adapter v1.1.1
9
github.com/alecthomas/chroma/v2 v2.15.0
10
github.com/bluekeyes/go-gitdiff v0.8.1
11
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188
12
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1
13
github.com/casbin/casbin/v2 v2.103.0
14
github.com/cyphar/filepath-securejoin v0.4.1
···
19
github.com/go-git/go-git/v5 v5.14.0
20
github.com/google/uuid v1.6.0
21
github.com/gorilla/sessions v1.4.0
22
+
github.com/haileyok/atproto-oauth-golang v0.0.2
23
github.com/ipfs/go-cid v0.5.0
24
+
github.com/lestrrat-go/jwx/v2 v2.0.12
25
github.com/mattn/go-sqlite3 v1.14.24
26
github.com/microcosm-cc/bluemonday v1.0.27
27
github.com/resend/resend-go/v2 v2.15.0
···
43
github.com/casbin/govaluate v1.3.0 // indirect
44
github.com/cespare/xxhash/v2 v2.3.0 // indirect
45
github.com/cloudflare/circl v1.6.0 // indirect
46
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
47
github.com/dlclark/regexp2 v1.11.5 // indirect
48
github.com/emirpasic/gods v1.18.1 // indirect
49
github.com/felixge/httpsnoop v1.0.4 // indirect
50
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
51
github.com/go-git/go-billy/v5 v5.6.2 // indirect
52
+
github.com/go-logr/logr v1.4.2 // indirect
53
github.com/go-logr/stdr v1.2.2 // indirect
54
github.com/goccy/go-json v0.10.2 // indirect
55
github.com/gogo/protobuf v1.3.2 // indirect
56
+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
57
github.com/gorilla/css v1.0.1 // indirect
58
github.com/gorilla/securecookie v1.1.2 // indirect
59
github.com/gorilla/websocket v1.5.1 // indirect
···
78
github.com/kevinburke/ssh_config v1.2.0 // indirect
79
github.com/klauspost/compress v1.17.9 // indirect
80
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
81
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
82
+
github.com/lestrrat-go/httpcc v1.0.1 // indirect
83
+
github.com/lestrrat-go/httprc v1.0.4 // indirect
84
+
github.com/lestrrat-go/iter v1.0.2 // indirect
85
+
github.com/lestrrat-go/option v1.0.1 // indirect
86
github.com/mattn/go-isatty v0.0.20 // indirect
87
github.com/minio/sha256-simd v1.0.1 // indirect
88
github.com/mr-tron/base58 v1.2.0 // indirect
···
94
github.com/opentracing/opentracing-go v1.2.0 // indirect
95
github.com/pjbgf/sha1cd v0.3.2 // indirect
96
github.com/pkg/errors v0.9.1 // indirect
97
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
98
github.com/prometheus/client_golang v1.19.1 // indirect
99
github.com/prometheus/client_model v0.6.1 // indirect
100
github.com/prometheus/common v0.54.0 // indirect
101
github.com/prometheus/procfs v0.15.1 // indirect
102
+
github.com/segmentio/asm v1.2.0 // indirect
103
github.com/sergi/go-diff v1.3.1 // indirect
104
github.com/skeema/knownhosts v1.3.1 // indirect
105
github.com/spaolacci/murmur3 v1.1.0 // indirect
106
github.com/xanzy/ssh-agent v0.3.3 // indirect
107
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
108
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
109
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
110
+
go.opentelemetry.io/otel v1.29.0 // indirect
111
+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
112
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
113
go.uber.org/atomic v1.11.0 // indirect
114
go.uber.org/multierr v1.11.0 // indirect
115
go.uber.org/zap v1.26.0 // indirect
116
golang.org/x/crypto v0.37.0 // indirect
117
golang.org/x/net v0.39.0 // indirect
118
golang.org/x/sys v0.32.0 // indirect
119
+
golang.org/x/time v0.8.0 // indirect
120
google.golang.org/protobuf v1.34.2 // indirect
121
gopkg.in/warnings.v0 v0.1.2 // indirect
122
lukechampine.com/blake3 v1.2.1 // indirect
123
)
124
+61
-16
go.sum
+61
-16
go.sum
···
26
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
27
github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI=
28
github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
29
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 h1:yHusfYYi8odoCcsI6AurU+dRWb7itHAQNwt3/Rl9Vfs=
30
-
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20/go.mod h1:Qp4YqWf+AQ3TwQCxV5Ls8O2tXE55zVTGVs3zTmn7BOg=
31
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA=
32
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4=
33
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
···
52
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
53
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
54
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
56
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
58
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
59
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
···
82
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
83
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
84
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
85
-
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
86
-
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
87
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
88
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
89
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
91
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
92
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
93
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
94
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
95
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
96
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
···
111
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
112
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
113
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
114
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
115
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
116
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
159
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
160
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
161
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
162
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
163
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
164
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
···
177
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
178
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
179
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
180
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
181
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
182
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
···
212
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
213
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
214
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
215
-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
216
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
217
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
218
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
219
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
···
227
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
228
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
229
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
230
-
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
231
-
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
232
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
233
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
234
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
235
github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog=
···
246
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
247
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
248
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
249
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
250
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
251
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
252
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
253
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
254
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
255
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
···
270
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
271
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
272
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
273
-
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
274
-
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
275
-
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
276
-
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
277
-
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
278
-
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
279
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
280
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
281
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
···
303
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
304
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
305
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
306
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
307
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
308
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
···
314
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
315
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
316
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
317
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
318
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
319
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
327
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
328
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
329
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
330
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
331
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
332
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
···
334
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
335
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
336
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
337
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
338
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
339
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
348
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
349
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
350
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
351
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
352
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
353
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
···
357
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
358
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
359
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
360
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
361
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
362
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
···
364
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
365
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
366
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
367
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
368
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
369
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
···
372
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
373
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
374
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
375
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
376
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
377
-
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
378
-
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
379
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
380
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
381
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
···
389
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
390
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
391
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
392
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
393
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
394
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
···
26
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
27
github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI=
28
github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
29
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk=
30
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
31
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA=
32
github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4=
33
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
···
52
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
53
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
54
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
57
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
58
+
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
59
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
60
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
61
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
62
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
63
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
···
86
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
87
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
88
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
89
+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
90
+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
91
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
92
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
93
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
···
95
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
96
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
97
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
98
+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
99
+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
100
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
101
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
102
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
···
117
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
118
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
119
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
120
+
github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8=
121
+
github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
122
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
123
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
124
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
167
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
168
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
169
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
170
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
171
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
172
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
173
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
174
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
···
187
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
188
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
189
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
190
+
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
191
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
192
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
193
+
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
194
+
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
195
+
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
196
+
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
197
+
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
198
+
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
199
+
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
200
+
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
201
+
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
202
+
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
203
+
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
204
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
205
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
206
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
···
236
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
237
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
238
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
239
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
240
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
241
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
242
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
243
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
244
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
···
252
github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE=
253
github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
254
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
255
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
256
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
257
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
258
+
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
259
+
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
260
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
261
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
262
github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog=
···
273
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
274
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
275
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
276
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
277
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
278
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
279
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
280
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
281
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
282
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
283
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
284
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
285
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
286
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
287
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
288
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
···
303
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
304
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
305
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
306
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
307
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
308
+
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
309
+
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
310
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
311
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
312
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
313
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
314
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
···
336
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
337
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
338
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
339
+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
340
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
341
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
342
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
···
348
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
349
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
350
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
351
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
352
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
353
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
354
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
362
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
363
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
364
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
365
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
366
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
367
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
368
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
···
370
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
371
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
372
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
373
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
374
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
375
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
376
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
385
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
386
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
387
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
388
+
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
389
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
390
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
391
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
···
395
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
396
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
397
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
398
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
399
+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
400
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
401
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
402
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
···
404
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
405
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
406
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
407
+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
408
+
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
409
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
410
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
411
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
···
414
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
415
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
416
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
417
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
418
+
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
419
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
420
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
421
+
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
422
+
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
423
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
424
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
425
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
···
433
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
434
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
435
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
436
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
437
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
438
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
439
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+38
knotserver/routes.go
+38
knotserver/routes.go
···
600
name := data.Name
601
defaultBranch := data.DefaultBranch
602
603
relativeRepoPath := filepath.Join(did, name)
604
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
605
err := git.InitBare(repoPath, defaultBranch)
···
1078
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1079
w.Write([]byte("ok"))
1080
}
···
600
name := data.Name
601
defaultBranch := data.DefaultBranch
602
603
+
if err := validateRepoName(name); err != nil {
604
+
l.Error("creating repo", "error", err.Error())
605
+
writeError(w, err.Error(), http.StatusBadRequest)
606
+
return
607
+
}
608
+
609
relativeRepoPath := filepath.Join(did, name)
610
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
611
err := git.InitBare(repoPath, defaultBranch)
···
1084
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1085
w.Write([]byte("ok"))
1086
}
1087
+
1088
+
func validateRepoName(name string) error {
1089
+
// check for path traversal attempts
1090
+
if name == "." || name == ".." ||
1091
+
strings.Contains(name, "/") || strings.Contains(name, "\\") {
1092
+
return fmt.Errorf("Repository name contains invalid path characters")
1093
+
}
1094
+
1095
+
// check for sequences that could be used for traversal when normalized
1096
+
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
1097
+
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
1098
+
return fmt.Errorf("Repository name contains invalid path sequence")
1099
+
}
1100
+
1101
+
// then continue with character validation
1102
+
for _, char := range name {
1103
+
if !((char >= 'a' && char <= 'z') ||
1104
+
(char >= 'A' && char <= 'Z') ||
1105
+
(char >= '0' && char <= '9') ||
1106
+
char == '-' || char == '_' || char == '.') {
1107
+
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
1108
+
}
1109
+
}
1110
+
1111
+
// additional check to prevent multiple sequential dots
1112
+
if strings.Contains(name, "..") {
1113
+
return fmt.Errorf("Repository name cannot contain sequential dots")
1114
+
}
1115
+
1116
+
// if all checks pass
1117
+
return nil
1118
+
}
+72
lexicons/actor/profile.json
+72
lexicons/actor/profile.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.actor.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A declaration of a Tangled account profile.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"bluesky"
13
+
],
14
+
"properties": {
15
+
"description": {
16
+
"type": "string",
17
+
"description": "Free-form profile description text.",
18
+
"maxGraphemes": 256,
19
+
"maxLength": 2560
20
+
},
21
+
"links": {
22
+
"type": "array",
23
+
"minLength": 0,
24
+
"maxLength": 5,
25
+
"items": {
26
+
"type": "string",
27
+
"description": "Any URI, intended for social profiles or websites, can be used to link DIDs/AT-URIs too.",
28
+
"format": "uri"
29
+
}
30
+
},
31
+
"stats": {
32
+
"type": "array",
33
+
"minLength": 0,
34
+
"maxLength": 2,
35
+
"items": {
36
+
"type": "string",
37
+
"description": "Vanity stats.",
38
+
"enum": [
39
+
"merged-pull-request-count",
40
+
"closed-pull-request-count",
41
+
"open-pull-request-count",
42
+
"open-issue-count",
43
+
"closed-issue-count",
44
+
"repository-count"
45
+
]
46
+
}
47
+
},
48
+
"bluesky": {
49
+
"type": "boolean",
50
+
"description": "Include link to this account on Bluesky."
51
+
},
52
+
"location": {
53
+
"type": "string",
54
+
"description": "Free-form location text.",
55
+
"maxGraphemes": 40,
56
+
"maxLength": 400
57
+
},
58
+
"pinnedRepositories": {
59
+
"type": "array",
60
+
"description": "Any ATURI, it is up to appviews to validate these fields.",
61
+
"minLength": 0,
62
+
"maxLength": 6,
63
+
"items": {
64
+
"type": "string",
65
+
"format": "at-uri"
66
+
}
67
+
}
68
+
}
69
+
}
70
+
}
71
+
}
72
+
}
-1
lexicons/publicKey.json
-1
lexicons/publicKey.json