Monorepo for Tangled
at master 566 lines 13 kB view raw
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}