forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package models 2 3import ( 4 "crypto/sha1" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "slices" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/consts" 14) 15 16type ConcreteType string 17 18const ( 19 ConcreteTypeNull ConcreteType = "null" 20 ConcreteTypeString ConcreteType = "string" 21 ConcreteTypeInt ConcreteType = "integer" 22 ConcreteTypeBool ConcreteType = "boolean" 23) 24 25type ValueTypeFormat string 26 27const ( 28 ValueTypeFormatAny ValueTypeFormat = "any" 29 ValueTypeFormatDid ValueTypeFormat = "did" 30) 31 32// ValueType represents an atproto lexicon type definition with constraints 33type ValueType struct { 34 Type ConcreteType `json:"type"` 35 Format ValueTypeFormat `json:"format,omitempty"` 36 Enum []string `json:"enum,omitempty"` 37} 38 39func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType { 40 return tangled.LabelDefinition_ValueType{ 41 Type: string(vt.Type), 42 Format: string(vt.Format), 43 Enum: vt.Enum, 44 } 45} 46 47func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType { 48 return ValueType{ 49 Type: ConcreteType(record.Type), 50 Format: ValueTypeFormat(record.Format), 51 Enum: record.Enum, 52 } 53} 54 55func (vt ValueType) IsConcreteType() bool { 56 return vt.Type == ConcreteTypeNull || 57 vt.Type == ConcreteTypeString || 58 vt.Type == ConcreteTypeInt || 59 vt.Type == ConcreteTypeBool 60} 61 62func (vt ValueType) IsNull() bool { 63 return vt.Type == ConcreteTypeNull 64} 65 66func (vt ValueType) IsString() bool { 67 return vt.Type == ConcreteTypeString 68} 69 70func (vt ValueType) IsInt() bool { 71 return vt.Type == ConcreteTypeInt 72} 73 74func (vt ValueType) IsBool() bool { 75 return vt.Type == ConcreteTypeBool 76} 77 78func (vt ValueType) IsEnum() bool { 79 return len(vt.Enum) > 0 80} 81 82func (vt ValueType) IsDidFormat() bool { 83 return vt.Format == ValueTypeFormatDid 84} 85 86func (vt ValueType) IsAnyFormat() bool { 87 return vt.Format == ValueTypeFormatAny 88} 89 90type LabelDefinition struct { 91 Id int64 92 Did string 93 Rkey string 94 95 Name string 96 ValueType ValueType 97 Scope []string 98 Color *string 99 Multiple bool 100 Created time.Time 101} 102 103func (l *LabelDefinition) AtUri() syntax.ATURI { 104 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 105} 106 107func (l *LabelDefinition) AsRecord() tangled.LabelDefinition { 108 vt := l.ValueType.AsRecord() 109 return tangled.LabelDefinition{ 110 Name: l.Name, 111 Color: l.Color, 112 CreatedAt: l.Created.Format(time.RFC3339), 113 Multiple: &l.Multiple, 114 Scope: l.Scope, 115 ValueType: &vt, 116 } 117} 118 119// random color for a given seed 120func randomColor(seed string) string { 121 hash := sha1.Sum([]byte(seed)) 122 hexStr := hex.EncodeToString(hash[:]) 123 r := hexStr[0:2] 124 g := hexStr[2:4] 125 b := hexStr[4:6] 126 127 return fmt.Sprintf("#%s%s%s", r, g, b) 128} 129 130func (ld LabelDefinition) GetColor() string { 131 if ld.Color == nil { 132 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey) 133 color := randomColor(seed) 134 return color 135 } 136 137 return *ld.Color 138} 139 140func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) { 141 created, err := time.Parse(time.RFC3339, record.CreatedAt) 142 if err != nil { 143 created = time.Now() 144 } 145 146 multiple := false 147 if record.Multiple != nil { 148 multiple = *record.Multiple 149 } 150 151 var vt ValueType 152 if record.ValueType != nil { 153 vt = ValueTypeFromRecord(*record.ValueType) 154 } 155 156 return &LabelDefinition{ 157 Did: did, 158 Rkey: rkey, 159 160 Name: record.Name, 161 ValueType: vt, 162 Scope: record.Scope, 163 Color: record.Color, 164 Multiple: multiple, 165 Created: created, 166 }, nil 167} 168 169type LabelOp struct { 170 Id int64 171 Did string 172 Rkey string 173 Subject syntax.ATURI 174 Operation LabelOperation 175 OperandKey string 176 OperandValue string 177 PerformedAt time.Time 178 IndexedAt time.Time 179} 180 181func (l LabelOp) SortAt() time.Time { 182 createdAt := l.PerformedAt 183 indexedAt := l.IndexedAt 184 185 // if we don't have an indexedat, fall back to now 186 if indexedAt.IsZero() { 187 indexedAt = time.Now() 188 } 189 190 // if createdat is invalid (before epoch), treat as null -> return zero time 191 if createdAt.Before(time.UnixMicro(0)) { 192 return time.Time{} 193 } 194 195 // if createdat is <= indexedat, use createdat 196 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) { 197 return createdAt 198 } 199 200 // otherwise, createdat is in the future relative to indexedat -> use indexedat 201 return indexedAt 202} 203 204type LabelOperation string 205 206const ( 207 LabelOperationAdd LabelOperation = "add" 208 LabelOperationDel LabelOperation = "del" 209) 210 211// a record can create multiple label ops 212func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { 213 performed, err := time.Parse(time.RFC3339, record.PerformedAt) 214 if err != nil { 215 performed = time.Now() 216 } 217 218 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { 219 return LabelOp{ 220 Did: did, 221 Rkey: rkey, 222 Subject: syntax.ATURI(record.Subject), 223 OperandKey: operand.Key, 224 OperandValue: operand.Value, 225 PerformedAt: performed, 226 } 227 } 228 229 var ops []LabelOp 230 for _, o := range record.Add { 231 if o != nil { 232 op := mkOp(o) 233 op.Operation = LabelOperationAdd 234 ops = append(ops, op) 235 } 236 } 237 for _, o := range record.Delete { 238 if o != nil { 239 op := mkOp(o) 240 op.Operation = LabelOperationDel 241 ops = append(ops, op) 242 } 243 } 244 245 return ops 246} 247 248func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { 249 if len(ops) == 0 { 250 return tangled.LabelOp{} 251 } 252 253 // use the first operation to establish common fields 254 first := ops[0] 255 record := tangled.LabelOp{ 256 Subject: string(first.Subject), 257 PerformedAt: first.PerformedAt.Format(time.RFC3339), 258 } 259 260 var addOperands []*tangled.LabelOp_Operand 261 var deleteOperands []*tangled.LabelOp_Operand 262 263 for _, op := range ops { 264 operand := &tangled.LabelOp_Operand{ 265 Key: op.OperandKey, 266 Value: op.OperandValue, 267 } 268 269 switch op.Operation { 270 case LabelOperationAdd: 271 addOperands = append(addOperands, operand) 272 case LabelOperationDel: 273 deleteOperands = append(deleteOperands, operand) 274 default: 275 return tangled.LabelOp{} 276 } 277 } 278 279 record.Add = addOperands 280 record.Delete = deleteOperands 281 282 return record 283} 284 285type set = map[string]struct{} 286 287type LabelState struct { 288 inner map[string]set 289} 290 291func NewLabelState() LabelState { 292 return LabelState{ 293 inner: make(map[string]set), 294 } 295} 296 297func (s LabelState) Inner() map[string]set { 298 return s.inner 299} 300 301func (s LabelState) ContainsLabel(l string) bool { 302 if valset, exists := s.inner[l]; exists { 303 if valset != nil { 304 return true 305 } 306 } 307 308 return false 309} 310 311// go maps behavior in templates make this necessary, 312// indexing a map and getting `set` in return is apparently truthy 313func (s LabelState) ContainsLabelAndVal(l, v string) bool { 314 if valset, exists := s.inner[l]; exists { 315 if _, exists := valset[v]; exists { 316 return true 317 } 318 } 319 320 return false 321} 322 323func (s LabelState) GetValSet(l string) set { 324 if valset, exists := s.inner[l]; exists { 325 return valset 326 } else { 327 return make(set) 328 } 329} 330 331type LabelApplicationCtx struct { 332 Defs map[string]*LabelDefinition // labelAt -> labelDef 333} 334 335var ( 336 LabelNoOpError = errors.New("no-op") 337) 338 339func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { 340 def, ok := c.Defs[op.OperandKey] 341 if !ok { 342 // this def was deleted, but an op exists, so we just skip over the op 343 return nil 344 } 345 346 switch op.Operation { 347 case LabelOperationAdd: 348 // if valueset is empty, init it 349 if state.inner[op.OperandKey] == nil { 350 state.inner[op.OperandKey] = make(set) 351 } 352 353 // if valueset is populated & this val alr exists, this labelop is a noop 354 if valueSet, exists := state.inner[op.OperandKey]; exists { 355 if _, exists = valueSet[op.OperandValue]; exists { 356 return LabelNoOpError 357 } 358 } 359 360 if def.Multiple { 361 // append to set 362 state.inner[op.OperandKey][op.OperandValue] = struct{}{} 363 } else { 364 // reset to just this value 365 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} 366 } 367 368 case LabelOperationDel: 369 // if label DNE, then deletion is a no-op 370 if valueSet, exists := state.inner[op.OperandKey]; !exists { 371 return LabelNoOpError 372 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op 373 return LabelNoOpError 374 } 375 376 if def.Multiple { 377 // remove from set 378 delete(state.inner[op.OperandKey], op.OperandValue) 379 } else { 380 // reset the entire label 381 delete(state.inner, op.OperandKey) 382 } 383 384 // if the map becomes empty, then set it to nil, this is just the inverse of add 385 if len(state.inner[op.OperandKey]) == 0 { 386 state.inner[op.OperandKey] = nil 387 } 388 389 } 390 391 return nil 392} 393 394func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 395 // sort label ops in sort order first 396 slices.SortFunc(ops, func(a, b LabelOp) int { 397 return a.SortAt().Compare(b.SortAt()) 398 }) 399 400 // apply ops in sequence 401 for _, o := range ops { 402 _ = c.ApplyLabelOp(state, o) 403 } 404} 405 406// IsInverse checks if one label operation is the inverse of another 407// returns true if one is an add and the other is a delete with the same key and value 408func (op1 LabelOp) IsInverse(op2 LabelOp) bool { 409 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { 410 return false 411 } 412 413 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || 414 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) 415} 416 417// removes pairs of label operations that are inverses of each other 418// from the given slice. the function preserves the order of remaining operations. 419func ReduceLabelOps(ops []LabelOp) []LabelOp { 420 if len(ops) <= 1 { 421 return ops 422 } 423 424 keep := make([]bool, len(ops)) 425 for i := range keep { 426 keep[i] = true 427 } 428 429 for i := range ops { 430 if !keep[i] { 431 continue 432 } 433 434 for j := i + 1; j < len(ops); j++ { 435 if !keep[j] { 436 continue 437 } 438 439 if ops[i].IsInverse(ops[j]) { 440 keep[i] = false 441 keep[j] = false 442 break // move to next i since this one is now eliminated 443 } 444 } 445 } 446 447 // build result slice with only kept operations 448 var result []LabelOp 449 for i, op := range ops { 450 if keep[i] { 451 result = append(result, op) 452 } 453 } 454 455 return result 456} 457 458func DefaultLabelDefs() []string { 459 rkeys := []string{ 460 "wontfix", 461 "duplicate", 462 "assignee", 463 "good-first-issue", 464 "documentation", 465 } 466 467 defs := make([]string, len(rkeys)) 468 for i, r := range rkeys { 469 defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r) 470 } 471 472 return defs 473}