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