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}