Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/models: move db.Label* into models

- db.{LabelOp,LabelDefinition,LabelState,LabelApplicationCtx} have been
moved
- auxilliary helpers used to calculate label state have been moved

Signed-off-by: oppiliappan <me@oppi.li>

+550 -537
+2 -1
appview/db/issues.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/api/tangled" 14 + "tangled.org/core/appview/models" 14 15 "tangled.org/core/appview/pagination" 15 16 ) 16 17 ··· 31 30 // optionally, populate this when querying for reverse mappings 32 31 // like comment counts, parent repo etc. 33 32 Comments []IssueComment 34 - Labels LabelState 33 + Labels models.LabelState 35 34 Repo *Repo 36 35 } 37 36
+33 -496
appview/db/label.go
··· 1 1 package db 2 2 3 3 import ( 4 - "crypto/sha1" 5 4 "database/sql" 6 - "encoding/hex" 7 - "errors" 8 5 "fmt" 9 6 "maps" 10 7 "slices" ··· 9 12 "time" 10 13 11 14 "github.com/bluesky-social/indigo/atproto/syntax" 12 - "tangled.sh/tangled.sh/core/api/tangled" 13 - "tangled.sh/tangled.sh/core/consts" 15 + "tangled.org/core/appview/models" 14 16 ) 15 - 16 - type ConcreteType string 17 - 18 - const ( 19 - ConcreteTypeNull ConcreteType = "null" 20 - ConcreteTypeString ConcreteType = "string" 21 - ConcreteTypeInt ConcreteType = "integer" 22 - ConcreteTypeBool ConcreteType = "boolean" 23 - ) 24 - 25 - type ValueTypeFormat string 26 - 27 - const ( 28 - ValueTypeFormatAny ValueTypeFormat = "any" 29 - ValueTypeFormatDid ValueTypeFormat = "did" 30 - ) 31 - 32 - // ValueType represents an atproto lexicon type definition with constraints 33 - type ValueType struct { 34 - Type ConcreteType `json:"type"` 35 - Format ValueTypeFormat `json:"format,omitempty"` 36 - Enum []string `json:"enum,omitempty"` 37 - } 38 - 39 - func (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 - 47 - func 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 - 55 - func (vt ValueType) IsConcreteType() bool { 56 - return vt.Type == ConcreteTypeNull || 57 - vt.Type == ConcreteTypeString || 58 - vt.Type == ConcreteTypeInt || 59 - vt.Type == ConcreteTypeBool 60 - } 61 - 62 - func (vt ValueType) IsNull() bool { 63 - return vt.Type == ConcreteTypeNull 64 - } 65 - 66 - func (vt ValueType) IsString() bool { 67 - return vt.Type == ConcreteTypeString 68 - } 69 - 70 - func (vt ValueType) IsInt() bool { 71 - return vt.Type == ConcreteTypeInt 72 - } 73 - 74 - func (vt ValueType) IsBool() bool { 75 - return vt.Type == ConcreteTypeBool 76 - } 77 - 78 - func (vt ValueType) IsEnum() bool { 79 - return len(vt.Enum) > 0 80 - } 81 - 82 - func (vt ValueType) IsDidFormat() bool { 83 - return vt.Format == ValueTypeFormatDid 84 - } 85 - 86 - func (vt ValueType) IsAnyFormat() bool { 87 - return vt.Format == ValueTypeFormatAny 88 - } 89 - 90 - type 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 - 103 - func (l *LabelDefinition) AtUri() syntax.ATURI { 104 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 105 - } 106 - 107 - func (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 120 - func 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 - 130 - func (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 - 140 - func 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 - 169 - func DeleteLabelDefinition(e Execer, filters ...filter) error { 170 - var conditions []string 171 - var args []any 172 - for _, filter := range filters { 173 - conditions = append(conditions, filter.Condition()) 174 - args = append(args, filter.Arg()...) 175 - } 176 - whereClause := "" 177 - if conditions != nil { 178 - whereClause = " where " + strings.Join(conditions, " and ") 179 - } 180 - query := fmt.Sprintf(`delete from label_definitions %s`, whereClause) 181 - _, err := e.Exec(query, args...) 182 - return err 183 - } 184 17 185 18 // no updating type for now 186 - func AddLabelDefinition(e Execer, l *LabelDefinition) (int64, error) { 19 + func AddLabelDefinition(e Execer, l *models.LabelDefinition) (int64, error) { 187 20 result, err := e.Exec( 188 21 `insert into label_definitions ( 189 22 did, ··· 59 232 return id, nil 60 233 } 61 234 62 - func GetLabelDefinitions(e Execer, filters ...filter) ([]LabelDefinition, error) { 63 - var labelDefinitions []LabelDefinition 235 + func DeleteLabelDefinition(e Execer, filters ...filter) error { 236 + var conditions []string 237 + var args []any 238 + for _, filter := range filters { 239 + conditions = append(conditions, filter.Condition()) 240 + args = append(args, filter.Arg()...) 241 + } 242 + whereClause := "" 243 + if conditions != nil { 244 + whereClause = " where " + strings.Join(conditions, " and ") 245 + } 246 + query := fmt.Sprintf(`delete from label_definitions %s`, whereClause) 247 + _, err := e.Exec(query, args...) 248 + return err 249 + } 250 + 251 + func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) { 252 + var labelDefinitions []models.LabelDefinition 64 253 var conditions []string 65 254 var args []any 66 255 ··· 118 275 defer rows.Close() 119 276 120 277 for rows.Next() { 121 - var labelDefinition LabelDefinition 278 + var labelDefinition models.LabelDefinition 122 279 var createdAt, enumVariants, scopes string 123 280 var color sql.Null[string] 124 281 var multiple int ··· 167 324 } 168 325 169 326 // helper to get exactly one label def 170 - func GetLabelDefinition(e Execer, filters ...filter) (*LabelDefinition, error) { 327 + func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) { 171 328 labels, err := GetLabelDefinitions(e, filters...) 172 329 if err != nil { 173 330 return nil, err ··· 184 341 return &labels[0], nil 185 342 } 186 343 187 - type LabelOp struct { 188 - Id int64 189 - Did string 190 - Rkey string 191 - Subject syntax.ATURI 192 - Operation LabelOperation 193 - OperandKey string 194 - OperandValue string 195 - PerformedAt time.Time 196 - IndexedAt time.Time 197 - } 198 - 199 - func (l LabelOp) SortAt() time.Time { 200 - createdAt := l.PerformedAt 201 - indexedAt := l.IndexedAt 202 - 203 - // if we don't have an indexedat, fall back to now 204 - if indexedAt.IsZero() { 205 - indexedAt = time.Now() 206 - } 207 - 208 - // if createdat is invalid (before epoch), treat as null -> return zero time 209 - if createdAt.Before(time.UnixMicro(0)) { 210 - return time.Time{} 211 - } 212 - 213 - // if createdat is <= indexedat, use createdat 214 - if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) { 215 - return createdAt 216 - } 217 - 218 - // otherwise, createdat is in the future relative to indexedat -> use indexedat 219 - return indexedAt 220 - } 221 - 222 - type LabelOperation string 223 - 224 - const ( 225 - LabelOperationAdd LabelOperation = "add" 226 - LabelOperationDel LabelOperation = "del" 227 - ) 228 - 229 - // a record can create multiple label ops 230 - func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp { 231 - performed, err := time.Parse(time.RFC3339, record.PerformedAt) 232 - if err != nil { 233 - performed = time.Now() 234 - } 235 - 236 - mkOp := func(operand *tangled.LabelOp_Operand) LabelOp { 237 - return LabelOp{ 238 - Did: did, 239 - Rkey: rkey, 240 - Subject: syntax.ATURI(record.Subject), 241 - OperandKey: operand.Key, 242 - OperandValue: operand.Value, 243 - PerformedAt: performed, 244 - } 245 - } 246 - 247 - var ops []LabelOp 248 - for _, o := range record.Add { 249 - if o != nil { 250 - op := mkOp(o) 251 - op.Operation = LabelOperationAdd 252 - ops = append(ops, op) 253 - } 254 - } 255 - for _, o := range record.Delete { 256 - if o != nil { 257 - op := mkOp(o) 258 - op.Operation = LabelOperationDel 259 - ops = append(ops, op) 260 - } 261 - } 262 - 263 - return ops 264 - } 265 - 266 - func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp { 267 - if len(ops) == 0 { 268 - return tangled.LabelOp{} 269 - } 270 - 271 - // use the first operation to establish common fields 272 - first := ops[0] 273 - record := tangled.LabelOp{ 274 - Subject: string(first.Subject), 275 - PerformedAt: first.PerformedAt.Format(time.RFC3339), 276 - } 277 - 278 - var addOperands []*tangled.LabelOp_Operand 279 - var deleteOperands []*tangled.LabelOp_Operand 280 - 281 - for _, op := range ops { 282 - operand := &tangled.LabelOp_Operand{ 283 - Key: op.OperandKey, 284 - Value: op.OperandValue, 285 - } 286 - 287 - switch op.Operation { 288 - case LabelOperationAdd: 289 - addOperands = append(addOperands, operand) 290 - case LabelOperationDel: 291 - deleteOperands = append(deleteOperands, operand) 292 - default: 293 - return tangled.LabelOp{} 294 - } 295 - } 296 - 297 - record.Add = addOperands 298 - record.Delete = deleteOperands 299 - 300 - return record 301 - } 302 - 303 - func AddLabelOp(e Execer, l *LabelOp) (int64, error) { 344 + func AddLabelOp(e Execer, l *models.LabelOp) (int64, error) { 304 345 now := time.Now() 305 346 result, err := e.Exec( 306 347 `insert into label_ops ( ··· 227 500 return id, nil 228 501 } 229 502 230 - func GetLabelOps(e Execer, filters ...filter) ([]LabelOp, error) { 231 - var labelOps []LabelOp 503 + func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) { 504 + var labelOps []models.LabelOp 232 505 var conditions []string 233 506 var args []any 234 507 ··· 268 541 defer rows.Close() 269 542 270 543 for rows.Next() { 271 - var labelOp LabelOp 544 + var labelOp models.LabelOp 272 545 var performedAt, indexedAt string 273 546 274 547 if err := rows.Scan( ··· 302 575 } 303 576 304 577 // get labels for a given list of subject URIs 305 - func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]LabelState, error) { 578 + func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) { 306 579 ops, err := GetLabelOps(e, filters...) 307 580 if err != nil { 308 581 return nil, err 309 582 } 310 583 311 584 // group ops by subject 312 - opsBySubject := make(map[syntax.ATURI][]LabelOp) 585 + opsBySubject := make(map[syntax.ATURI][]models.LabelOp) 313 586 for _, op := range ops { 314 587 subject := syntax.ATURI(op.Subject) 315 588 opsBySubject[subject] = append(opsBySubject[subject], op) ··· 328 601 } 329 602 330 603 // apply label ops for each subject and collect results 331 - results := make(map[syntax.ATURI]LabelState) 604 + results := make(map[syntax.ATURI]models.LabelState) 332 605 for subject, subjectOps := range opsBySubject { 333 - state := NewLabelState() 606 + state := models.NewLabelState() 334 607 actx.ApplyLabelOps(state, subjectOps) 335 608 results[subject] = state 336 609 } ··· 338 611 return results, nil 339 612 } 340 613 341 - type set = map[string]struct{} 342 - 343 - type LabelState struct { 344 - inner map[string]set 345 - } 346 - 347 - func NewLabelState() LabelState { 348 - return LabelState{ 349 - inner: make(map[string]set), 350 - } 351 - } 352 - 353 - func (s LabelState) Inner() map[string]set { 354 - return s.inner 355 - } 356 - 357 - func (s LabelState) ContainsLabel(l string) bool { 358 - if valset, exists := s.inner[l]; exists { 359 - if valset != nil { 360 - return true 361 - } 362 - } 363 - 364 - return false 365 - } 366 - 367 - // go maps behavior in templates make this necessary, 368 - // indexing a map and getting `set` in return is apparently truthy 369 - func (s LabelState) ContainsLabelAndVal(l, v string) bool { 370 - if valset, exists := s.inner[l]; exists { 371 - if _, exists := valset[v]; exists { 372 - return true 373 - } 374 - } 375 - 376 - return false 377 - } 378 - 379 - func (s LabelState) GetValSet(l string) set { 380 - if valset, exists := s.inner[l]; exists { 381 - return valset 382 - } else { 383 - return make(set) 384 - } 385 - } 386 - 387 - type LabelApplicationCtx struct { 388 - Defs map[string]*LabelDefinition // labelAt -> labelDef 389 - } 390 - 391 - var ( 392 - LabelNoOpError = errors.New("no-op") 393 - ) 394 - 395 - func NewLabelApplicationCtx(e Execer, filters ...filter) (*LabelApplicationCtx, error) { 614 + func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) { 396 615 labels, err := GetLabelDefinitions(e, filters...) 397 616 if err != nil { 398 617 return nil, err 399 618 } 400 619 401 - defs := make(map[string]*LabelDefinition) 620 + defs := make(map[string]*models.LabelDefinition) 402 621 for _, l := range labels { 403 622 defs[l.AtUri().String()] = &l 404 623 } 405 624 406 - return &LabelApplicationCtx{defs}, nil 407 - } 408 - 409 - func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error { 410 - def, ok := c.Defs[op.OperandKey] 411 - if !ok { 412 - // this def was deleted, but an op exists, so we just skip over the op 413 - return nil 414 - } 415 - 416 - switch op.Operation { 417 - case LabelOperationAdd: 418 - // if valueset is empty, init it 419 - if state.inner[op.OperandKey] == nil { 420 - state.inner[op.OperandKey] = make(set) 421 - } 422 - 423 - // if valueset is populated & this val alr exists, this labelop is a noop 424 - if valueSet, exists := state.inner[op.OperandKey]; exists { 425 - if _, exists = valueSet[op.OperandValue]; exists { 426 - return LabelNoOpError 427 - } 428 - } 429 - 430 - if def.Multiple { 431 - // append to set 432 - state.inner[op.OperandKey][op.OperandValue] = struct{}{} 433 - } else { 434 - // reset to just this value 435 - state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}} 436 - } 437 - 438 - case LabelOperationDel: 439 - // if label DNE, then deletion is a no-op 440 - if valueSet, exists := state.inner[op.OperandKey]; !exists { 441 - return LabelNoOpError 442 - } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op 443 - return LabelNoOpError 444 - } 445 - 446 - if def.Multiple { 447 - // remove from set 448 - delete(state.inner[op.OperandKey], op.OperandValue) 449 - } else { 450 - // reset the entire label 451 - delete(state.inner, op.OperandKey) 452 - } 453 - 454 - // if the map becomes empty, then set it to nil, this is just the inverse of add 455 - if len(state.inner[op.OperandKey]) == 0 { 456 - state.inner[op.OperandKey] = nil 457 - } 458 - 459 - } 460 - 461 - return nil 462 - } 463 - 464 - func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 465 - // sort label ops in sort order first 466 - slices.SortFunc(ops, func(a, b LabelOp) int { 467 - return a.SortAt().Compare(b.SortAt()) 468 - }) 469 - 470 - // apply ops in sequence 471 - for _, o := range ops { 472 - _ = c.ApplyLabelOp(state, o) 473 - } 474 - } 475 - 476 - // IsInverse checks if one label operation is the inverse of another 477 - // returns true if one is an add and the other is a delete with the same key and value 478 - func (op1 LabelOp) IsInverse(op2 LabelOp) bool { 479 - if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue { 480 - return false 481 - } 482 - 483 - return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) || 484 - (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd) 485 - } 486 - 487 - // removes pairs of label operations that are inverses of each other 488 - // from the given slice. the function preserves the order of remaining operations. 489 - func ReduceLabelOps(ops []LabelOp) []LabelOp { 490 - if len(ops) <= 1 { 491 - return ops 492 - } 493 - 494 - keep := make([]bool, len(ops)) 495 - for i := range keep { 496 - keep[i] = true 497 - } 498 - 499 - for i := range ops { 500 - if !keep[i] { 501 - continue 502 - } 503 - 504 - for j := i + 1; j < len(ops); j++ { 505 - if !keep[j] { 506 - continue 507 - } 508 - 509 - if ops[i].IsInverse(ops[j]) { 510 - keep[i] = false 511 - keep[j] = false 512 - break // move to next i since this one is now eliminated 513 - } 514 - } 515 - } 516 - 517 - // build result slice with only kept operations 518 - var result []LabelOp 519 - for i, op := range ops { 520 - if keep[i] { 521 - result = append(result, op) 522 - } 523 - } 524 - 525 - return result 526 - } 527 - 528 - func DefaultLabelDefs() []string { 529 - rkeys := []string{ 530 - "wontfix", 531 - "duplicate", 532 - "assignee", 533 - "good-first-issue", 534 - "documentation", 535 - } 536 - 537 - defs := make([]string, len(rkeys)) 538 - for i, r := range rkeys { 539 - defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r) 540 - } 541 - 542 - return defs 625 + return &models.LabelApplicationCtx{defs}, nil 543 626 }
+1 -1
appview/ingester.go
··· 923 923 return fmt.Errorf("invalid record: %w", err) 924 924 } 925 925 926 - def, err := db.LabelDefinitionFromRecord(did, rkey, record) 926 + def, err := models.LabelDefinitionFromRecord(did, rkey, record) 927 927 if err != nil { 928 928 return fmt.Errorf("failed to parse labeldef from record: %w", err) 929 929 }
+3 -2
appview/issues/issues.go
··· 19 19 "tangled.org/core/api/tangled" 20 20 "tangled.org/core/appview/config" 21 21 "tangled.org/core/appview/db" 22 + "tangled.org/core/appview/models" 22 23 "tangled.org/core/appview/notify" 23 24 "tangled.org/core/appview/oauth" 24 25 "tangled.org/core/appview/pages" ··· 104 103 return 105 104 } 106 105 107 - defs := make(map[string]*db.LabelDefinition) 106 + defs := make(map[string]*models.LabelDefinition) 108 107 for _, l := range labelDefs { 109 108 defs[l.AtUri().String()] = &l 110 109 } ··· 797 796 return 798 797 } 799 798 800 - defs := make(map[string]*db.LabelDefinition) 799 + defs := make(map[string]*models.LabelDefinition) 801 800 for _, l := range labelDefs { 802 801 defs[l.AtUri().String()] = &l 803 802 }
+10 -9
appview/labels/labels.go
··· 17 17 "tangled.sh/tangled.sh/core/api/tangled" 18 18 "tangled.sh/tangled.sh/core/appview/db" 19 19 "tangled.sh/tangled.sh/core/appview/middleware" 20 + "tangled.sh/tangled.sh/core/appview/models" 20 21 "tangled.sh/tangled.sh/core/appview/oauth" 21 22 "tangled.sh/tangled.sh/core/appview/pages" 22 23 "tangled.sh/tangled.sh/core/appview/validator" ··· 114 113 return 115 114 } 116 115 117 - labelState := db.NewLabelState() 116 + labelState := models.NewLabelState() 118 117 actx.ApplyLabelOps(labelState, existingOps) 119 118 120 - var labelOps []db.LabelOp 119 + var labelOps []models.LabelOp 121 120 122 121 // first delete all existing state 123 122 for key, vals := range labelState.Inner() { 124 123 for val := range vals { 125 - labelOps = append(labelOps, db.LabelOp{ 124 + labelOps = append(labelOps, models.LabelOp{ 126 125 Did: did, 127 126 Rkey: rkey, 128 127 Subject: syntax.ATURI(subjectUri), 129 - Operation: db.LabelOperationDel, 128 + Operation: models.LabelOperationDel, 130 129 OperandKey: key, 131 130 OperandValue: val, 132 131 PerformedAt: performedAt, ··· 142 141 } 143 142 144 143 for _, val := range vals { 145 - labelOps = append(labelOps, db.LabelOp{ 144 + labelOps = append(labelOps, models.LabelOp{ 146 145 Did: did, 147 146 Rkey: rkey, 148 147 Subject: syntax.ATURI(subjectUri), 149 - Operation: db.LabelOperationAdd, 148 + Operation: models.LabelOperationAdd, 150 149 OperandKey: key, 151 150 OperandValue: val, 152 151 PerformedAt: performedAt, ··· 156 155 } 157 156 158 157 // reduce the opset 159 - labelOps = db.ReduceLabelOps(labelOps) 158 + labelOps = models.ReduceLabelOps(labelOps) 160 159 161 160 for i := range labelOps { 162 161 def := actx.Defs[labelOps[i].OperandKey] ··· 169 168 // next, apply all ops introduced in this request and filter out ones that are no-ops 170 169 validLabelOps := labelOps[:0] 171 170 for _, op := range labelOps { 172 - if err = actx.ApplyLabelOp(labelState, op); err != db.LabelNoOpError { 171 + if err = actx.ApplyLabelOp(labelState, op); err != models.LabelNoOpError { 173 172 validLabelOps = append(validLabelOps, op) 174 173 } 175 174 } ··· 181 180 } 182 181 183 182 // create an atproto record of valid ops 184 - record := db.LabelOpsAsRecord(validLabelOps) 183 + record := models.LabelOpsAsRecord(validLabelOps) 185 184 186 185 client, err := l.oauth.AuthorizedClient(r) 187 186 if err != nil {
+473
appview/models/label.go
··· 1 + package models 2 + 3 + import ( 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 + 16 + type ConcreteType string 17 + 18 + const ( 19 + ConcreteTypeNull ConcreteType = "null" 20 + ConcreteTypeString ConcreteType = "string" 21 + ConcreteTypeInt ConcreteType = "integer" 22 + ConcreteTypeBool ConcreteType = "boolean" 23 + ) 24 + 25 + type ValueTypeFormat string 26 + 27 + const ( 28 + ValueTypeFormatAny ValueTypeFormat = "any" 29 + ValueTypeFormatDid ValueTypeFormat = "did" 30 + ) 31 + 32 + // ValueType represents an atproto lexicon type definition with constraints 33 + type ValueType struct { 34 + Type ConcreteType `json:"type"` 35 + Format ValueTypeFormat `json:"format,omitempty"` 36 + Enum []string `json:"enum,omitempty"` 37 + } 38 + 39 + func (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 + 47 + func 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 + 55 + func (vt ValueType) IsConcreteType() bool { 56 + return vt.Type == ConcreteTypeNull || 57 + vt.Type == ConcreteTypeString || 58 + vt.Type == ConcreteTypeInt || 59 + vt.Type == ConcreteTypeBool 60 + } 61 + 62 + func (vt ValueType) IsNull() bool { 63 + return vt.Type == ConcreteTypeNull 64 + } 65 + 66 + func (vt ValueType) IsString() bool { 67 + return vt.Type == ConcreteTypeString 68 + } 69 + 70 + func (vt ValueType) IsInt() bool { 71 + return vt.Type == ConcreteTypeInt 72 + } 73 + 74 + func (vt ValueType) IsBool() bool { 75 + return vt.Type == ConcreteTypeBool 76 + } 77 + 78 + func (vt ValueType) IsEnum() bool { 79 + return len(vt.Enum) > 0 80 + } 81 + 82 + func (vt ValueType) IsDidFormat() bool { 83 + return vt.Format == ValueTypeFormatDid 84 + } 85 + 86 + func (vt ValueType) IsAnyFormat() bool { 87 + return vt.Format == ValueTypeFormatAny 88 + } 89 + 90 + type 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 + 103 + func (l *LabelDefinition) AtUri() syntax.ATURI { 104 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey)) 105 + } 106 + 107 + func (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 120 + func 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 + 130 + func (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 + 140 + func 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 + 169 + type 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 + 181 + func (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 + 204 + type LabelOperation string 205 + 206 + const ( 207 + LabelOperationAdd LabelOperation = "add" 208 + LabelOperationDel LabelOperation = "del" 209 + ) 210 + 211 + // a record can create multiple label ops 212 + func 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 + 248 + func 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 + 285 + type set = map[string]struct{} 286 + 287 + type LabelState struct { 288 + inner map[string]set 289 + } 290 + 291 + func NewLabelState() LabelState { 292 + return LabelState{ 293 + inner: make(map[string]set), 294 + } 295 + } 296 + 297 + func (s LabelState) Inner() map[string]set { 298 + return s.inner 299 + } 300 + 301 + func (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 313 + func (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 + 323 + func (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 + 331 + type LabelApplicationCtx struct { 332 + Defs map[string]*LabelDefinition // labelAt -> labelDef 333 + } 334 + 335 + var ( 336 + LabelNoOpError = errors.New("no-op") 337 + ) 338 + 339 + func (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 + 394 + func (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 408 + func (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. 419 + func 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 + 458 + func 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 + }
+8 -8
appview/pages/pages.go
··· 841 841 type RepoGeneralSettingsParams struct { 842 842 LoggedInUser *oauth.User 843 843 RepoInfo repoinfo.RepoInfo 844 - Labels []db.LabelDefinition 845 - DefaultLabels []db.LabelDefinition 844 + Labels []models.LabelDefinition 845 + DefaultLabels []models.LabelDefinition 846 846 SubscribedLabels map[string]struct{} 847 847 Active string 848 848 Tabs []map[string]any ··· 890 890 RepoInfo repoinfo.RepoInfo 891 891 Active string 892 892 Issues []db.Issue 893 - LabelDefs map[string]*db.LabelDefinition 893 + LabelDefs map[string]*models.LabelDefinition 894 894 Page pagination.Page 895 895 FilteringByOpen bool 896 896 } ··· 906 906 Active string 907 907 Issue *db.Issue 908 908 CommentList []db.CommentListItem 909 - LabelDefs map[string]*db.LabelDefinition 909 + LabelDefs map[string]*models.LabelDefinition 910 910 911 911 OrderedReactionKinds []db.ReactionKind 912 912 Reactions map[db.ReactionKind]int ··· 1236 1236 type LabelPanelParams struct { 1237 1237 LoggedInUser *oauth.User 1238 1238 RepoInfo repoinfo.RepoInfo 1239 - Defs map[string]*db.LabelDefinition 1239 + Defs map[string]*models.LabelDefinition 1240 1240 Subject string 1241 - State db.LabelState 1241 + State models.LabelState 1242 1242 } 1243 1243 1244 1244 func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { ··· 1248 1248 type EditLabelPanelParams struct { 1249 1249 LoggedInUser *oauth.User 1250 1250 RepoInfo repoinfo.RepoInfo 1251 - Defs map[string]*db.LabelDefinition 1251 + Defs map[string]*models.LabelDefinition 1252 1252 Subject string 1253 - State db.LabelState 1253 + State models.LabelState 1254 1254 } 1255 1255 1256 1256 func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
+8 -8
appview/repo/repo.go
··· 1005 1005 concreteType = "null" 1006 1006 } 1007 1007 1008 - format := db.ValueTypeFormatAny 1008 + format := models.ValueTypeFormatAny 1009 1009 if valueFormat == "did" { 1010 - format = db.ValueTypeFormatDid 1010 + format = models.ValueTypeFormatDid 1011 1011 } 1012 1012 1013 - valueType := db.ValueType{ 1014 - Type: db.ConcreteType(concreteType), 1013 + valueType := models.ValueType{ 1014 + Type: models.ConcreteType(concreteType), 1015 1015 Format: format, 1016 1016 Enum: variants, 1017 1017 } 1018 1018 1019 - label := db.LabelDefinition{ 1019 + label := models.LabelDefinition{ 1020 1020 Did: user.Did, 1021 1021 Rkey: tid.TID(), 1022 1022 Name: name, ··· 1396 1396 return 1397 1397 } 1398 1398 1399 - defs := make(map[string]*db.LabelDefinition) 1399 + defs := make(map[string]*models.LabelDefinition) 1400 1400 for _, l := range labelDefs { 1401 1401 defs[l.AtUri().String()] = &l 1402 1402 } ··· 1444 1444 return 1445 1445 } 1446 1446 1447 - defs := make(map[string]*db.LabelDefinition) 1447 + defs := make(map[string]*models.LabelDefinition) 1448 1448 for _, l := range labelDefs { 1449 1449 defs[l.AtUri().String()] = &l 1450 1450 } ··· 1895 1895 return 1896 1896 } 1897 1897 1898 - defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", db.DefaultLabelDefs())) 1898 + defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs())) 1899 1899 if err != nil { 1900 1900 log.Println("failed to fetch labels", err) 1901 1901 rp.pages.Error503(w)
+12 -12
appview/validator/label.go
··· 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "golang.org/x/exp/slices" 11 11 "tangled.sh/tangled.sh/core/api/tangled" 12 - "tangled.sh/tangled.sh/core/appview/db" 12 + "tangled.sh/tangled.sh/core/appview/models" 13 13 ) 14 14 15 15 var ( ··· 21 21 validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID} 22 22 ) 23 23 24 - func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error { 24 + func (v *Validator) ValidateLabelDefinition(label *models.LabelDefinition) error { 25 25 if label.Name == "" { 26 26 return fmt.Errorf("label name is empty") 27 27 } ··· 95 95 return nil 96 96 } 97 97 98 - func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error { 98 + func (v *Validator) ValidateLabelOp(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error { 99 99 if labelDef == nil { 100 100 return fmt.Errorf("label definition is required") 101 101 } ··· 108 108 return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey) 109 109 } 110 110 111 - if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel { 111 + if labelOp.Operation != models.LabelOperationAdd && labelOp.Operation != models.LabelOperationDel { 112 112 return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation) 113 113 } 114 114 ··· 131 131 return nil 132 132 } 133 133 134 - func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error { 134 + func (v *Validator) validateOperandValue(labelDef *models.LabelDefinition, labelOp *models.LabelOp) error { 135 135 valueType := labelDef.ValueType 136 136 137 137 // this is permitted, it "unsets" a label 138 138 if labelOp.OperandValue == "" { 139 - labelOp.Operation = db.LabelOperationDel 139 + labelOp.Operation = models.LabelOperationDel 140 140 return nil 141 141 } 142 142 143 143 switch valueType.Type { 144 - case db.ConcreteTypeNull: 144 + case models.ConcreteTypeNull: 145 145 // For null type, value should be empty 146 146 if labelOp.OperandValue != "null" { 147 147 return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue) 148 148 } 149 149 150 - case db.ConcreteTypeString: 150 + case models.ConcreteTypeString: 151 151 // For string type, validate enum constraints if present 152 152 if valueType.IsEnum() { 153 153 if !slices.Contains(valueType.Enum, labelOp.OperandValue) { ··· 156 156 } 157 157 158 158 switch valueType.Format { 159 - case db.ValueTypeFormatDid: 159 + case models.ValueTypeFormatDid: 160 160 id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue) 161 161 if err != nil { 162 162 return fmt.Errorf("failed to resolve did/handle: %w", err) ··· 164 164 165 165 labelOp.OperandValue = id.DID.String() 166 166 167 - case db.ValueTypeFormatAny, "": 167 + case models.ValueTypeFormatAny, "": 168 168 default: 169 169 return fmt.Errorf("unsupported format constraint: %q", valueType.Format) 170 170 } 171 171 172 - case db.ConcreteTypeInt: 172 + case models.ConcreteTypeInt: 173 173 if labelOp.OperandValue == "" { 174 174 return fmt.Errorf("integer type requires non-empty value") 175 175 } ··· 183 183 } 184 184 } 185 185 186 - case db.ConcreteTypeBool: 186 + case models.ConcreteTypeBool: 187 187 if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" { 188 188 return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue) 189 189 }