A vibe coded tangled fork which supports pijul.
at sl/uosmolqmqxvw 724 lines 18 kB view raw
1package models 2 3import ( 4 "context" 5 "crypto/sha1" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "regexp" 11 "slices" 12 "strings" 13 "time" 14 15 "github.com/bluesky-social/indigo/api/atproto" 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 "github.com/bluesky-social/indigo/xrpc" 18 "tangled.org/core/api/tangled" 19 "tangled.org/core/idresolver" 20) 21 22type ConcreteType string 23 24const ( 25 ConcreteTypeNull ConcreteType = "null" 26 ConcreteTypeString ConcreteType = "string" 27 ConcreteTypeInt ConcreteType = "integer" 28 ConcreteTypeBool ConcreteType = "boolean" 29) 30 31type ValueTypeFormat string 32 33const ( 34 ValueTypeFormatAny ValueTypeFormat = "any" 35 ValueTypeFormatDid ValueTypeFormat = "did" 36) 37 38// ValueType represents an atproto lexicon type definition with constraints 39type ValueType struct { 40 Type ConcreteType `json:"type"` 41 Format ValueTypeFormat `json:"format,omitempty"` 42 Enum []string `json:"enum,omitempty"` 43} 44 45func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType { 46 return tangled.LabelDefinition_ValueType{ 47 Type: string(vt.Type), 48 Format: string(vt.Format), 49 Enum: vt.Enum, 50 } 51} 52 53func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType { 54 return ValueType{ 55 Type: ConcreteType(record.Type), 56 Format: ValueTypeFormat(record.Format), 57 Enum: record.Enum, 58 } 59} 60 61func (vt ValueType) IsConcreteType() bool { 62 return vt.Type == ConcreteTypeNull || 63 vt.Type == ConcreteTypeString || 64 vt.Type == ConcreteTypeInt || 65 vt.Type == ConcreteTypeBool 66} 67 68func (vt ValueType) IsNull() bool { 69 return vt.Type == ConcreteTypeNull 70} 71 72func (vt ValueType) IsString() bool { 73 return vt.Type == ConcreteTypeString 74} 75 76func (vt ValueType) IsInt() bool { 77 return vt.Type == ConcreteTypeInt 78} 79 80func (vt ValueType) IsBool() bool { 81 return vt.Type == ConcreteTypeBool 82} 83 84func (vt ValueType) IsEnum() bool { 85 return len(vt.Enum) > 0 86} 87 88func (vt ValueType) IsDidFormat() bool { 89 return vt.Format == ValueTypeFormatDid 90} 91 92func (vt ValueType) IsAnyFormat() bool { 93 return vt.Format == ValueTypeFormatAny 94} 95 96type LabelDefinition struct { 97 Id int64 98 Did string 99 Rkey string 100 101 Name string 102 ValueType ValueType 103 Scope []string 104 Color *string 105 Multiple bool 106 Created time.Time 107} 108 109func (l *LabelDefinition) AtUri() syntax.ATURI { 110 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 111} 112 113func (l *LabelDefinition) AsRecord() tangled.LabelDefinition { 114 vt := l.ValueType.AsRecord() 115 return tangled.LabelDefinition{ 116 Name: l.Name, 117 Color: l.Color, 118 CreatedAt: l.Created.Format(time.RFC3339), 119 Multiple: &l.Multiple, 120 Scope: l.Scope, 121 ValueType: &vt, 122 } 123} 124 125var ( 126 // Label name should be alphanumeric with hyphens/underscores, but not start/end with them 127 labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`) 128 // Color should be a valid hex color 129 colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`) 130 // You can only label issues and pulls presently 131 validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID} 132) 133 134var _ Validator = new(LabelDefinition) 135 136func (l *LabelDefinition) Validate() error { 137 if l.Name == "" { 138 return fmt.Errorf("label name is empty") 139 } 140 if len(l.Name) > 40 { 141 return fmt.Errorf("label name too long (max 40 graphemes)") 142 } 143 if len(l.Name) < 1 { 144 return fmt.Errorf("label name too short (min 1 grapheme)") 145 } 146 if !labelNameRegex.MatchString(l.Name) { 147 return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)") 148 } 149 150 if !l.ValueType.IsConcreteType() { 151 return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", l.ValueType.Type) 152 } 153 154 // null type checks: cannot be enums, multiple or explicit format 155 if l.ValueType.IsNull() && l.ValueType.IsEnum() { 156 return fmt.Errorf("null type cannot be used in conjunction with enum type") 157 } 158 if l.ValueType.IsNull() && l.Multiple { 159 return fmt.Errorf("null type labels cannot be multiple") 160 } 161 if l.ValueType.IsNull() && !l.ValueType.IsAnyFormat() { 162 return fmt.Errorf("format cannot be used in conjunction with null type") 163 } 164 165 // format checks: cannot be used with enum, or integers 166 if !l.ValueType.IsAnyFormat() && l.ValueType.IsEnum() { 167 return fmt.Errorf("enum types cannot be used in conjunction with format specification") 168 } 169 170 if !l.ValueType.IsAnyFormat() && !l.ValueType.IsString() { 171 return fmt.Errorf("format specifications are only permitted on string types") 172 } 173 174 // validate scope (nsid format) 175 if l.Scope == nil { 176 return fmt.Errorf("scope is required") 177 } 178 for _, s := range l.Scope { 179 if _, err := syntax.ParseNSID(s); err != nil { 180 return fmt.Errorf("failed to parse scope: %w", err) 181 } 182 if !slices.Contains(validScopes, s) { 183 return fmt.Errorf("invalid scope: scope must be present in %q", validScopes) 184 } 185 } 186 187 // validate color if provided 188 if l.Color != nil { 189 color := strings.TrimSpace(*l.Color) 190 if color == "" { 191 // empty color is fine, set to nil 192 l.Color = nil 193 } else { 194 if !colorRegex.MatchString(color) { 195 return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)") 196 } 197 // expand 3-digit hex to 6-digit hex 198 if len(color) == 4 { // #ABC 199 color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3]) 200 } 201 // convert to uppercase for consistency 202 color = strings.ToUpper(color) 203 l.Color = &color 204 } 205 } 206 207 return nil 208} 209 210// ValidateOperandValue validates the label operation operand value based on 211// label definition. 212// 213// NOTE: This can modify the [LabelOp] 214func (def *LabelDefinition) ValidateOperandValue(op *LabelOp) error { 215 expectedKey := def.AtUri().String() 216 if op.OperandKey != def.AtUri().String() { 217 return fmt.Errorf("operand key %q does not match label definition URI %q", op.OperandKey, expectedKey) 218 } 219 220 valueType := def.ValueType 221 222 // this is permitted, it "unsets" a label 223 if op.OperandValue == "" { 224 op.Operation = LabelOperationDel 225 return nil 226 } 227 228 switch valueType.Type { 229 case ConcreteTypeNull: 230 // For null type, value should be empty 231 if op.OperandValue != "null" { 232 return fmt.Errorf("null type requires empty value, got %q", op.OperandValue) 233 } 234 235 case ConcreteTypeString: 236 // For string type, validate enum constraints if present 237 if valueType.IsEnum() { 238 if !slices.Contains(valueType.Enum, op.OperandValue) { 239 return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) 240 } 241 } 242 243 switch valueType.Format { 244 case ValueTypeFormatDid: 245 if _, err := syntax.ParseDID(op.OperandValue); err != nil { 246 return fmt.Errorf("failed to resolve did/handle: %w", err) 247 } 248 case ValueTypeFormatAny, "": 249 default: 250 return fmt.Errorf("unsupported format constraint: %q", valueType.Format) 251 } 252 253 case ConcreteTypeInt: 254 if op.OperandValue == "" { 255 return fmt.Errorf("integer type requires non-empty value") 256 } 257 if _, err := fmt.Sscanf(op.OperandValue, "%d", new(int)); err != nil { 258 return fmt.Errorf("value %q is not a valid integer", op.OperandValue) 259 } 260 261 if valueType.IsEnum() { 262 if !slices.Contains(valueType.Enum, op.OperandValue) { 263 return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) 264 } 265 } 266 267 case ConcreteTypeBool: 268 if op.OperandValue != "true" && op.OperandValue != "false" { 269 return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", op.OperandValue) 270 } 271 272 // validate enum constraints if present (though uncommon for booleans) 273 if valueType.IsEnum() { 274 if !slices.Contains(valueType.Enum, op.OperandValue) { 275 return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) 276 } 277 } 278 279 default: 280 return fmt.Errorf("unsupported value type: %q", valueType.Type) 281 } 282 283 return nil 284} 285 286// random color for a given seed 287func randomColor(seed string) string { 288 hash := sha1.Sum([]byte(seed)) 289 hexStr := hex.EncodeToString(hash[:]) 290 r := hexStr[0:2] 291 g := hexStr[2:4] 292 b := hexStr[4:6] 293 294 return fmt.Sprintf("#%s%s%s", r, g, b) 295} 296 297func (l LabelDefinition) GetColor() string { 298 if l.Color == nil { 299 seed := fmt.Sprintf("%d:%s:%s", l.Id, l.Did, l.Rkey) 300 color := randomColor(seed) 301 return color 302 } 303 304 return *l.Color 305} 306 307func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) { 308 created, err := time.Parse(time.RFC3339, record.CreatedAt) 309 if err != nil { 310 created = time.Now() 311 } 312 313 multiple := false 314 if record.Multiple != nil { 315 multiple = *record.Multiple 316 } 317 318 var vt ValueType 319 if record.ValueType != nil { 320 vt = ValueTypeFromRecord(*record.ValueType) 321 } 322 323 return &LabelDefinition{ 324 Did: did, 325 Rkey: rkey, 326 327 Name: record.Name, 328 ValueType: vt, 329 Scope: record.Scope, 330 Color: record.Color, 331 Multiple: multiple, 332 Created: created, 333 }, nil 334} 335 336type LabelOp struct { 337 Id int64 338 Did string 339 Rkey string 340 Subject syntax.ATURI 341 Operation LabelOperation 342 OperandKey string 343 OperandValue string 344 PerformedAt time.Time 345 IndexedAt time.Time 346} 347 348func (l LabelOp) SortAt() time.Time { 349 createdAt := l.PerformedAt 350 indexedAt := l.IndexedAt 351 352 // if we don't have an indexedat, fall back to now 353 if indexedAt.IsZero() { 354 indexedAt = time.Now() 355 } 356 357 // if createdat is invalid (before epoch), treat as null -> return zero time 358 if createdAt.Before(time.UnixMicro(0)) { 359 return time.Time{} 360 } 361 362 // if createdat is <= indexedat, use createdat 363 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) { 364 return createdAt 365 } 366 367 // otherwise, createdat is in the future relative to indexedat -> use indexedat 368 return indexedAt 369} 370 371var _ Validator = new(LabelOp) 372 373func (l *LabelOp) Validate() error { 374 if _, err := syntax.ParseATURI(string(l.Subject)); err != nil { 375 return fmt.Errorf("invalid subject URI: %w", err) 376 } 377 if l.Operation != LabelOperationAdd && l.Operation != LabelOperationDel { 378 return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", l.Operation) 379 } 380 // Validate performed time is not zero/invalid 381 if l.PerformedAt.IsZero() { 382 return fmt.Errorf("performed_at timestamp is required") 383 } 384 return nil 385} 386 387type LabelOperation string 388 389const ( 390 LabelOperationAdd LabelOperation = "add" 391 LabelOperationDel LabelOperation = "del" 392) 393 394// a record can create multiple label ops 395func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { 396 performed, err := time.Parse(time.RFC3339, record.PerformedAt) 397 if err != nil { 398 performed = time.Now() 399 } 400 401 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { 402 return LabelOp{ 403 Did: did, 404 Rkey: rkey, 405 Subject: syntax.ATURI(record.Subject), 406 OperandKey: operand.Key, 407 OperandValue: operand.Value, 408 PerformedAt: performed, 409 } 410 } 411 412 var ops []LabelOp 413 // deletes first, then additions 414 for _, o := range record.Delete { 415 if o != nil { 416 op := mkOp(o) 417 op.Operation = LabelOperationDel 418 ops = append(ops, op) 419 } 420 } 421 for _, o := range record.Add { 422 if o != nil { 423 op := mkOp(o) 424 op.Operation = LabelOperationAdd 425 ops = append(ops, op) 426 } 427 } 428 429 return ops 430} 431 432func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { 433 if len(ops) == 0 { 434 return tangled.LabelOp{} 435 } 436 437 // use the first operation to establish common fields 438 first := ops[0] 439 record := tangled.LabelOp{ 440 Subject: string(first.Subject), 441 PerformedAt: first.PerformedAt.Format(time.RFC3339), 442 } 443 444 var addOperands []*tangled.LabelOp_Operand 445 var deleteOperands []*tangled.LabelOp_Operand 446 447 for _, op := range ops { 448 operand := &tangled.LabelOp_Operand{ 449 Key: op.OperandKey, 450 Value: op.OperandValue, 451 } 452 453 switch op.Operation { 454 case LabelOperationAdd: 455 addOperands = append(addOperands, operand) 456 case LabelOperationDel: 457 deleteOperands = append(deleteOperands, operand) 458 default: 459 return tangled.LabelOp{} 460 } 461 } 462 463 record.Add = addOperands 464 record.Delete = deleteOperands 465 466 return record 467} 468 469type set = map[string]struct{} 470 471type LabelState struct { 472 inner map[string]set 473 names map[string]string 474} 475 476func NewLabelState() LabelState { 477 return LabelState{ 478 inner: make(map[string]set), 479 names: make(map[string]string), 480 } 481} 482 483func (s LabelState) LabelNames() []string { 484 var result []string 485 for key, valset := range s.inner { 486 if valset == nil { 487 continue 488 } 489 if name, ok := s.names[key]; ok { 490 result = append(result, name) 491 } 492 } 493 return result 494} 495 496func (s LabelState) Inner() map[string]set { 497 return s.inner 498} 499 500func (s LabelState) SetName(key, name string) { 501 s.names[key] = name 502} 503 504func (s LabelState) ContainsLabel(l string) bool { 505 if valset, exists := s.inner[l]; exists { 506 if valset != nil { 507 return true 508 } 509 } 510 511 return false 512} 513 514// go maps behavior in templates make this necessary, 515// indexing a map and getting `set` in return is apparently truthy 516func (s LabelState) ContainsLabelAndVal(l, v string) bool { 517 if valset, exists := s.inner[l]; exists { 518 if _, exists := valset[v]; exists { 519 return true 520 } 521 } 522 523 return false 524} 525 526func (s LabelState) GetValSet(l string) set { 527 if valset, exists := s.inner[l]; exists { 528 return valset 529 } else { 530 return make(set) 531 } 532} 533 534type LabelApplicationCtx struct { 535 Defs map[string]*LabelDefinition // labelAt -> labelDef 536} 537 538var ( 539 LabelNoOpError = errors.New("no-op") 540) 541 542func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { 543 def, ok := c.Defs[op.OperandKey] 544 if !ok { 545 // this def was deleted, but an op exists, so we just skip over the op 546 return nil 547 } 548 549 state.names[op.OperandKey] = def.Name 550 551 switch op.Operation { 552 case LabelOperationAdd: 553 // if valueset is empty, init it 554 if state.inner[op.OperandKey] == nil { 555 state.inner[op.OperandKey] = make(set) 556 } 557 558 // if valueset is populated & this val alr exists, this labelop is a noop 559 if valueSet, exists := state.inner[op.OperandKey]; exists { 560 if _, exists = valueSet[op.OperandValue]; exists { 561 return LabelNoOpError 562 } 563 } 564 565 if def.Multiple { 566 // append to set 567 state.inner[op.OperandKey][op.OperandValue] = struct{}{} 568 } else { 569 // reset to just this value 570 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} 571 } 572 573 case LabelOperationDel: 574 // if label DNE, then deletion is a no-op 575 if valueSet, exists := state.inner[op.OperandKey]; !exists { 576 return LabelNoOpError 577 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op 578 return LabelNoOpError 579 } 580 581 if def.Multiple { 582 // remove from set 583 delete(state.inner[op.OperandKey], op.OperandValue) 584 } else { 585 // reset the entire label 586 delete(state.inner, op.OperandKey) 587 } 588 589 // if the map becomes empty, then set it to nil, this is just the inverse of add 590 if len(state.inner[op.OperandKey]) == 0 { 591 state.inner[op.OperandKey] = nil 592 } 593 594 } 595 596 return nil 597} 598 599func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 600 // sort label ops in sort order first 601 slices.SortFunc(ops, func(a, b LabelOp) int { 602 return a.SortAt().Compare(b.SortAt()) 603 }) 604 605 // apply ops in sequence 606 for _, o := range ops { 607 _ = c.ApplyLabelOp(state, o) 608 } 609} 610 611// IsInverse checks if one label operation is the inverse of another 612// returns true if one is an add and the other is a delete with the same key and value 613func (op1 LabelOp) IsInverse(op2 LabelOp) bool { 614 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { 615 return false 616 } 617 618 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || 619 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) 620} 621 622// removes pairs of label operations that are inverses of each other 623// from the given slice. the function preserves the order of remaining operations. 624func ReduceLabelOps(ops []LabelOp) []LabelOp { 625 if len(ops) <= 1 { 626 return ops 627 } 628 629 keep := make([]bool, len(ops)) 630 for i := range keep { 631 keep[i] = true 632 } 633 634 for i := range ops { 635 if !keep[i] { 636 continue 637 } 638 639 for j := i + 1; j < len(ops); j++ { 640 if !keep[j] { 641 continue 642 } 643 644 if ops[i].IsInverse(ops[j]) { 645 keep[i] = false 646 keep[j] = false 647 break // move to next i since this one is now eliminated 648 } 649 } 650 } 651 652 // build result slice with only kept operations 653 var result []LabelOp 654 for i, op := range ops { 655 if keep[i] { 656 result = append(result, op) 657 } 658 } 659 660 return result 661} 662 663func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) { 664 var labelDefs []LabelDefinition 665 ctx := context.Background() 666 667 for _, dl := range aturis { 668 atUri, err := syntax.ParseATURI(dl) 669 if err != nil { 670 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err) 671 } 672 if atUri.Collection() != tangled.LabelDefinitionNSID { 673 return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri) 674 } 675 676 owner, err := r.ResolveIdent(ctx, atUri.Authority().String()) 677 if err != nil { 678 return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err) 679 } 680 681 xrpcc := xrpc.Client{ 682 Host: owner.PDSEndpoint(), 683 } 684 685 record, err := atproto.RepoGetRecord( 686 ctx, 687 &xrpcc, 688 "", 689 atUri.Collection().String(), 690 atUri.Authority().String(), 691 atUri.RecordKey().String(), 692 ) 693 if err != nil { 694 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err) 695 } 696 697 if record != nil { 698 bytes, err := record.Value.MarshalJSON() 699 if err != nil { 700 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err) 701 } 702 703 raw := json.RawMessage(bytes) 704 labelRecord := tangled.LabelDefinition{} 705 err = json.Unmarshal(raw, &labelRecord) 706 if err != nil { 707 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err) 708 } 709 710 labelDef, err := LabelDefinitionFromRecord( 711 atUri.Authority().String(), 712 atUri.RecordKey().String(), 713 labelRecord, 714 ) 715 if err != nil { 716 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err) 717 } 718 719 labelDefs = append(labelDefs, *labelDef) 720 } 721 } 722 723 return labelDefs, nil 724}