Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749package models
import ( "cmp" "context" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "regexp" "slices" "strings" "time"
"github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" "tangled.org/core/api/tangled" "tangled.org/core/idresolver")
type ConcreteType string
const ( ConcreteTypeNull ConcreteType = "null" ConcreteTypeString ConcreteType = "string" ConcreteTypeInt ConcreteType = "integer" ConcreteTypeBool ConcreteType = "boolean")
type ValueTypeFormat string
const ( ValueTypeFormatAny ValueTypeFormat = "any" ValueTypeFormatDid ValueTypeFormat = "did")
// ValueType represents an atproto lexicon type definition with constraintstype ValueType struct { Type ConcreteType `json:"type"` Format ValueTypeFormat `json:"format,omitempty"` Enum []string `json:"enum,omitempty"`}
func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType { return tangled.LabelDefinition_ValueType{ Type: string(vt.Type), Format: string(vt.Format), Enum: vt.Enum, }}
func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType { return ValueType{ Type: ConcreteType(record.Type), Format: ValueTypeFormat(record.Format), Enum: record.Enum, }}
func (vt ValueType) IsConcreteType() bool { return vt.Type == ConcreteTypeNull || vt.Type == ConcreteTypeString || vt.Type == ConcreteTypeInt || vt.Type == ConcreteTypeBool}
func (vt ValueType) IsNull() bool { return vt.Type == ConcreteTypeNull}
func (vt ValueType) IsString() bool { return vt.Type == ConcreteTypeString}
func (vt ValueType) IsInt() bool { return vt.Type == ConcreteTypeInt}
func (vt ValueType) IsBool() bool { return vt.Type == ConcreteTypeBool}
func (vt ValueType) IsEnum() bool { return len(vt.Enum) > 0}
func (vt ValueType) IsDidFormat() bool { return vt.Format == ValueTypeFormatDid}
func (vt ValueType) IsAnyFormat() bool { return vt.Format == ValueTypeFormatAny}
type LabelDefinition struct { Id int64 Did string Rkey string
Name string ValueType ValueType Scope []string Color *string Multiple bool Created time.Time}
func (l *LabelDefinition) AtUri() syntax.ATURI { return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))}
func (l *LabelDefinition) AsRecord() tangled.LabelDefinition { vt := l.ValueType.AsRecord() return tangled.LabelDefinition{ Name: l.Name, Color: l.Color, CreatedAt: l.Created.Format(time.RFC3339), Multiple: &l.Multiple, Scope: l.Scope, ValueType: &vt, }}
var ( // Label name should be alphanumeric with hyphens/underscores, but not start/end with them labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`) // Color should be a valid hex color colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`) // You can only label issues and pulls presently validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID})
var _ Validator = new(LabelDefinition)
func (l *LabelDefinition) Validate() error { if l.Name == "" { return fmt.Errorf("label name is empty") } if len(l.Name) > 40 { return fmt.Errorf("label name too long (max 40 graphemes)") } if len(l.Name) < 1 { return fmt.Errorf("label name too short (min 1 grapheme)") } if !labelNameRegex.MatchString(l.Name) { return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)") }
if !l.ValueType.IsConcreteType() { return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", l.ValueType.Type) }
// null type checks: cannot be enums, multiple or explicit format if l.ValueType.IsNull() && l.ValueType.IsEnum() { return fmt.Errorf("null type cannot be used in conjunction with enum type") } if l.ValueType.IsNull() && l.Multiple { return fmt.Errorf("null type labels cannot be multiple") } if l.ValueType.IsNull() && !l.ValueType.IsAnyFormat() { return fmt.Errorf("format cannot be used in conjunction with null type") }
// format checks: cannot be used with enum, or integers if !l.ValueType.IsAnyFormat() && l.ValueType.IsEnum() { return fmt.Errorf("enum types cannot be used in conjunction with format specification") }
if !l.ValueType.IsAnyFormat() && !l.ValueType.IsString() { return fmt.Errorf("format specifications are only permitted on string types") }
// validate scope (nsid format) if l.Scope == nil { return fmt.Errorf("scope is required") } for _, s := range l.Scope { if _, err := syntax.ParseNSID(s); err != nil { return fmt.Errorf("failed to parse scope: %w", err) } if !slices.Contains(validScopes, s) { return fmt.Errorf("invalid scope: scope must be present in %q", validScopes) } }
// validate color if provided if l.Color != nil { color := strings.TrimSpace(*l.Color) if color == "" { // empty color is fine, set to nil l.Color = nil } else { if !colorRegex.MatchString(color) { return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)") } // expand 3-digit hex to 6-digit hex if len(color) == 4 { // #ABC color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3]) } // convert to uppercase for consistency color = strings.ToUpper(color) l.Color = &color } }
return nil}
// ValidateOperandValue validates the label operation operand value based on// label definition.//// NOTE: This can modify the [LabelOp]func (def *LabelDefinition) ValidateOperandValue(op *LabelOp) error { expectedKey := def.AtUri().String() if op.OperandKey != def.AtUri().String() { return fmt.Errorf("operand key %q does not match label definition URI %q", op.OperandKey, expectedKey) }
valueType := def.ValueType
// this is permitted, it "unsets" a label if op.OperandValue == "" { op.Operation = LabelOperationDel return nil }
switch valueType.Type { case ConcreteTypeNull: // For null type, value should be empty if op.OperandValue != "null" { return fmt.Errorf("null type requires empty value, got %q", op.OperandValue) }
case ConcreteTypeString: // For string type, validate enum constraints if present if valueType.IsEnum() { if !slices.Contains(valueType.Enum, op.OperandValue) { return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) } }
switch valueType.Format { case ValueTypeFormatDid: if _, err := syntax.ParseDID(op.OperandValue); err != nil { return fmt.Errorf("failed to resolve did/handle: %w", err) } case ValueTypeFormatAny, "": default: return fmt.Errorf("unsupported format constraint: %q", valueType.Format) }
case ConcreteTypeInt: if op.OperandValue == "" { return fmt.Errorf("integer type requires non-empty value") } if _, err := fmt.Sscanf(op.OperandValue, "%d", new(int)); err != nil { return fmt.Errorf("value %q is not a valid integer", op.OperandValue) }
if valueType.IsEnum() { if !slices.Contains(valueType.Enum, op.OperandValue) { return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) } }
case ConcreteTypeBool: if op.OperandValue != "true" && op.OperandValue != "false" { return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", op.OperandValue) }
// validate enum constraints if present (though uncommon for booleans) if valueType.IsEnum() { if !slices.Contains(valueType.Enum, op.OperandValue) { return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum) } }
default: return fmt.Errorf("unsupported value type: %q", valueType.Type) }
return nil}
// random color for a given seedfunc randomColor(seed string) string { hash := sha1.Sum([]byte(seed)) hexStr := hex.EncodeToString(hash[:]) r := hexStr[0:2] g := hexStr[2:4] b := hexStr[4:6]
return fmt.Sprintf("#%s%s%s", r, g, b)}
func (l LabelDefinition) GetColor() string { if l.Color == nil { seed := fmt.Sprintf("%d:%s:%s", l.Id, l.Did, l.Rkey) color := randomColor(seed) return color }
return *l.Color}
func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { created = time.Time{} }
multiple := false if record.Multiple != nil { multiple = *record.Multiple }
var vt ValueType if record.ValueType != nil { vt = ValueTypeFromRecord(*record.ValueType) }
return &LabelDefinition{ Did: did, Rkey: rkey,
Name: record.Name, ValueType: vt, Scope: record.Scope, Color: record.Color, Multiple: multiple, Created: created, }, nil}
type LabelOp struct { Id int64 Did string Rkey string Subject syntax.ATURI Operation LabelOperation OperandKey string OperandValue string PerformedAt time.Time}
func (l LabelOp) SortAt() time.Time { // if createdat is invalid (before epoch), treat as null -> return zero time if l.PerformedAt.Before(time.UnixMicro(0)) { return time.Time{} } return l.PerformedAt}
var _ Validator = new(LabelOp)
func (l *LabelOp) Validate() error { if _, err := syntax.ParseATURI(string(l.Subject)); err != nil { return fmt.Errorf("invalid subject URI: %w", err) } if l.Operation != LabelOperationAdd && l.Operation != LabelOperationDel { return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", l.Operation) } // Validate performed time is not zero/invalid if l.PerformedAt.IsZero() { return fmt.Errorf("performed_at timestamp is required") } return nil}
type LabelOperation string
const ( LabelOperationAdd LabelOperation = "add" LabelOperationDel LabelOperation = "del")
// a record can create multiple label opsfunc LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { performed, err := time.Parse(time.RFC3339, record.PerformedAt) if err != nil { performed = time.Time{} }
mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { return LabelOp{ Did: did, Rkey: rkey, Subject: syntax.ATURI(record.Subject), OperandKey: operand.Key, OperandValue: operand.Value, PerformedAt: performed, } }
var ops []LabelOp // deletes first, then additions for _, o := range record.Delete { if o != nil { op := mkOp(o) op.Operation = LabelOperationDel ops = append(ops, op) } } for _, o := range record.Add { if o != nil { op := mkOp(o) op.Operation = LabelOperationAdd ops = append(ops, op) } }
return ops}
func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { if len(ops) == 0 { return tangled.LabelOp{} }
// use the first operation to establish common fields first := ops[0] record := tangled.LabelOp{ Subject: string(first.Subject), PerformedAt: first.PerformedAt.Format(time.RFC3339), }
var addOperands []*tangled.LabelOp_Operand var deleteOperands []*tangled.LabelOp_Operand
for _, op := range ops { operand := &tangled.LabelOp_Operand{ Key: op.OperandKey, Value: op.OperandValue, }
switch op.Operation { case LabelOperationAdd: addOperands = append(addOperands, operand) case LabelOperationDel: deleteOperands = append(deleteOperands, operand) default: return tangled.LabelOp{} } }
record.Add = addOperands record.Delete = deleteOperands
return record}
type set = map[string]struct{}
type LabelState struct { inner map[string]set names map[string]string}
func NewLabelState() LabelState { return LabelState{ inner: make(map[string]set), names: make(map[string]string), }}
func (s LabelState) LabelNames() []string { var result []string for key, valset := range s.inner { if valset == nil { continue } if name, ok := s.names[key]; ok { result = append(result, name) } } return result}
// LabelNameValues returns composite "name:value" strings for all labels// that have non-empty values.func (s LabelState) LabelNameValues() []string { var result []string for key, valset := range s.inner { if valset == nil { continue } name, ok := s.names[key] if !ok { continue } for val := range valset { if val != "" { result = append(result, name+":"+val) } } } return result}
func (s LabelState) Inner() map[string]set { return s.inner}
func (s LabelState) SetName(key, name string) { s.names[key] = name}
func (s LabelState) ContainsLabel(l string) bool { if valset, exists := s.inner[l]; exists { if valset != nil { return true } }
return false}
// go maps behavior in templates make this necessary,// indexing a map and getting `set` in return is apparently truthyfunc (s LabelState) ContainsLabelAndVal(l, v string) bool { if valset, exists := s.inner[l]; exists { if _, exists := valset[v]; exists { return true } }
return false}
func (s LabelState) GetValSet(l string) set { if valset, exists := s.inner[l]; exists { return valset } else { return make(set) }}
type LabelApplicationCtx struct { Defs map[string]*LabelDefinition // labelAt -> labelDef}
var ( LabelNoOpError = errors.New("no-op"))
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { def, ok := c.Defs[op.OperandKey] if !ok { // this def was deleted, but an op exists, so we just skip over the op return nil }
if op.Subject != "" && len(def.Scope) > 0 && !slices.Contains(def.Scope, op.Subject.Collection().String()) { return nil }
state.names[op.OperandKey] = def.Name
switch op.Operation { case LabelOperationAdd: // if valueset is empty, init it if state.inner[op.OperandKey] == nil { state.inner[op.OperandKey] = make(set) }
// if valueset is populated & this val alr exists, this labelop is a noop if valueSet, exists := state.inner[op.OperandKey]; exists { if _, exists = valueSet[op.OperandValue]; exists { return LabelNoOpError } }
if def.Multiple { // append to set state.inner[op.OperandKey][op.OperandValue] = struct{}{} } else { // reset to just this value state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} }
case LabelOperationDel: // if label DNE, then deletion is a no-op if valueSet, exists := state.inner[op.OperandKey]; !exists { return LabelNoOpError } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op return LabelNoOpError }
if def.Multiple { // remove from set delete(state.inner[op.OperandKey], op.OperandValue) } else { // reset the entire label delete(state.inner, op.OperandKey) }
// if the map becomes empty, then set it to nil, this is just the inverse of add if len(state.inner[op.OperandKey]) == 0 { state.inner[op.OperandKey] = nil }
}
return nil}
func labelOpRank(op LabelOperation) int { if op == LabelOperationDel { return 0 } return 1}
func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { // sort label ops in sort order first slices.SortFunc(ops, func(a, b LabelOp) int { return cmp.Or( a.SortAt().Compare(b.SortAt()), cmp.Compare(a.Did, b.Did), cmp.Compare(a.Rkey, b.Rkey), cmp.Compare(labelOpRank(a.Operation), labelOpRank(b.Operation)), cmp.Compare(a.OperandKey, b.OperandKey), cmp.Compare(a.OperandValue, b.OperandValue), ) })
// apply ops in sequence for _, o := range ops { _ = c.ApplyLabelOp(state, o) }}
// IsInverse checks if one label operation is the inverse of another// returns true if one is an add and the other is a delete with the same key and valuefunc (op1 LabelOp) IsInverse(op2 LabelOp) bool { if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { return false }
return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)}
// removes pairs of label operations that are inverses of each other// from the given slice. the function preserves the order of remaining operations.func ReduceLabelOps(ops []LabelOp) []LabelOp { if len(ops) <= 1 { return ops }
keep := make([]bool, len(ops)) for i := range keep { keep[i] = true }
for i := range ops { if !keep[i] { continue }
for j := i + 1; j < len(ops); j++ { if !keep[j] { continue }
if ops[i].IsInverse(ops[j]) { keep[i] = false keep[j] = false break // move to next i since this one is now eliminated } } }
// build result slice with only kept operations var result []LabelOp for i, op := range ops { if keep[i] { result = append(result, op) } }
return result}
func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) { var labelDefs []LabelDefinition ctx := context.Background()
for _, dl := range aturis { atUri, err := syntax.ParseATURI(dl) if err != nil { return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err) } if atUri.Collection() != tangled.LabelDefinitionNSID { return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri) }
owner, err := r.ResolveIdent(ctx, atUri.Authority().String()) if err != nil { return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err) }
xrpcc := xrpc.Client{ Host: owner.PDSEndpoint(), }
record, err := atproto.RepoGetRecord( ctx, &xrpcc, "", atUri.Collection().String(), atUri.Authority().String(), atUri.RecordKey().String(), ) if err != nil { return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err) }
if record != nil { bytes, err := record.Value.MarshalJSON() if err != nil { return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err) }
raw := json.RawMessage(bytes) labelRecord := tangled.LabelDefinition{} err = json.Unmarshal(raw, &labelRecord) if err != nil { return nil, fmt.Errorf("invalid record for %s: %w", atUri, err) }
labelDef, err := LabelDefinitionFromRecord( atUri.Authority().String(), atUri.RecordKey().String(), labelRecord, ) if err != nil { return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err) }
labelDefs = append(labelDefs, *labelDef) } }
return labelDefs, nil}