this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial labeling pkg

+255
+6
labeling/doc.go
··· 1 + /* 2 + Experimental extensions to github.com/bluesky-social/indigo/atproto/labeling 3 + 4 + Has not been reviewed and not super confident in some of the naming and API shapes. 5 + */ 6 + package labeling
+57
labeling/labeling_test.go
··· 1 + package labeling 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/bluesky-social/indigo/atproto/atcrypto" 7 + "github.com/bluesky-social/indigo/atproto/labeling" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + 10 + "github.com/stretchr/testify/assert" 11 + ) 12 + 13 + func TestLabeling(t *testing.T) { 14 + assert := assert.New(t) 15 + ctx := t.Context() 16 + 17 + priv, err := atcrypto.GeneratePrivateKeyP256() 18 + if err != nil { 19 + t.Fail() 20 + } 21 + pub, err := priv.PublicKey() 22 + if err != nil { 23 + t.Fail() 24 + } 25 + 26 + signer := LabelMaker{ 27 + DID: syntax.DID("did:web:labeler.example.com"), 28 + SigningKey: priv, 29 + } 30 + 31 + l1, err := signer.CreateLabel("at://did:web:subj.example.com/com.example.record/one", "", "great") 32 + if err != nil { 33 + t.Fail() 34 + } 35 + 36 + l2, err := signer.CreateLabel("did:web:subj.example.com", "", "wunderbar") 37 + if err != nil { 38 + t.Fail() 39 + } 40 + 41 + store := NewMemStore() 42 + assert.NoError(store.PutLabels(ctx, []labeling.Label{*l1, *l2})) 43 + 44 + out1, err := store.GetLabels(ctx, []string{l1.URI, l2.URI}, []syntax.DID{signer.DID}) 45 + assert.NoError(err) 46 + assert.Equal(2, len(out1)) 47 + assert.NoError(out1[0].VerifySignature(pub)) 48 + assert.NoError(out1[1].VerifySignature(pub)) 49 + 50 + out2, err := store.GetLabels(ctx, []string{"did:web:other.example.com"}, []syntax.DID{signer.DID}) 51 + assert.NoError(err) 52 + assert.Equal(0, len(out2)) 53 + 54 + out3, err := store.GetLabels(ctx, []string{l1.URI, l2.URI}, []syntax.DID{"did:web:subj.example.com"}) 55 + assert.NoError(err) 56 + assert.Equal(0, len(out3)) 57 + }
+60
labeling/labelmaker.go
··· 1 + package labeling 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/atcrypto" 5 + "github.com/bluesky-social/indigo/atproto/labeling" 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + ) 8 + 9 + type LabelMaker struct { 10 + DID syntax.DID 11 + SigningKey atcrypto.PrivateKey 12 + } 13 + 14 + // CID is optional, use empty string if not known. 15 + func (ls *LabelMaker) CreateLabel(uri string, cid string, val string) (*labeling.Label, error) { 16 + return ls.CreateExpiringLabel(uri, cid, val, "") 17 + } 18 + 19 + // CID is optional, use empty string if not known. 20 + func (ls *LabelMaker) CreateExpiringLabel(uri string, cid string, val string, exp syntax.Datetime) (*labeling.Label, error) { 21 + // TODO: validate 'val'? 22 + l := labeling.Label{ 23 + CreatedAt: syntax.DatetimeNow().String(), 24 + SourceDID: ls.DID.String(), 25 + URI: uri, 26 + Val: val, 27 + Version: labeling.ATPROTO_LABEL_VERSION, 28 + } 29 + if cid != "" { 30 + // TODO: copy string? 31 + l.CID = &cid 32 + } 33 + if exp != "" { 34 + expStr := exp.String() 35 + l.ExpiresAt = &expStr 36 + } 37 + 38 + if err := l.Sign(ls.SigningKey); err != nil { 39 + return nil, err 40 + } 41 + return &l, nil 42 + } 43 + 44 + // CID is optional, use empty string if not known. 45 + func (ls *LabelMaker) NegateLabel(uri string, cid string, val string) (*labeling.Label, error) { 46 + yes := true 47 + l := labeling.Label{ 48 + CreatedAt: syntax.DatetimeNow().String(), 49 + SourceDID: ls.DID.String(), 50 + URI: uri, 51 + Val: val, 52 + Negated: &yes, 53 + Version: labeling.ATPROTO_LABEL_VERSION, 54 + } 55 + 56 + if err := l.Sign(ls.SigningKey); err != nil { 57 + return nil, err 58 + } 59 + return &l, nil 60 + }
+27
labeling/labelstore.go
··· 1 + package labeling 2 + 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/labeling" 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + // Subset of a full label, including only fields relevant to a subject at this moment (eg, not negated, not expired). 11 + // 12 + // Includes CID to disambiguate if a specific record is labeled, and includes 'cts' because it is required by `com.atproto.label.defs#defs`. 13 + type ShortLabel struct { 14 + SourceDID string `json:"src" cborgen:"src"` 15 + URI string `json:"uri" cborgen:"uri"` 16 + CID *string `json:"cid,omitempty" cborgen:"cid,omitempty"` 17 + Val string `json:"val" cborgen:"val"` 18 + CreatedAt string `json:"cts" cborgen:"cts"` 19 + } 20 + 21 + type LabelStore interface { 22 + // TODO: should this automatically parse URIs and extract DIDs? 23 + // fetches complete signed label objects from database. Must not return expired, negated, or future labels ('cts' in the future beyond a fuzzy window). 24 + GetLabels(ctx context.Context, subjects []string, labelers []syntax.DID) ([]labeling.Label, error) 25 + // Returns the same labels as [GetLabels], but only a subset of fields. 26 + GetShortLabels(ctx context.Context, subjects []string, labelers []syntax.DID) ([]ShortLabel, error) 27 + }
+105
labeling/memstore.go
··· 1 + package labeling 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/labeling" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + ) 11 + 12 + // Ephemeral in-memory implementation of [LabelStore]. 13 + // 14 + // Stores only a single label per source per URI. Eg, does not store separate labels for different record versions (CIDs). 15 + type MemStore struct { 16 + Labels map[string]map[syntax.DID]labeling.Label 17 + } 18 + 19 + func NewMemStore() *MemStore { 20 + ms := MemStore{ 21 + Labels: map[string]map[syntax.DID]labeling.Label{}, 22 + } 23 + return &ms 24 + } 25 + 26 + func (s *MemStore) GetLabels(ctx context.Context, uris []string, srcs []syntax.DID) ([]labeling.Label, error) { 27 + if len(uris) == 0 || len(srcs) == 0 { 28 + return []labeling.Label{}, nil 29 + } 30 + 31 + now := time.Now() 32 + 33 + out := []labeling.Label{} 34 + for _, uri := range uris { 35 + m, ok := s.Labels[uri] 36 + if !ok { 37 + continue 38 + } 39 + for _, src := range srcs { 40 + l, ok := m[syntax.DID(src)] 41 + if !ok { 42 + continue 43 + } 44 + if l.Negated != nil && *l.Negated == true { 45 + continue 46 + } 47 + cts, err := syntax.ParseDatetime(l.CreatedAt) 48 + if err != nil { 49 + // ignore bad timestamp 50 + continue 51 + } 52 + // one minute of fuzzy time 53 + if cts.Time().After(now.Add(time.Minute)) { 54 + // ignore future labels 55 + continue 56 + } 57 + if l.ExpiresAt != nil { 58 + exp, err := syntax.ParseDatetime(*l.ExpiresAt) 59 + if err != nil { 60 + continue 61 + } 62 + if now.After(exp.Time()) { 63 + continue 64 + } 65 + } 66 + out = append(out, l) 67 + } 68 + } 69 + return out, nil 70 + } 71 + 72 + func (s *MemStore) GetShortLabels(ctx context.Context, uris []string, srcs []syntax.DID) ([]ShortLabel, error) { 73 + labels, err := s.GetLabels(ctx, uris, srcs) 74 + if err != nil { 75 + return nil, err 76 + } 77 + out := make([]ShortLabel, len(labels)) 78 + for i, l := range labels { 79 + out[i] = ShortLabel{ 80 + SourceDID: l.SourceDID, 81 + URI: l.URI, 82 + CID: l.CID, 83 + Val: l.Val, 84 + CreatedAt: l.CreatedAt, 85 + } 86 + } 87 + return out, nil 88 + } 89 + 90 + func (s *MemStore) PutLabels(ctx context.Context, labels []labeling.Label) error { 91 + for _, l := range labels { 92 + src, err := syntax.ParseDID(l.SourceDID) 93 + if err != nil { 94 + return fmt.Errorf("invalid DID in label (%s): %w", l.SourceDID, err) 95 + } 96 + 97 + _, ok := s.Labels[l.URI] 98 + if !ok { 99 + s.Labels[l.URI] = map[syntax.DID]labeling.Label{} 100 + } 101 + 102 + s.Labels[l.URI][src] = l 103 + } 104 + return nil 105 + }