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