forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
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/idresolver"
18)
19
20type ConcreteType string
21
22const (
23 ConcreteTypeNull ConcreteType = "null"
24 ConcreteTypeString ConcreteType = "string"
25 ConcreteTypeInt ConcreteType = "integer"
26 ConcreteTypeBool ConcreteType = "boolean"
27)
28
29type ValueTypeFormat string
30
31const (
32 ValueTypeFormatAny ValueTypeFormat = "any"
33 ValueTypeFormatDid ValueTypeFormat = "did"
34)
35
36// ValueType represents an atproto lexicon type definition with constraints
37type ValueType struct {
38 Type ConcreteType `json:"type"`
39 Format ValueTypeFormat `json:"format,omitempty"`
40 Enum []string `json:"enum,omitempty"`
41}
42
43func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
44 return tangled.LabelDefinition_ValueType{
45 Type: string(vt.Type),
46 Format: string(vt.Format),
47 Enum: vt.Enum,
48 }
49}
50
51func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
52 return ValueType{
53 Type: ConcreteType(record.Type),
54 Format: ValueTypeFormat(record.Format),
55 Enum: record.Enum,
56 }
57}
58
59func (vt ValueType) IsConcreteType() bool {
60 return vt.Type == ConcreteTypeNull ||
61 vt.Type == ConcreteTypeString ||
62 vt.Type == ConcreteTypeInt ||
63 vt.Type == ConcreteTypeBool
64}
65
66func (vt ValueType) IsNull() bool {
67 return vt.Type == ConcreteTypeNull
68}
69
70func (vt ValueType) IsString() bool {
71 return vt.Type == ConcreteTypeString
72}
73
74func (vt ValueType) IsInt() bool {
75 return vt.Type == ConcreteTypeInt
76}
77
78func (vt ValueType) IsBool() bool {
79 return vt.Type == ConcreteTypeBool
80}
81
82func (vt ValueType) IsEnum() bool {
83 return len(vt.Enum) > 0
84}
85
86func (vt ValueType) IsDidFormat() bool {
87 return vt.Format == ValueTypeFormatDid
88}
89
90func (vt ValueType) IsAnyFormat() bool {
91 return vt.Format == ValueTypeFormatAny
92}
93
94type LabelDefinition struct {
95 Id int64
96 Did string
97 Rkey string
98
99 Name string
100 ValueType ValueType
101 Scope []string
102 Color *string
103 Multiple bool
104 Created time.Time
105}
106
107func (l *LabelDefinition) AtUri() syntax.ATURI {
108 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
109}
110
111func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
112 vt := l.ValueType.AsRecord()
113 return tangled.LabelDefinition{
114 Name: l.Name,
115 Color: l.Color,
116 CreatedAt: l.Created.Format(time.RFC3339),
117 Multiple: &l.Multiple,
118 Scope: l.Scope,
119 ValueType: &vt,
120 }
121}
122
123// random color for a given seed
124func randomColor(seed string) string {
125 hash := sha1.Sum([]byte(seed))
126 hexStr := hex.EncodeToString(hash[:])
127 r := hexStr[0:2]
128 g := hexStr[2:4]
129 b := hexStr[4:6]
130
131 return fmt.Sprintf("#%s%s%s", r, g, b)
132}
133
134func (ld LabelDefinition) GetColor() string {
135 if ld.Color == nil {
136 seed := fmt.Sprintf("%d:%s:%s", ld.Id, ld.Did, ld.Rkey)
137 color := randomColor(seed)
138 return color
139 }
140
141 return *ld.Color
142}
143
144func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
145 created, err := time.Parse(time.RFC3339, record.CreatedAt)
146 if err != nil {
147 created = time.Now()
148 }
149
150 multiple := false
151 if record.Multiple != nil {
152 multiple = *record.Multiple
153 }
154
155 var vt ValueType
156 if record.ValueType != nil {
157 vt = ValueTypeFromRecord(*record.ValueType)
158 }
159
160 return &LabelDefinition{
161 Did: did,
162 Rkey: rkey,
163
164 Name: record.Name,
165 ValueType: vt,
166 Scope: record.Scope,
167 Color: record.Color,
168 Multiple: multiple,
169 Created: created,
170 }, nil
171}
172
173type LabelOp struct {
174 Id int64
175 Did string
176 Rkey string
177 Subject syntax.ATURI
178 Operation LabelOperation
179 OperandKey string
180 OperandValue string
181 PerformedAt time.Time
182 IndexedAt time.Time
183}
184
185func (l LabelOp) SortAt() time.Time {
186 createdAt := l.PerformedAt
187 indexedAt := l.IndexedAt
188
189 // if we don't have an indexedat, fall back to now
190 if indexedAt.IsZero() {
191 indexedAt = time.Now()
192 }
193
194 // if createdat is invalid (before epoch), treat as null -> return zero time
195 if createdAt.Before(time.UnixMicro(0)) {
196 return time.Time{}
197 }
198
199 // if createdat is <= indexedat, use createdat
200 if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
201 return createdAt
202 }
203
204 // otherwise, createdat is in the future relative to indexedat -> use indexedat
205 return indexedAt
206}
207
208type LabelOperation string
209
210const (
211 LabelOperationAdd LabelOperation = "add"
212 LabelOperationDel LabelOperation = "del"
213)
214
215// a record can create multiple label ops
216func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
217 performed, err := time.Parse(time.RFC3339, record.PerformedAt)
218 if err != nil {
219 performed = time.Now()
220 }
221
222 mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
223 return LabelOp{
224 Did: did,
225 Rkey: rkey,
226 Subject: syntax.ATURI(record.Subject),
227 OperandKey: operand.Key,
228 OperandValue: operand.Value,
229 PerformedAt: performed,
230 }
231 }
232
233 var ops []LabelOp
234 // deletes first, then additions
235 for _, o := range record.Delete {
236 if o != nil {
237 op := mkOp(o)
238 op.Operation = LabelOperationDel
239 ops = append(ops, op)
240 }
241 }
242 for _, o := range record.Add {
243 if o != nil {
244 op := mkOp(o)
245 op.Operation = LabelOperationAdd
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 names map[string]string
295}
296
297func NewLabelState() LabelState {
298 return LabelState{
299 inner: make(map[string]set),
300 names: make(map[string]string),
301 }
302}
303
304func (s LabelState) LabelNames() []string {
305 var result []string
306 for key, valset := range s.inner {
307 if valset == nil {
308 continue
309 }
310 if name, ok := s.names[key]; ok {
311 result = append(result, name)
312 }
313 }
314 return result
315}
316
317func (s LabelState) Inner() map[string]set {
318 return s.inner
319}
320
321func (s LabelState) SetName(key, name string) {
322 s.names[key] = name
323}
324
325func (s LabelState) ContainsLabel(l string) bool {
326 if valset, exists := s.inner[l]; exists {
327 if valset != nil {
328 return true
329 }
330 }
331
332 return false
333}
334
335// go maps behavior in templates make this necessary,
336// indexing a map and getting `set` in return is apparently truthy
337func (s LabelState) ContainsLabelAndVal(l, v string) bool {
338 if valset, exists := s.inner[l]; exists {
339 if _, exists := valset[v]; exists {
340 return true
341 }
342 }
343
344 return false
345}
346
347func (s LabelState) GetValSet(l string) set {
348 if valset, exists := s.inner[l]; exists {
349 return valset
350 } else {
351 return make(set)
352 }
353}
354
355type LabelApplicationCtx struct {
356 Defs map[string]*LabelDefinition // labelAt -> labelDef
357}
358
359var (
360 LabelNoOpError = errors.New("no-op")
361)
362
363func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
364 def, ok := c.Defs[op.OperandKey]
365 if !ok {
366 // this def was deleted, but an op exists, so we just skip over the op
367 return nil
368 }
369
370 state.names[op.OperandKey] = def.Name
371
372 switch op.Operation {
373 case LabelOperationAdd:
374 // if valueset is empty, init it
375 if state.inner[op.OperandKey] == nil {
376 state.inner[op.OperandKey] = make(set)
377 }
378
379 // if valueset is populated & this val alr exists, this labelop is a noop
380 if valueSet, exists := state.inner[op.OperandKey]; exists {
381 if _, exists = valueSet[op.OperandValue]; exists {
382 return LabelNoOpError
383 }
384 }
385
386 if def.Multiple {
387 // append to set
388 state.inner[op.OperandKey][op.OperandValue] = struct{}{}
389 } else {
390 // reset to just this value
391 state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
392 }
393
394 case LabelOperationDel:
395 // if label DNE, then deletion is a no-op
396 if valueSet, exists := state.inner[op.OperandKey]; !exists {
397 return LabelNoOpError
398 } else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
399 return LabelNoOpError
400 }
401
402 if def.Multiple {
403 // remove from set
404 delete(state.inner[op.OperandKey], op.OperandValue)
405 } else {
406 // reset the entire label
407 delete(state.inner, op.OperandKey)
408 }
409
410 // if the map becomes empty, then set it to nil, this is just the inverse of add
411 if len(state.inner[op.OperandKey]) == 0 {
412 state.inner[op.OperandKey] = nil
413 }
414
415 }
416
417 return nil
418}
419
420func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
421 // sort label ops in sort order first
422 slices.SortFunc(ops, func(a, b LabelOp) int {
423 return a.SortAt().Compare(b.SortAt())
424 })
425
426 // apply ops in sequence
427 for _, o := range ops {
428 _ = c.ApplyLabelOp(state, o)
429 }
430}
431
432// IsInverse checks if one label operation is the inverse of another
433// returns true if one is an add and the other is a delete with the same key and value
434func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
435 if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
436 return false
437 }
438
439 return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
440 (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
441}
442
443// removes pairs of label operations that are inverses of each other
444// from the given slice. the function preserves the order of remaining operations.
445func ReduceLabelOps(ops []LabelOp) []LabelOp {
446 if len(ops) <= 1 {
447 return ops
448 }
449
450 keep := make([]bool, len(ops))
451 for i := range keep {
452 keep[i] = true
453 }
454
455 for i := range ops {
456 if !keep[i] {
457 continue
458 }
459
460 for j := i + 1; j < len(ops); j++ {
461 if !keep[j] {
462 continue
463 }
464
465 if ops[i].IsInverse(ops[j]) {
466 keep[i] = false
467 keep[j] = false
468 break // move to next i since this one is now eliminated
469 }
470 }
471 }
472
473 // build result slice with only kept operations
474 var result []LabelOp
475 for i, op := range ops {
476 if keep[i] {
477 result = append(result, op)
478 }
479 }
480
481 return result
482}
483
484func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
485 var labelDefs []LabelDefinition
486 ctx := context.Background()
487
488 for _, dl := range aturis {
489 atUri, err := syntax.ParseATURI(dl)
490 if err != nil {
491 return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
492 }
493 if atUri.Collection() != tangled.LabelDefinitionNSID {
494 return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
495 }
496
497 owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
498 if err != nil {
499 return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
500 }
501
502 xrpcc := xrpc.Client{
503 Host: owner.PDSEndpoint(),
504 }
505
506 record, err := atproto.RepoGetRecord(
507 ctx,
508 &xrpcc,
509 "",
510 atUri.Collection().String(),
511 atUri.Authority().String(),
512 atUri.RecordKey().String(),
513 )
514 if err != nil {
515 return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
516 }
517
518 if record != nil {
519 bytes, err := record.Value.MarshalJSON()
520 if err != nil {
521 return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err)
522 }
523
524 raw := json.RawMessage(bytes)
525 labelRecord := tangled.LabelDefinition{}
526 err = json.Unmarshal(raw, &labelRecord)
527 if err != nil {
528 return nil, fmt.Errorf("invalid record for %s: %w", atUri, err)
529 }
530
531 labelDef, err := LabelDefinitionFromRecord(
532 atUri.Authority().String(),
533 atUri.RecordKey().String(),
534 labelRecord,
535 )
536 if err != nil {
537 return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err)
538 }
539
540 labelDefs = append(labelDefs, *labelDef)
541 }
542 }
543
544 return labelDefs, nil
545}