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