Monorepo for Tangled
at f6ac2d9789063b566b9b50795c2edf1a72f33172 545 lines 12 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 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}