···
15
+
"github.com/bluesky-social/indigo/atproto/syntax"
16
+
"tangled.sh/tangled.sh/core/api/tangled"
19
+
type ConcreteType string
22
+
ConcreteTypeNull ConcreteType = "null"
23
+
ConcreteTypeString ConcreteType = "string"
24
+
ConcreteTypeInt ConcreteType = "integer"
25
+
ConcreteTypeBool ConcreteType = "boolean"
28
+
type ValueTypeFormat string
31
+
ValueTypeFormatAny ValueTypeFormat = "any"
32
+
ValueTypeFormatDid ValueTypeFormat = "did"
35
+
// ValueType represents an atproto lexicon type definition with constraints
36
+
type ValueType struct {
37
+
Type ConcreteType `json:"type"`
38
+
Format ValueTypeFormat `json:"format,omitempty"`
39
+
Enum []string `json:"enum,omitempty"`
42
+
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
43
+
return tangled.LabelDefinition_ValueType{
44
+
Type: string(vt.Type),
45
+
Format: string(vt.Format),
50
+
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
52
+
Type: ConcreteType(record.Type),
53
+
Format: ValueTypeFormat(record.Format),
58
+
func (vt ValueType) IsConcreteType() bool {
59
+
return vt.Type == ConcreteTypeNull ||
60
+
vt.Type == ConcreteTypeString ||
61
+
vt.Type == ConcreteTypeInt ||
62
+
vt.Type == ConcreteTypeBool
65
+
func (vt ValueType) IsNull() bool {
66
+
return vt.Type == ConcreteTypeNull
69
+
func (vt ValueType) IsString() bool {
70
+
return vt.Type == ConcreteTypeString
73
+
func (vt ValueType) IsInt() bool {
74
+
return vt.Type == ConcreteTypeInt
77
+
func (vt ValueType) IsBool() bool {
78
+
return vt.Type == ConcreteTypeBool
81
+
func (vt ValueType) IsEnumType() bool {
82
+
return len(vt.Enum) > 0
85
+
type LabelDefinition struct {
98
+
func (l *LabelDefinition) AtUri() syntax.ATURI {
99
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
102
+
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
103
+
vt := l.ValueType.AsRecord()
104
+
return tangled.LabelDefinition{
107
+
CreatedAt: l.Created.Format(time.RFC3339),
108
+
Multiple: &l.Multiple,
109
+
Scope: l.Scope.String(),
114
+
// random color for a given seed
115
+
func randomColor(seed string) string {
116
+
hash := sha1.Sum([]byte(seed))
117
+
hexStr := hex.EncodeToString(hash[:])
122
+
return fmt.Sprintf("#%s%s%s", r, g, b)
125
+
func (ld LabelDefinition) GetColor() string {
126
+
if ld.Color == nil {
127
+
seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
128
+
color := randomColor(seed)
135
+
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) LabelDefinition {
136
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
138
+
created = time.Now()
142
+
if record.Multiple != nil {
143
+
multiple = *record.Multiple
147
+
if record.ValueType != nil {
148
+
vt = ValueTypeFromRecord(*record.ValueType)
151
+
return LabelDefinition{
157
+
Scope: syntax.NSID(record.Scope),
158
+
Color: record.Color,
159
+
Multiple: multiple,
164
+
func DeleteLabelDefinition(e Execer, filters ...filter) error {
165
+
var conditions []string
167
+
for _, filter := range filters {
168
+
conditions = append(conditions, filter.Condition())
169
+
args = append(args, filter.Arg()...)
172
+
if conditions != nil {
173
+
whereClause = " where " + strings.Join(conditions, " and ")
175
+
query := fmt.Sprintf(`delete from label_definitions %s`, whereClause)
176
+
_, err := e.Exec(query, args...)
180
+
func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) {
181
+
result, err := e.Exec(
182
+
`insert into label_definitions (
194
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
+
on conflict(did, rkey) do update set
196
+
name = excluded.name,
197
+
scope = excluded.scope,
198
+
color = excluded.color,
199
+
multiple = excluded.multiple`,
204
+
l.ValueType.Format,
205
+
strings.Join(l.ValueType.Enum, ","),
209
+
l.Created.Format(time.RFC3339),
210
+
time.Now().Format(time.RFC3339),
216
+
id, err := result.LastInsertId()
226
+
func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) {
227
+
var labelDefinitions []LabelDefinition
228
+
var conditions []string
231
+
for _, filter := range filters {
232
+
conditions = append(conditions, filter.Condition())
233
+
args = append(args, filter.Arg()...)
237
+
if conditions != nil {
238
+
whereClause = " where " + strings.Join(conditions, " and ")
241
+
query := fmt.Sprintf(
255
+
from label_definitions
262
+
rows, err := e.Query(query, args...)
269
+
var labelDefinition LabelDefinition
270
+
var createdAt, enumVariants string
271
+
var color sql.Null[string]
274
+
if err := rows.Scan(
275
+
&labelDefinition.Id,
276
+
&labelDefinition.Did,
277
+
&labelDefinition.Rkey,
278
+
&labelDefinition.Name,
279
+
&labelDefinition.ValueType.Type,
280
+
&labelDefinition.ValueType.Format,
282
+
&labelDefinition.Scope,
290
+
labelDefinition.Created, err = time.Parse(time.RFC3339, createdAt)
292
+
labelDefinition.Created = time.Now()
296
+
labelDefinition.Color = &color.V
300
+
labelDefinition.Multiple = true
303
+
if enumVariants != "" {
304
+
labelDefinition.ValueType.Enum = strings.Split(enumVariants, ",")
307
+
labelDefinitions = append(labelDefinitions, labelDefinition)
310
+
return labelDefinitions, nil
313
+
// helper to get exactly one label def
314
+
func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) {
315
+
labels, err := GetLabelDefinitions(e, filters...)
321
+
return nil, sql.ErrNoRows
324
+
if len(labels) != 1 {
325
+
return nil, fmt.Errorf("too many rows returned")
328
+
return &labels[0], nil
331
+
type LabelOp struct {
335
+
Subject syntax.ATURI
336
+
Operation LabelOperation
338
+
OperandValue string
339
+
PerformedAt time.Time
340
+
IndexedAt time.Time
343
+
func (l LabelOp) SortAt() time.Time {
344
+
createdAt := l.PerformedAt
345
+
indexedAt := l.IndexedAt
347
+
// if we don't have an indexedat, fall back to now
348
+
if indexedAt.IsZero() {
349
+
indexedAt = time.Now()
352
+
// if createdat is invalid (before epoch), treat as null -> return zero time
353
+
if createdAt.Before(time.UnixMicro(0)) {
357
+
// if createdat is <= indexedat, use createdat
358
+
if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
362
+
// otherwise, createdat is in the future relative to indexedat -> use indexedat
366
+
type LabelOperation string
369
+
LabelOperationAdd LabelOperation = "add"
370
+
LabelOperationDel LabelOperation = "del"
373
+
// a record can create multiple label ops
374
+
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
375
+
performed, err := time.Parse(time.RFC3339, record.PerformedAt)
377
+
performed = time.Now()
380
+
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
384
+
Subject: syntax.ATURI(record.Subject),
385
+
OperandKey: operand.Key,
386
+
OperandValue: operand.Value,
387
+
PerformedAt: performed,
392
+
for _, o := range record.Add {
395
+
op.Operation = LabelOperationAdd
396
+
ops = append(ops, op)
399
+
for _, o := range record.Delete {
402
+
op.Operation = LabelOperationDel
403
+
ops = append(ops, op)
410
+
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
412
+
return tangled.LabelOp{}
415
+
// use the first operation to establish common fields
417
+
record := tangled.LabelOp{
418
+
Subject: string(first.Subject),
419
+
PerformedAt: first.PerformedAt.Format(time.RFC3339),
422
+
var addOperands []*tangled.LabelOp_Operand
423
+
var deleteOperands []*tangled.LabelOp_Operand
425
+
for _, op := range ops {
426
+
operand := &tangled.LabelOp_Operand{
427
+
Key: op.OperandKey,
428
+
Value: op.OperandValue,
431
+
switch op.Operation {
432
+
case LabelOperationAdd:
433
+
addOperands = append(addOperands, operand)
434
+
case LabelOperationDel:
435
+
deleteOperands = append(deleteOperands, operand)
437
+
return tangled.LabelOp{}
441
+
record.Add = addOperands
442
+
record.Delete = deleteOperands
447
+
func AddLabelOp(e Execer, l *LabelOp) (int64, error) {
449
+
result, err := e.Exec(
450
+
`insert into label_ops (
460
+
values (?, ?, ?, ?, ?, ?, ?, ?)
461
+
on conflict(did, rkey, subject, operand_key, operand_value) do update set
462
+
operation = excluded.operation,
463
+
operand_value = excluded.operand_value,
464
+
performed = excluded.performed,
465
+
indexed = excluded.indexed`,
468
+
l.Subject.String(),
469
+
string(l.Operation),
472
+
l.PerformedAt.Format(time.RFC3339),
473
+
now.Format(time.RFC3339),
479
+
id, err := result.LastInsertId()
490
+
func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) {
491
+
var labelOps []LabelOp
492
+
var conditions []string
495
+
for _, filter := range filters {
496
+
conditions = append(conditions, filter.Condition())
497
+
args = append(args, filter.Arg()...)
501
+
if conditions != nil {
502
+
whereClause = " where " + strings.Join(conditions, " and ")
505
+
query := fmt.Sprintf(
524
+
rows, err := e.Query(query, args...)
531
+
var labelOp LabelOp
532
+
var performedAt, indexedAt string
534
+
if err := rows.Scan(
539
+
&labelOp.Operation,
540
+
&labelOp.OperandKey,
541
+
&labelOp.OperandValue,
548
+
labelOp.PerformedAt, err = time.Parse(time.RFC3339, performedAt)
550
+
labelOp.PerformedAt = time.Now()
553
+
labelOp.IndexedAt, err = time.Parse(time.RFC3339, indexedAt)
555
+
labelOp.IndexedAt = time.Now()
558
+
labelOps = append(labelOps, labelOp)
561
+
return labelOps, nil
564
+
// get labels for a given list of subject URIs
565
+
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) {
566
+
ops, err := GetLabelOps(e, filters...)
571
+
// group ops by subject
572
+
opsBySubject := make(map[syntax.ATURI][]LabelOp)
573
+
for _, op := range ops {
574
+
subject := syntax.ATURI(op.Subject)
575
+
opsBySubject[subject] = append(opsBySubject[subject], op)
578
+
// get all unique labelats for creating the context
579
+
labelAtSet := make(map[string]bool)
580
+
for _, op := range ops {
581
+
labelAtSet[op.OperandKey] = true
583
+
labelAts := slices.Collect(maps.Keys(labelAtSet))
585
+
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
590
+
// apply label ops for each subject and collect results
591
+
results := make(map[syntax.ATURI]LabelState)
592
+
for subject, subjectOps := range opsBySubject {
593
+
state := NewLabelState()
594
+
actx.ApplyLabelOps(state, subjectOps)
595
+
results[subject] = state
598
+
log.Println("results for get labels", "s", results)
600
+
return results, nil
603
+
type set = map[string]struct{}
605
+
type LabelState struct {
606
+
inner map[string]set
609
+
func NewLabelState() LabelState {
611
+
inner: make(map[string]set),
615
+
func (s LabelState) Inner() map[string]set {
619
+
func (s LabelState) ContainsLabel(l string) bool {
620
+
if valset, exists := s.inner[l]; exists {
629
+
func (s *LabelState) GetValSet(l string) set {
633
+
type LabelApplicationCtx struct {
634
+
defs map[string]*LabelDefinition // labelAt -> labelDef
638
+
LabelNoOpError = errors.New("no-op")
641
+
func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) {
642
+
labels, err := GetLabelDefinitions(e, filters...)
647
+
defs := make(map[string]*LabelDefinition)
648
+
for _, l := range labels {
649
+
defs[l.AtUri().String()] = &l
652
+
return &LabelApplicationCtx{defs}, nil
655
+
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
656
+
def := c.defs[op.OperandKey]
658
+
switch op.Operation {
659
+
case LabelOperationAdd:
660
+
// if valueset is empty, init it
661
+
if state.inner[op.OperandKey] == nil {
662
+
state.inner[op.OperandKey] = make(set)
665
+
// if valueset is populated & this val alr exists, this labelop is a noop
666
+
if valueSet, exists := state.inner[op.OperandKey]; exists {
667
+
if _, exists = valueSet[op.OperandValue]; exists {
668
+
return LabelNoOpError
674
+
state.inner[op.OperandKey][op.OperandValue] = struct{}{}
676
+
// reset to just this value
677
+
state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
680
+
case LabelOperationDel:
681
+
// if label DNE, then deletion is a no-op
682
+
if valueSet, exists := state.inner[op.OperandKey]; !exists {
683
+
return LabelNoOpError
684
+
} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
685
+
return LabelNoOpError
690
+
delete(state.inner[op.OperandKey], op.OperandValue)
692
+
// reset the entire label
693
+
delete(state.inner, op.OperandKey)
696
+
// if the map becomes empty, then set it to nil, this is just the inverse of add
697
+
if len(state.inner[op.OperandKey]) == 0 {
698
+
state.inner[op.OperandKey] = nil
706
+
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
707
+
// sort label ops in sort order first
708
+
slices.SortFunc(ops, func(a, b LabelOp) int {
709
+
return a.SortAt().Compare(b.SortAt())
712
+
// apply ops in sequence
713
+
for _, o := range ops {
714
+
_ = c.ApplyLabelOp(state, o)
718
+
type Label struct {
719
+
def *LabelDefinition