social_wot.go raw
1 //go:build !(js && wasm)
2
3 package acl
4
5 import (
6 "sync"
7
8 "next.orly.dev/pkg/lol/log"
9 "next.orly.dev/pkg/database"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 )
12
13 // WoTDepthMap maintains a mapping from pubkey serial to WoT depth (1-N).
14 // It is computed via BFS from anchor pubkeys using the ppg/gpp materialized
15 // indexes and is shared between the social ACL driver and the GC.
16 //
17 // Thread-safe: reads are concurrent, writes (Recompute) take an exclusive lock.
18 type WoTDepthMap struct {
19 mu sync.RWMutex
20 depths map[uint64]int // pubkey serial → WoT depth (1..maxDepth)
21 hexIndex map[string]int // pubkey hex → WoT depth (for ACL layer)
22 anchors [][]byte // anchor pubkeys (32-byte raw)
23 maxDepth int
24 }
25
26 // NewWoTDepthMap creates a new WoT depth map.
27 // anchors: 32-byte raw pubkeys of relay owners/admins.
28 // maxDepth: maximum BFS traversal depth (typically 3).
29 func NewWoTDepthMap(anchors [][]byte, maxDepth int) *WoTDepthMap {
30 if maxDepth <= 0 {
31 maxDepth = 3
32 }
33 if maxDepth > 16 {
34 maxDepth = 16
35 }
36 return &WoTDepthMap{
37 depths: make(map[uint64]int),
38 hexIndex: make(map[string]int),
39 anchors: anchors,
40 maxDepth: maxDepth,
41 }
42 }
43
44 // Recompute runs BFS from each anchor pubkey using the existing
45 // TraversePubkeyPubkey with direction="out" and repopulates the depth map.
46 // For multiple anchors, the minimum depth across all anchors is kept.
47 func (w *WoTDepthMap) Recompute(db *database.D) error {
48 newDepths := make(map[uint64]int)
49 newHex := make(map[string]int)
50
51 for _, anchor := range w.anchors {
52 result, err := db.TraversePubkeyPubkey(anchor, w.maxDepth, "out")
53 if err != nil {
54 log.W.F("WoTDepthMap: BFS failed for anchor %s: %v", hex.Enc(anchor), err)
55 continue
56 }
57
58 // TraversePubkeyPubkey returns GraphResult with PubkeysByDepth map[int][]string
59 for depth, pubkeys := range result.PubkeysByDepth {
60 for _, pkHex := range pubkeys {
61 // Convert hex to raw bytes then to serial
62 pkBytes, err := hex.Dec(pkHex)
63 if err != nil || len(pkBytes) != 32 {
64 continue
65 }
66 serial, err := db.GetPubkeySerial(pkBytes)
67 if err != nil {
68 continue
69 }
70 serialVal := serial.Get()
71
72 // Keep minimum depth across all anchors
73 if existing, ok := newDepths[serialVal]; !ok || depth < existing {
74 newDepths[serialVal] = depth
75 newHex[pkHex] = depth
76 }
77 }
78 }
79 }
80
81 // Also add anchors themselves at depth 0 (owners/admins)
82 for _, anchor := range w.anchors {
83 serial, err := db.GetPubkeySerial(anchor)
84 if err != nil {
85 continue
86 }
87 newDepths[serial.Get()] = 0
88 newHex[hex.Enc(anchor)] = 0
89 }
90
91 w.mu.Lock()
92 w.depths = newDepths
93 w.hexIndex = newHex
94 w.mu.Unlock()
95
96 log.I.F("WoTDepthMap: recomputed with %d pubkeys across %d anchors (max depth %d)",
97 len(newDepths), len(w.anchors), w.maxDepth)
98
99 return nil
100 }
101
102 // GetDepth returns the WoT depth for a pubkey serial.
103 // Returns 0 if unknown (not in WoT) -- callers must distinguish
104 // "depth 0 = anchor" from "not found" using the ok return value.
105 func (w *WoTDepthMap) GetDepth(pubkeySerial uint64) (depth int, ok bool) {
106 w.mu.RLock()
107 depth, ok = w.depths[pubkeySerial]
108 w.mu.RUnlock()
109 return
110 }
111
112 // GetDepthByHex returns the WoT depth for a hex-encoded pubkey.
113 // Returns -1 if not in WoT.
114 func (w *WoTDepthMap) GetDepthByHex(pubkeyHex string) int {
115 w.mu.RLock()
116 depth, ok := w.hexIndex[pubkeyHex]
117 w.mu.RUnlock()
118 if !ok {
119 return -1
120 }
121 return depth
122 }
123
124 // Size returns the number of pubkeys in the depth map.
125 func (w *WoTDepthMap) Size() int {
126 w.mu.RLock()
127 defer w.mu.RUnlock()
128 return len(w.depths)
129 }
130
131 // GetDepthForGC implements the WoTProvider interface used by the GC.
132 // Returns the depth bonus tier: 1, 2, 3 for WoT members, 0 for outsiders.
133 // Anchors (depth 0) are treated as tier 1.
134 func (w *WoTDepthMap) GetDepthForGC(pubkeySerial uint64) int {
135 depth, ok := w.GetDepth(pubkeySerial)
136 if !ok {
137 return 0 // outsider
138 }
139 if depth == 0 {
140 return 1 // anchor = same as depth 1
141 }
142 return depth
143 }
144