A vibe coded tangled fork which supports pijul.
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
496// LabelNameValues returns composite "name:value" strings for all labels
497// that have non-empty values.
498func (s LabelState) LabelNameValues() []string {
499 var result []string
500 for key, valset := range s.inner {
501 if valset == nil {
502 continue
503 }
504 name, ok := s.names[key]
505 if !ok {
506 continue
507 }
508 for val := range valset {
509 if val != "" {
510 result = append(result, name+":"+val)
511 }
512 }
513 }
514 return result
515}
516
517func (s LabelState) Inner() map[string]set {
518 return s.inner
519}
520
521func (s LabelState) SetName(key, name string) {
522 s.names[key] = name
523}
524
525func (s LabelState) ContainsLabel(l string) bool {
526 if valset, exists := s.inner[l]; exists {
527 if valset != nil {
528 return true
529 }
530 }
531
532 return false
533}
534
535// go maps behavior in templates make this necessary,
536// indexing a map and getting `set` in return is apparently truthy
537func (s LabelState) ContainsLabelAndVal(l, v string) bool {
538 if valset, exists := s.inner[l]; exists {
539 if _, exists := valset[v]; exists {
540 return true
541 }
542 }
543
544 return false
545}
546
547func (s LabelState) GetValSet(l string) set {
548 if valset, exists := s.inner[l]; exists {
549 return valset
550 } else {
551 return make(set)
552 }
553}
554
555type LabelApplicationCtx struct {
556 Defs map[string]*LabelDefinition // labelAt -> labelDef
557}
558
559var (
560 LabelNoOpError = errors.New("no-op")
561)
562
563func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
564 def, ok := c.Defs[op.OperandKey]
565 if !ok {
566 // this def was deleted, but an op exists, so we just skip over the op
567 return nil
568 }
569
570 state.names[op.OperandKey] = def.Name
571
572 switch op.Operation {
573 case LabelOperationAdd:
574 // if valueset is empty, init it
575 if state.inner[op.OperandKey] == nil {
576 state.inner[op.OperandKey] = make(set)
577 }
578
579 // if valueset is populated & this val alr exists, this labelop is a noop
580 if valueSet, exists := state.inner[op.OperandKey]; exists {
581 if _, exists = valueSet[op.OperandValue]; exists {
582 return LabelNoOpError
583 }
584 }
585
586 if def.Multiple {
587 // append to set
588 state.inner[op.OperandKey][op.OperandValue] = struct{}{}
589 } else {
590 // reset to just this value
591 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
592 }
593
594 case LabelOperationDel:
595 // if label DNE, then deletion is a no-op
596 if valueSet, exists := state.inner[op.OperandKey]; !exists {
597 return LabelNoOpError
598 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
599 return LabelNoOpError
600 }
601
602 if def.Multiple {
603 // remove from set
604 delete(state.inner[op.OperandKey], op.OperandValue)
605 } else {
606 // reset the entire label
607 delete(state.inner, op.OperandKey)
608 }
609
610 // if the map becomes empty, then set it to nil, this is just the inverse of add
611 if len(state.inner[op.OperandKey]) == 0 {
612 state.inner[op.OperandKey] = nil
613 }
614
615 }
616
617 return nil
618}
619
620func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
621 // sort label ops in sort order first
622 slices.SortFunc(ops, func(a, b LabelOp) int {
623 return a.SortAt().Compare(b.SortAt())
624 })
625
626 // apply ops in sequence
627 for _, o := range ops {
628 _ = c.ApplyLabelOp(state, o)
629 }
630}
631
632// IsInverse checks if one label operation is the inverse of another
633// returns true if one is an add and the other is a delete with the same key and value
634func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
635 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
636 return false
637 }
638
639 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
640 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
641}
642
643// removes pairs of label operations that are inverses of each other
644// from the given slice. the function preserves the order of remaining operations.
645func ReduceLabelOps(ops []LabelOp) []LabelOp {
646 if len(ops) <= 1 {
647 return ops
648 }
649
650 keep := make([]bool, len(ops))
651 for i := range keep {
652 keep[i] = true
653 }
654
655 for i := range ops {
656 if !keep[i] {
657 continue
658 }
659
660 for j := i + 1; j < len(ops); j++ {
661 if !keep[j] {
662 continue
663 }
664
665 if ops[i].IsInverse(ops[j]) {
666 keep[i] = false
667 keep[j] = false
668 break // move to next i since this one is now eliminated
669 }
670 }
671 }
672
673 // build result slice with only kept operations
674 var result []LabelOp
675 for i, op := range ops {
676 if keep[i] {
677 result = append(result, op)
678 }
679 }
680
681 return result
682}
683
684func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
685 var labelDefs []LabelDefinition
686 ctx := context.Background()
687
688 for _, dl := range aturis {
689 atUri, err := syntax.ParseATURI(dl)
690 if err != nil {
691 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
692 }
693 if atUri.Collection() != tangled.LabelDefinitionNSID {
694 return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
695 }
696
697 owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
698 if err != nil {
699 return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
700 }
701
702 xrpcc := xrpc.Client{
703 Host: owner.PDSEndpoint(),
704 }
705
706 record, err := atproto.RepoGetRecord(
707 ctx,
708 &xrpcc,
709 "",
710 atUri.Collection().String(),
711 atUri.Authority().String(),
712 atUri.RecordKey().String(),
713 )
714 if err != nil {
715 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
716 }
717
718 if record != nil {
719 bytes, err := record.Value.MarshalJSON()
720 if err != nil {
721 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err)
722 }
723
724 raw := json.RawMessage(bytes)
725 labelRecord := tangled.LabelDefinition{}
726 err = json.Unmarshal(raw, &labelRecord)
727 if err != nil {
728 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err)
729 }
730
731 labelDef, err := LabelDefinitionFromRecord(
732 atUri.Authority().String(),
733 atUri.RecordKey().String(),
734 labelRecord,
735 )
736 if err != nil {
737 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err)
738 }
739
740 labelDefs = append(labelDefs, *labelDef)
741 }
742 }
743
744 return labelDefs, nil
745}