whitelist.go raw
1 package grapevine
2
3 import (
4 "context"
5 "encoding/hex"
6 "time"
7
8 "git.smesh.lol/orly/pkg/lol/log"
9 "git.smesh.lol/orly/pkg/nostr/encoders/bech32encoding"
10 )
11
12 // WhitelistUpdater manages automatic whitelist updates based on GrapeVine scores.
13 type WhitelistUpdater struct {
14 engine *Engine
15 influenceThreshold float64
16 updateInterval time.Duration
17 onUpdate func(pubkeys []string) error
18 ctx context.Context
19 cancel context.CancelFunc
20 running bool
21 }
22
23 // WhitelistConfig configures the whitelist updater.
24 type WhitelistConfig struct {
25 // InfluenceThreshold is the minimum influence score for whitelist inclusion (default: 0.5)
26 InfluenceThreshold float64
27 // UpdateInterval is how often to recalculate and update the whitelist (default: 6h)
28 UpdateInterval time.Duration
29 // OnUpdate is called with the new whitelist pubkeys when an update occurs
30 OnUpdate func(pubkeys []string) error
31 }
32
33 // NewWhitelistUpdater creates a new whitelist updater.
34 func NewWhitelistUpdater(ctx context.Context, engine *Engine, cfg WhitelistConfig) *WhitelistUpdater {
35 if cfg.InfluenceThreshold <= 0 {
36 cfg.InfluenceThreshold = 0.5
37 }
38 if cfg.UpdateInterval <= 0 {
39 cfg.UpdateInterval = 6 * time.Hour
40 }
41
42 ctx, cancel := context.WithCancel(ctx)
43 return &WhitelistUpdater{
44 engine: engine,
45 influenceThreshold: cfg.InfluenceThreshold,
46 updateInterval: cfg.UpdateInterval,
47 onUpdate: cfg.OnUpdate,
48 ctx: ctx,
49 cancel: cancel,
50 }
51 }
52
53 // Start begins the automatic whitelist update loop.
54 func (w *WhitelistUpdater) Start(observerHex string) error {
55 if w.running {
56 return nil
57 }
58 w.running = true
59
60 go w.updateLoop(observerHex)
61 log.I.F("grapevine whitelist updater: started (threshold: %.2f, interval: %v)",
62 w.influenceThreshold, w.updateInterval)
63 return nil
64 }
65
66 // Stop stops the whitelist updater.
67 func (w *WhitelistUpdater) Stop() {
68 if !w.running {
69 return
70 }
71 w.running = false
72 w.cancel()
73 log.I.F("grapevine whitelist updater: stopped")
74 }
75
76 // updateLoop runs the periodic whitelist update.
77 func (w *WhitelistUpdater) updateLoop(observerHex string) {
78 // Run immediately on start
79 w.updateOnce(observerHex)
80
81 ticker := time.NewTicker(w.updateInterval)
82 defer ticker.Stop()
83
84 for {
85 select {
86 case <-w.ctx.Done():
87 return
88 case <-ticker.C:
89 w.updateOnce(observerHex)
90 }
91 }
92 }
93
94 // updateOnce performs a single whitelist update.
95 func (w *WhitelistUpdater) updateOnce(observerHex string) {
96 start := time.Now()
97 log.I.F("grapevine whitelist updater: starting update for observer %s", observerHex[:12])
98
99 // Compute fresh GrapeVine scores
100 scoreSet, err := w.engine.Compute(observerHex)
101 if err != nil {
102 log.E.F("grapevine whitelist updater: failed to compute scores: %v", err)
103 return
104 }
105
106 // Filter pubkeys by influence threshold
107 var whitelist []string
108 for _, score := range scoreSet.Scores {
109 if score.Influence >= w.influenceThreshold {
110 whitelist = append(whitelist, score.PubkeyHex)
111 }
112 }
113
114 log.I.F("grapevine whitelist updater: computed %d whitelisted pubkeys (threshold: %.2f, total: %d)",
115 len(whitelist), w.influenceThreshold, len(scoreSet.Scores))
116
117 // Call the update callback
118 if w.onUpdate != nil {
119 if err := w.onUpdate(whitelist); err != nil {
120 log.E.F("grapevine whitelist updater: failed to apply whitelist update: %v", err)
121 return
122 }
123 }
124
125 log.I.F("grapevine whitelist updater: completed update in %v", time.Since(start))
126 }
127
128 // TriggerNow forces an immediate whitelist update.
129 func (w *WhitelistUpdater) TriggerNow(observerHex string) {
130 go w.updateOnce(observerHex)
131 }
132
133 // GetWhitelist returns the current whitelist for an observer without updating.
134 func (w *WhitelistUpdater) GetWhitelist(observerHex string) ([]string, error) {
135 scoreSet, err := w.engine.GetScores(observerHex)
136 if err != nil {
137 return nil, err
138 }
139 if scoreSet == nil {
140 return nil, nil
141 }
142
143 var whitelist []string
144 for _, score := range scoreSet.Scores {
145 if score.Influence >= w.influenceThreshold {
146 whitelist = append(whitelist, score.PubkeyHex)
147 }
148 }
149 return whitelist, nil
150 }
151
152 // PubkeysToHexStrings converts binary pubkeys to hex strings.
153 func PubkeysToHexStrings(pubkeys [][]byte) []string {
154 result := make([]string, 0, len(pubkeys))
155 for _, pk := range pubkeys {
156 result = append(result, hex.EncodeToString(pk))
157 }
158 return result
159 }
160
161 // HexStringsToPubkeys converts hex strings to binary pubkeys.
162 func HexStringsToPubkeys(hexKeys []string) ([][]byte, error) {
163 result := make([][]byte, 0, len(hexKeys))
164 for _, h := range hexKeys {
165 pk, err := hex.DecodeString(h)
166 if err != nil {
167 // Try npub format
168 pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(h)
169 if err != nil {
170 continue
171 }
172 }
173 if len(pk) == 32 {
174 result = append(result, pk)
175 }
176 }
177 return result, nil
178 }
179