Monorepo for Tangled
1package models
2
3import (
4 "context"
5 "crypto/sha1"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "slices"
11 "time"
12
13 "github.com/bluesky-social/indigo/api/atproto"
14 "github.com/bluesky-social/indigo/atproto/syntax"
15 "github.com/bluesky-social/indigo/xrpc"
16 "tangled.org/core/api/tangled"
17 "tangled.org/core/idresolver"
18)
19
20type ConcreteType string
21
22const (
23 ConcreteTypeNull ConcreteType = "null"
24 ConcreteTypeString ConcreteType = "string"
25 ConcreteTypeInt ConcreteType = "integer"
26 ConcreteTypeBool ConcreteType = "boolean"
27)
28
29type ValueTypeFormat string
30
31const (
32 ValueTypeFormatAny ValueTypeFormat = "any"
33 ValueTypeFormatDid ValueTypeFormat = "did"
34)
35
36// ValueType represents an atproto lexicon type definition with constraints
37type ValueType struct {
38 Type ConcreteType `json:"type"`
39 Format ValueTypeFormat `json:"format,omitempty"`
40 Enum []string `json:"enum,omitempty"`
41}
42
43func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
44 return tangled.LabelDefinition_ValueType{
45 Type: string(vt.Type),
46 Format: string(vt.Format),
47 Enum: vt.Enum,
48 }
49}
50
51func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
52 return ValueType{
53 Type: ConcreteType(record.Type),
54 Format: ValueTypeFormat(record.Format),
55 Enum: record.Enum,
56 }
57}
58
59func (vt ValueType) IsConcreteType() bool {
60 return vt.Type == ConcreteTypeNull ||
61 vt.Type == ConcreteTypeString ||
62 vt.Type == ConcreteTypeInt ||
63 vt.Type == ConcreteTypeBool
64}
65
66func (vt ValueType) IsNull() bool {
67 return vt.Type == ConcreteTypeNull
68}
69
70func (vt ValueType) IsString() bool {
71 return vt.Type == ConcreteTypeString
72}
73
74func (vt ValueType) IsInt() bool {
75 return vt.Type == ConcreteTypeInt
76}
77
78func (vt ValueType) IsBool() bool {
79 return vt.Type == ConcreteTypeBool
80}
81
82func (vt ValueType) IsEnum() bool {
83 return len(vt.Enum) > 0
84}
85
86func (vt ValueType) IsDidFormat() bool {
87 return vt.Format == ValueTypeFormatDid
88}
89
90func (vt ValueType) IsAnyFormat() bool {
91 return vt.Format == ValueTypeFormatAny
92}
93
94type LabelDefinition struct {
95 Id int64
96 Did string
97 Rkey string
98
99 Name string
100 ValueType ValueType
101 Scope []string
102 Color *string
103 Multiple bool
104 Created time.Time
105}
106
107func (l *LabelDefinition) AtUri() syntax.ATURI {
108 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
109}
110
111func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
112 vt := l.ValueType.AsRecord()
113 return tangled.LabelDefinition{
114 Name: l.Name,
115 Color: l.Color,
116 CreatedAt: l.Created.Format(time.RFC3339),
117 Multiple: &l.Multiple,
118 Scope: l.Scope,
119 ValueType: &vt,
120 }
121}
122
123// random color for a given seed
124func randomColor(seed string) string {
125 hash := sha1.Sum([]byte(seed))
126 hexStr := hex.EncodeToString(hash[:])
127 r := hexStr[0:2]
128 g := hexStr[2:4]
129 b := hexStr[4:6]
130
131 return fmt.Sprintf("#%s%s%s", r, g, b)
132}
133
134func (ld LabelDefinition) GetColor() string {
135 if ld.Color == nil {
136 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
137 color := randomColor(seed)
138 return color
139 }
140
141 return *ld.Color
142}
143
144func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
145 created, err := time.Parse(time.RFC3339, record.CreatedAt)
146 if err != nil {
147 created = time.Now()
148 }
149
150 multiple := false
151 if record.Multiple != nil {
152 multiple = *record.Multiple
153 }
154
155 var vt ValueType
156 if record.ValueType != nil {
157 vt = ValueTypeFromRecord(*record.ValueType)
158 }
159
160 return &LabelDefinition{
161 Did: did,
162 Rkey: rkey,
163
164 Name: record.Name,
165 ValueType: vt,
166 Scope: record.Scope,
167 Color: record.Color,
168 Multiple: multiple,
169 Created: created,
170 }, nil
171}
172
173type LabelOp struct {
174 Id int64
175 Did string
176 Rkey string
177 Subject syntax.ATURI
178 Operation LabelOperation
179 OperandKey string
180 OperandValue string
181 PerformedAt time.Time
182 IndexedAt time.Time
183}
184
185func (l LabelOp) SortAt() time.Time {
186 createdAt := l.PerformedAt
187 indexedAt := l.IndexedAt
188
189 // if we don't have an indexedat, fall back to now
190 if indexedAt.IsZero() {
191 indexedAt = time.Now()
192 }
193
194 // if createdat is invalid (before epoch), treat as null -> return zero time
195 if createdAt.Before(time.UnixMicro(0)) {
196 return time.Time{}
197 }
198
199 // if createdat is <= indexedat, use createdat
200 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
201 return createdAt
202 }
203
204 // otherwise, createdat is in the future relative to indexedat -> use indexedat
205 return indexedAt
206}
207
208type LabelOperation string
209
210const (
211 LabelOperationAdd LabelOperation = "add"
212 LabelOperationDel LabelOperation = "del"
213)
214
215// a record can create multiple label ops
216func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
217 performed, err := time.Parse(time.RFC3339, record.PerformedAt)
218 if err != nil {
219 performed = time.Now()
220 }
221
222 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
223 return LabelOp{
224 Did: did,
225 Rkey: rkey,
226 Subject: syntax.ATURI(record.Subject),
227 OperandKey: operand.Key,
228 OperandValue: operand.Value,
229 PerformedAt: performed,
230 }
231 }
232
233 var ops []LabelOp
234 // deletes first, then additions
235 for _, o := range record.Delete {
236 if o != nil {
237 op := mkOp(o)
238 op.Operation = LabelOperationDel
239 ops = append(ops, op)
240 }
241 }
242 for _, o := range record.Add {
243 if o != nil {
244 op := mkOp(o)
245 op.Operation = LabelOperationAdd
246 ops = append(ops, op)
247 }
248 }
249
250 return ops
251}
252
253func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
254 if len(ops) == 0 {
255 return tangled.LabelOp{}
256 }
257
258 // use the first operation to establish common fields
259 first := ops[0]
260 record := tangled.LabelOp{
261 Subject: string(first.Subject),
262 PerformedAt: first.PerformedAt.Format(time.RFC3339),
263 }
264
265 var addOperands []*tangled.LabelOp_Operand
266 var deleteOperands []*tangled.LabelOp_Operand
267
268 for _, op := range ops {
269 operand := &tangled.LabelOp_Operand{
270 Key: op.OperandKey,
271 Value: op.OperandValue,
272 }
273
274 switch op.Operation {
275 case LabelOperationAdd:
276 addOperands = append(addOperands, operand)
277 case LabelOperationDel:
278 deleteOperands = append(deleteOperands, operand)
279 default:
280 return tangled.LabelOp{}
281 }
282 }
283
284 record.Add = addOperands
285 record.Delete = deleteOperands
286
287 return record
288}
289
290type set = map[string]struct{}
291
292type LabelState struct {
293 inner map[string]set
294 names map[string]string
295}
296
297func NewLabelState() LabelState {
298 return LabelState{
299 inner: make(map[string]set),
300 names: make(map[string]string),
301 }
302}
303
304func (s LabelState) LabelNames() []string {
305 var result []string
306 for key, valset := range s.inner {
307 if valset == nil {
308 continue
309 }
310 if name, ok := s.names[key]; ok {
311 result = append(result, name)
312 }
313 }
314 return result
315}
316
317// LabelNameValues returns composite "name:value" strings for all labels
318// that have non-empty values.
319func (s LabelState) LabelNameValues() []string {
320 var result []string
321 for key, valset := range s.inner {
322 if valset == nil {
323 continue
324 }
325 name, ok := s.names[key]
326 if !ok {
327 continue
328 }
329 for val := range valset {
330 if val != "" {
331 result = append(result, name+":"+val)
332 }
333 }
334 }
335 return result
336}
337
338func (s LabelState) Inner() map[string]set {
339 return s.inner
340}
341
342func (s LabelState) SetName(key, name string) {
343 s.names[key] = name
344}
345
346func (s LabelState) ContainsLabel(l string) bool {
347 if valset, exists := s.inner[l]; exists {
348 if valset != nil {
349 return true
350 }
351 }
352
353 return false
354}
355
356// go maps behavior in templates make this necessary,
357// indexing a map and getting `set` in return is apparently truthy
358func (s LabelState) ContainsLabelAndVal(l, v string) bool {
359 if valset, exists := s.inner[l]; exists {
360 if _, exists := valset[v]; exists {
361 return true
362 }
363 }
364
365 return false
366}
367
368func (s LabelState) GetValSet(l string) set {
369 if valset, exists := s.inner[l]; exists {
370 return valset
371 } else {
372 return make(set)
373 }
374}
375
376type LabelApplicationCtx struct {
377 Defs map[string]*LabelDefinition // labelAt -> labelDef
378}
379
380var (
381 LabelNoOpError = errors.New("no-op")
382)
383
384func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
385 def, ok := c.Defs[op.OperandKey]
386 if !ok {
387 // this def was deleted, but an op exists, so we just skip over the op
388 return nil
389 }
390
391 state.names[op.OperandKey] = def.Name
392
393 switch op.Operation {
394 case LabelOperationAdd:
395 // if valueset is empty, init it
396 if state.inner[op.OperandKey] == nil {
397 state.inner[op.OperandKey] = make(set)
398 }
399
400 // if valueset is populated & this val alr exists, this labelop is a noop
401 if valueSet, exists := state.inner[op.OperandKey]; exists {
402 if _, exists = valueSet[op.OperandValue]; exists {
403 return LabelNoOpError
404 }
405 }
406
407 if def.Multiple {
408 // append to set
409 state.inner[op.OperandKey][op.OperandValue] = struct{}{}
410 } else {
411 // reset to just this value
412 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
413 }
414
415 case LabelOperationDel:
416 // if label DNE, then deletion is a no-op
417 if valueSet, exists := state.inner[op.OperandKey]; !exists {
418 return LabelNoOpError
419 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
420 return LabelNoOpError
421 }
422
423 if def.Multiple {
424 // remove from set
425 delete(state.inner[op.OperandKey], op.OperandValue)
426 } else {
427 // reset the entire label
428 delete(state.inner, op.OperandKey)
429 }
430
431 // if the map becomes empty, then set it to nil, this is just the inverse of add
432 if len(state.inner[op.OperandKey]) == 0 {
433 state.inner[op.OperandKey] = nil
434 }
435
436 }
437
438 return nil
439}
440
441func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
442 // sort label ops in sort order first
443 slices.SortFunc(ops, func(a, b LabelOp) int {
444 return a.SortAt().Compare(b.SortAt())
445 })
446
447 // apply ops in sequence
448 for _, o := range ops {
449 _ = c.ApplyLabelOp(state, o)
450 }
451}
452
453// IsInverse checks if one label operation is the inverse of another
454// returns true if one is an add and the other is a delete with the same key and value
455func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
456 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
457 return false
458 }
459
460 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
461 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
462}
463
464// removes pairs of label operations that are inverses of each other
465// from the given slice. the function preserves the order of remaining operations.
466func ReduceLabelOps(ops []LabelOp) []LabelOp {
467 if len(ops) <= 1 {
468 return ops
469 }
470
471 keep := make([]bool, len(ops))
472 for i := range keep {
473 keep[i] = true
474 }
475
476 for i := range ops {
477 if !keep[i] {
478 continue
479 }
480
481 for j := i + 1; j < len(ops); j++ {
482 if !keep[j] {
483 continue
484 }
485
486 if ops[i].IsInverse(ops[j]) {
487 keep[i] = false
488 keep[j] = false
489 break // move to next i since this one is now eliminated
490 }
491 }
492 }
493
494 // build result slice with only kept operations
495 var result []LabelOp
496 for i, op := range ops {
497 if keep[i] {
498 result = append(result, op)
499 }
500 }
501
502 return result
503}
504
505func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
506 var labelDefs []LabelDefinition
507 ctx := context.Background()
508
509 for _, dl := range aturis {
510 atUri, err := syntax.ParseATURI(dl)
511 if err != nil {
512 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
513 }
514 if atUri.Collection() != tangled.LabelDefinitionNSID {
515 return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
516 }
517
518 owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
519 if err != nil {
520 return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
521 }
522
523 xrpcc := xrpc.Client{
524 Host: owner.PDSEndpoint(),
525 }
526
527 record, err := atproto.RepoGetRecord(
528 ctx,
529 &xrpcc,
530 "",
531 atUri.Collection().String(),
532 atUri.Authority().String(),
533 atUri.RecordKey().String(),
534 )
535 if err != nil {
536 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
537 }
538
539 if record != nil {
540 bytes, err := record.Value.MarshalJSON()
541 if err != nil {
542 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err)
543 }
544
545 raw := json.RawMessage(bytes)
546 labelRecord := tangled.LabelDefinition{}
547 err = json.Unmarshal(raw, &labelRecord)
548 if err != nil {
549 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err)
550 }
551
552 labelDef, err := LabelDefinitionFromRecord(
553 atUri.Authority().String(),
554 atUri.RecordKey().String(),
555 labelRecord,
556 )
557 if err != nil {
558 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err)
559 }
560
561 labelDefs = append(labelDefs, *labelDef)
562 }
563 }
564
565 return labelDefs, nil
566}