forked from tangled.org/core
this repo has no description
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 // deletes first, then additions 236 for _, o := range record.Delete { 237 if o != nil { 238 op := mkOp(o) 239 op.Operation = LabelOperationDel 240 ops = append(ops, op) 241 } 242 } 243 for _, o := range record.Add { 244 if o != nil { 245 op := mkOp(o) 246 op.Operation = LabelOperationAdd 247 ops = append(ops, op) 248 } 249 } 250 251 return ops 252} 253 254func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { 255 if len(ops) == 0 { 256 return tangled.LabelOp{} 257 } 258 259 // use the first operation to establish common fields 260 first := ops[0] 261 record := tangled.LabelOp{ 262 Subject: string(first.Subject), 263 PerformedAt: first.PerformedAt.Format(time.RFC3339), 264 } 265 266 var addOperands []*tangled.LabelOp_Operand 267 var deleteOperands []*tangled.LabelOp_Operand 268 269 for _, op := range ops { 270 operand := &tangled.LabelOp_Operand{ 271 Key: op.OperandKey, 272 Value: op.OperandValue, 273 } 274 275 switch op.Operation { 276 case LabelOperationAdd: 277 addOperands = append(addOperands, operand) 278 case LabelOperationDel: 279 deleteOperands = append(deleteOperands, operand) 280 default: 281 return tangled.LabelOp{} 282 } 283 } 284 285 record.Add = addOperands 286 record.Delete = deleteOperands 287 288 return record 289} 290 291type set = map[string]struct{} 292 293type LabelState struct { 294 inner map[string]set 295} 296 297func NewLabelState() LabelState { 298 return LabelState{ 299 inner: make(map[string]set), 300 } 301} 302 303func (s LabelState) Inner() map[string]set { 304 return s.inner 305} 306 307func (s LabelState) ContainsLabel(l string) bool { 308 if valset, exists := s.inner[l]; exists { 309 if valset != nil { 310 return true 311 } 312 } 313 314 return false 315} 316 317// go maps behavior in templates make this necessary, 318// indexing a map and getting `set` in return is apparently truthy 319func (s LabelState) ContainsLabelAndVal(l, v string) bool { 320 if valset, exists := s.inner[l]; exists { 321 if _, exists := valset[v]; exists { 322 return true 323 } 324 } 325 326 return false 327} 328 329func (s LabelState) GetValSet(l string) set { 330 if valset, exists := s.inner[l]; exists { 331 return valset 332 } else { 333 return make(set) 334 } 335} 336 337type LabelApplicationCtx struct { 338 Defs map[string]*LabelDefinition // labelAt -> labelDef 339} 340 341var ( 342 LabelNoOpError = errors.New("no-op") 343) 344 345func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { 346 def, ok := c.Defs[op.OperandKey] 347 if !ok { 348 // this def was deleted, but an op exists, so we just skip over the op 349 return nil 350 } 351 352 switch op.Operation { 353 case LabelOperationAdd: 354 // if valueset is empty, init it 355 if state.inner[op.OperandKey] == nil { 356 state.inner[op.OperandKey] = make(set) 357 } 358 359 // if valueset is populated & this val alr exists, this labelop is a noop 360 if valueSet, exists := state.inner[op.OperandKey]; exists { 361 if _, exists = valueSet[op.OperandValue]; exists { 362 return LabelNoOpError 363 } 364 } 365 366 if def.Multiple { 367 // append to set 368 state.inner[op.OperandKey][op.OperandValue] = struct{}{} 369 } else { 370 // reset to just this value 371 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} 372 } 373 374 case LabelOperationDel: 375 // if label DNE, then deletion is a no-op 376 if valueSet, exists := state.inner[op.OperandKey]; !exists { 377 return LabelNoOpError 378 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op 379 return LabelNoOpError 380 } 381 382 if def.Multiple { 383 // remove from set 384 delete(state.inner[op.OperandKey], op.OperandValue) 385 } else { 386 // reset the entire label 387 delete(state.inner, op.OperandKey) 388 } 389 390 // if the map becomes empty, then set it to nil, this is just the inverse of add 391 if len(state.inner[op.OperandKey]) == 0 { 392 state.inner[op.OperandKey] = nil 393 } 394 395 } 396 397 return nil 398} 399 400func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 401 // sort label ops in sort order first 402 slices.SortFunc(ops, func(a, b LabelOp) int { 403 return a.SortAt().Compare(b.SortAt()) 404 }) 405 406 // apply ops in sequence 407 for _, o := range ops { 408 _ = c.ApplyLabelOp(state, o) 409 } 410} 411 412// IsInverse checks if one label operation is the inverse of another 413// returns true if one is an add and the other is a delete with the same key and value 414func (op1 LabelOp) IsInverse(op2 LabelOp) bool { 415 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { 416 return false 417 } 418 419 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || 420 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) 421} 422 423// removes pairs of label operations that are inverses of each other 424// from the given slice. the function preserves the order of remaining operations. 425func ReduceLabelOps(ops []LabelOp) []LabelOp { 426 if len(ops) <= 1 { 427 return ops 428 } 429 430 keep := make([]bool, len(ops)) 431 for i := range keep { 432 keep[i] = true 433 } 434 435 for i := range ops { 436 if !keep[i] { 437 continue 438 } 439 440 for j := i + 1; j < len(ops); j++ { 441 if !keep[j] { 442 continue 443 } 444 445 if ops[i].IsInverse(ops[j]) { 446 keep[i] = false 447 keep[j] = false 448 break // move to next i since this one is now eliminated 449 } 450 } 451 } 452 453 // build result slice with only kept operations 454 var result []LabelOp 455 for i, op := range ops { 456 if keep[i] { 457 result = append(result, op) 458 } 459 } 460 461 return result 462} 463 464func DefaultLabelDefs() []string { 465 rkeys := []string{ 466 "wontfix", 467 "duplicate", 468 "assignee", 469 "good-first-issue", 470 "documentation", 471 } 472 473 defs := make([]string, len(rkeys)) 474 for i, r := range rkeys { 475 defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r) 476 } 477 478 return defs 479} 480 481func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) { 482 resolved, err := r.ResolveIdent(context.Background(), consts.TangledDid) 483 if err != nil { 484 return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", consts.TangledDid, err) 485 } 486 pdsEndpoint := resolved.PDSEndpoint() 487 if pdsEndpoint == "" { 488 return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", consts.TangledDid) 489 } 490 client := &xrpc.Client{ 491 Host: pdsEndpoint, 492 } 493 494 var labelDefs []LabelDefinition 495 496 for _, dl := range DefaultLabelDefs() { 497 atUri := syntax.ATURI(dl) 498 parsedUri, err := syntax.ParseATURI(string(atUri)) 499 if err != nil { 500 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", atUri, err) 501 } 502 record, err := atproto.RepoGetRecord( 503 context.Background(), 504 client, 505 "", 506 parsedUri.Collection().String(), 507 parsedUri.Authority().String(), 508 parsedUri.RecordKey().String(), 509 ) 510 if err != nil { 511 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err) 512 } 513 514 if record != nil { 515 bytes, err := record.Value.MarshalJSON() 516 if err != nil { 517 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err) 518 } 519 520 raw := json.RawMessage(bytes) 521 labelRecord := tangled.LabelDefinition{} 522 err = json.Unmarshal(raw, &labelRecord) 523 if err != nil { 524 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err) 525 } 526 527 labelDef, err := LabelDefinitionFromRecord( 528 parsedUri.Authority().String(), 529 parsedUri.RecordKey().String(), 530 labelRecord, 531 ) 532 if err != nil { 533 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err) 534 } 535 536 labelDefs = append(labelDefs, *labelDef) 537 } 538 } 539 540 return labelDefs, nil 541}