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
317func (s LabelState) Inner() map[string]set {
318 return s.inner
319}
320
321func (s LabelState) SetName(key, name string) {
322 s.names[key] = name
323}
324
325func (s LabelState) ContainsLabel(l string) bool {
326 if valset, exists := s.inner[l]; exists {
327 if valset != nil {
328 return true
329 }
330 }
331
332 return false
333}
334
335// go maps behavior in templates make this necessary,
336// indexing a map and getting `set` in return is apparently truthy
337func (s LabelState) ContainsLabelAndVal(l, v string) bool {
338 if valset, exists := s.inner[l]; exists {
339 if _, exists := valset[v]; exists {
340 return true
341 }
342 }
343
344 return false
345}
346
347func (s LabelState) GetValSet(l string) set {
348 if valset, exists := s.inner[l]; exists {
349 return valset
350 } else {
351 return make(set)
352 }
353}
354
355type LabelApplicationCtx struct {
356 Defs map[string]*LabelDefinition // labelAt -> labelDef
357}
358
359var (
360 LabelNoOpError = errors.New("no-op")
361)
362
363func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
364 def, ok := c.Defs[op.OperandKey]
365 if !ok {
366 // this def was deleted, but an op exists, so we just skip over the op
367 return nil
368 }
369
370 state.names[op.OperandKey] = def.Name
371
372 switch op.Operation {
373 case LabelOperationAdd:
374 // if valueset is empty, init it
375 if state.inner[op.OperandKey] == nil {
376 state.inner[op.OperandKey] = make(set)
377 }
378
379 // if valueset is populated & this val alr exists, this labelop is a noop
380 if valueSet, exists := state.inner[op.OperandKey]; exists {
381 if _, exists = valueSet[op.OperandValue]; exists {
382 return LabelNoOpError
383 }
384 }
385
386 if def.Multiple {
387 // append to set
388 state.inner[op.OperandKey][op.OperandValue] = struct{}{}
389 } else {
390 // reset to just this value
391 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
392 }
393
394 case LabelOperationDel:
395 // if label DNE, then deletion is a no-op
396 if valueSet, exists := state.inner[op.OperandKey]; !exists {
397 return LabelNoOpError
398 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
399 return LabelNoOpError
400 }
401
402 if def.Multiple {
403 // remove from set
404 delete(state.inner[op.OperandKey], op.OperandValue)
405 } else {
406 // reset the entire label
407 delete(state.inner, op.OperandKey)
408 }
409
410 // if the map becomes empty, then set it to nil, this is just the inverse of add
411 if len(state.inner[op.OperandKey]) == 0 {
412 state.inner[op.OperandKey] = nil
413 }
414
415 }
416
417 return nil
418}
419
420func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
421 // sort label ops in sort order first
422 slices.SortFunc(ops, func(a, b LabelOp) int {
423 return a.SortAt().Compare(b.SortAt())
424 })
425
426 // apply ops in sequence
427 for _, o := range ops {
428 _ = c.ApplyLabelOp(state, o)
429 }
430}
431
432// IsInverse checks if one label operation is the inverse of another
433// returns true if one is an add and the other is a delete with the same key and value
434func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
435 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
436 return false
437 }
438
439 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
440 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
441}
442
443// removes pairs of label operations that are inverses of each other
444// from the given slice. the function preserves the order of remaining operations.
445func ReduceLabelOps(ops []LabelOp) []LabelOp {
446 if len(ops) <= 1 {
447 return ops
448 }
449
450 keep := make([]bool, len(ops))
451 for i := range keep {
452 keep[i] = true
453 }
454
455 for i := range ops {
456 if !keep[i] {
457 continue
458 }
459
460 for j := i + 1; j < len(ops); j++ {
461 if !keep[j] {
462 continue
463 }
464
465 if ops[i].IsInverse(ops[j]) {
466 keep[i] = false
467 keep[j] = false
468 break // move to next i since this one is now eliminated
469 }
470 }
471 }
472
473 // build result slice with only kept operations
474 var result []LabelOp
475 for i, op := range ops {
476 if keep[i] {
477 result = append(result, op)
478 }
479 }
480
481 return result
482}
483
484func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
485 var labelDefs []LabelDefinition
486 ctx := context.Background()
487
488 for _, dl := range aturis {
489 atUri, err := syntax.ParseATURI(dl)
490 if err != nil {
491 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
492 }
493 if atUri.Collection() != tangled.LabelDefinitionNSID {
494 return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
495 }
496
497 owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
498 if err != nil {
499 return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
500 }
501
502 xrpcc := xrpc.Client{
503 Host: owner.PDSEndpoint(),
504 }
505
506 record, err := atproto.RepoGetRecord(
507 ctx,
508 &xrpcc,
509 "",
510 atUri.Collection().String(),
511 atUri.Authority().String(),
512 atUri.RecordKey().String(),
513 )
514 if err != nil {
515 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
516 }
517
518 if record != nil {
519 bytes, err := record.Value.MarshalJSON()
520 if err != nil {
521 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err)
522 }
523
524 raw := json.RawMessage(bytes)
525 labelRecord := tangled.LabelDefinition{}
526 err = json.Unmarshal(raw, &labelRecord)
527 if err != nil {
528 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err)
529 }
530
531 labelDef, err := LabelDefinitionFromRecord(
532 atUri.Authority().String(),
533 atUri.RecordKey().String(),
534 labelRecord,
535 )
536 if err != nil {
537 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err)
538 }
539
540 labelDefs = append(labelDefs, *labelDef)
541 }
542 }
543
544 return labelDefs, nil
545}