forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 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} 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}