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/consts" 18 "tangled.org/core/idresolver" 19) 20 21type ConcreteType string 22 23const ( 24 ConcreteTypeNull ConcreteType = "null" 25 ConcreteTypeString ConcreteType = "string" 26 ConcreteTypeInt ConcreteType = "integer" 27 ConcreteTypeBool ConcreteType = "boolean" 28) 29 30type ValueTypeFormat string 31 32const ( 33 ValueTypeFormatAny ValueTypeFormat = "any" 34 ValueTypeFormatDid ValueTypeFormat = "did" 35) 36 37// ValueType represents an atproto lexicon type definition with constraints 38type ValueType struct { 39 Type ConcreteType `json:"type"` 40 Format ValueTypeFormat `json:"format,omitempty"` 41 Enum []string `json:"enum,omitempty"` 42} 43 44func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType { 45 return tangled.LabelDefinition_ValueType{ 46 Type: string(vt.Type), 47 Format: string(vt.Format), 48 Enum: vt.Enum, 49 } 50} 51 52func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType { 53 return ValueType{ 54 Type: ConcreteType(record.Type), 55 Format: ValueTypeFormat(record.Format), 56 Enum: record.Enum, 57 } 58} 59 60func (vt ValueType) IsConcreteType() bool { 61 return vt.Type == ConcreteTypeNull || 62 vt.Type == ConcreteTypeString || 63 vt.Type == ConcreteTypeInt || 64 vt.Type == ConcreteTypeBool 65} 66 67func (vt ValueType) IsNull() bool { 68 return vt.Type == ConcreteTypeNull 69} 70 71func (vt ValueType) IsString() bool { 72 return vt.Type == ConcreteTypeString 73} 74 75func (vt ValueType) IsInt() bool { 76 return vt.Type == ConcreteTypeInt 77} 78 79func (vt ValueType) IsBool() bool { 80 return vt.Type == ConcreteTypeBool 81} 82 83func (vt ValueType) IsEnum() bool { 84 return len(vt.Enum) > 0 85} 86 87func (vt ValueType) IsDidFormat() bool { 88 return vt.Format == ValueTypeFormatDid 89} 90 91func (vt ValueType) IsAnyFormat() bool { 92 return vt.Format == ValueTypeFormatAny 93} 94 95type LabelDefinition struct { 96 Id int64 97 Did string 98 Rkey string 99 100 Name string 101 ValueType ValueType 102 Scope []string 103 Color *string 104 Multiple bool 105 Created time.Time 106} 107 108func (l *LabelDefinition) AtUri() syntax.ATURI { 109 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 110} 111 112func (l *LabelDefinition) AsRecord() tangled.LabelDefinition { 113 vt := l.ValueType.AsRecord() 114 return tangled.LabelDefinition{ 115 Name: l.Name, 116 Color: l.Color, 117 CreatedAt: l.Created.Format(time.RFC3339), 118 Multiple: &l.Multiple, 119 Scope: l.Scope, 120 ValueType: &vt, 121 } 122} 123 124// random color for a given seed 125func randomColor(seed string) string { 126 hash := sha1.Sum([]byte(seed)) 127 hexStr := hex.EncodeToString(hash[:]) 128 r := hexStr[0:2] 129 g := hexStr[2:4] 130 b := hexStr[4:6] 131 132 return fmt.Sprintf("#%s%s%s", r, g, b) 133} 134 135func (ld LabelDefinition) GetColor() string { 136 if ld.Color == nil { 137 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey) 138 color := randomColor(seed) 139 return color 140 } 141 142 return *ld.Color 143} 144 145func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) { 146 created, err := time.Parse(time.RFC3339, record.CreatedAt) 147 if err != nil { 148 created = time.Now() 149 } 150 151 multiple := false 152 if record.Multiple != nil { 153 multiple = *record.Multiple 154 } 155 156 var vt ValueType 157 if record.ValueType != nil { 158 vt = ValueTypeFromRecord(*record.ValueType) 159 } 160 161 return &LabelDefinition{ 162 Did: did, 163 Rkey: rkey, 164 165 Name: record.Name, 166 ValueType: vt, 167 Scope: record.Scope, 168 Color: record.Color, 169 Multiple: multiple, 170 Created: created, 171 }, nil 172} 173 174type LabelOp struct { 175 Id int64 176 Did string 177 Rkey string 178 Subject syntax.ATURI 179 Operation LabelOperation 180 OperandKey string 181 OperandValue string 182 PerformedAt time.Time 183 IndexedAt time.Time 184} 185 186func (l LabelOp) SortAt() time.Time { 187 createdAt := l.PerformedAt 188 indexedAt := l.IndexedAt 189 190 // if we don't have an indexedat, fall back to now 191 if indexedAt.IsZero() { 192 indexedAt = time.Now() 193 } 194 195 // if createdat is invalid (before epoch), treat as null -> return zero time 196 if createdAt.Before(time.UnixMicro(0)) { 197 return time.Time{} 198 } 199 200 // if createdat is <= indexedat, use createdat 201 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) { 202 return createdAt 203 } 204 205 // otherwise, createdat is in the future relative to indexedat -> use indexedat 206 return indexedAt 207} 208 209type LabelOperation string 210 211const ( 212 LabelOperationAdd LabelOperation = "add" 213 LabelOperationDel LabelOperation = "del" 214) 215 216// a record can create multiple label ops 217func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { 218 performed, err := time.Parse(time.RFC3339, record.PerformedAt) 219 if err != nil { 220 performed = time.Now() 221 } 222 223 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { 224 return LabelOp{ 225 Did: did, 226 Rkey: rkey, 227 Subject: syntax.ATURI(record.Subject), 228 OperandKey: operand.Key, 229 OperandValue: operand.Value, 230 PerformedAt: performed, 231 } 232 } 233 234 var ops []LabelOp 235 for _, o := range record.Add { 236 if o != nil { 237 op := mkOp(o) 238 op.Operation = LabelOperationAdd 239 ops = append(ops, op) 240 } 241 } 242 for _, o := range record.Delete { 243 if o != nil { 244 op := mkOp(o) 245 op.Operation = LabelOperationDel 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 DefaultLabelDefs() []string { 464 rkeys := []string{ 465 "wontfix", 466 "duplicate", 467 "assignee", 468 "good-first-issue", 469 "documentation", 470 } 471 472 defs := make([]string, len(rkeys)) 473 for i, r := range rkeys { 474 defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r) 475 } 476 477 return defs 478} 479 480func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) { 481 resolved, err := r.ResolveIdent(context.Background(), consts.TangledDid) 482 if err != nil { 483 return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", consts.TangledDid, err) 484 } 485 pdsEndpoint := resolved.PDSEndpoint() 486 if pdsEndpoint == "" { 487 return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", consts.TangledDid) 488 } 489 client := &xrpc.Client{ 490 Host: pdsEndpoint, 491 } 492 493 var labelDefs []LabelDefinition 494 495 for _, dl := range DefaultLabelDefs() { 496 atUri := syntax.ATURI(dl) 497 parsedUri, err := syntax.ParseATURI(string(atUri)) 498 if err != nil { 499 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", atUri, err) 500 } 501 record, err := atproto.RepoGetRecord( 502 context.Background(), 503 client, 504 "", 505 parsedUri.Collection().String(), 506 parsedUri.Authority().String(), 507 parsedUri.RecordKey().String(), 508 ) 509 if err != nil { 510 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err) 511 } 512 513 if record != nil { 514 bytes, err := record.Value.MarshalJSON() 515 if err != nil { 516 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err) 517 } 518 519 raw := json.RawMessage(bytes) 520 labelRecord := tangled.LabelDefinition{} 521 err = json.Unmarshal(raw, &labelRecord) 522 if err != nil { 523 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err) 524 } 525 526 labelDef, err := LabelDefinitionFromRecord( 527 parsedUri.Authority().String(), 528 parsedUri.RecordKey().String(), 529 labelRecord, 530 ) 531 if err != nil { 532 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err) 533 } 534 535 labelDefs = append(labelDefs, *labelDef) 536 } 537 } 538 539 return labelDefs, nil 540}