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/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}