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}