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