200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
//go:build !(js && wasm)
|
|
|
|
package database
|
|
|
|
import (
|
|
"lol.mleku.dev/log"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
)
|
|
|
|
// TraverseFollows performs BFS traversal of the follow graph starting from a seed pubkey.
|
|
// Returns pubkeys grouped by first-discovered depth (no duplicates across depths).
|
|
//
|
|
// The traversal works by:
|
|
// 1. Starting with the seed pubkey at depth 0 (not included in results)
|
|
// 2. For each pubkey at the current depth, find their kind-3 contact list
|
|
// 3. Extract p-tags from the contact list to get follows
|
|
// 4. Add new (unseen) follows to the next depth
|
|
// 5. Continue until maxDepth is reached or no new pubkeys are found
|
|
//
|
|
// Early termination occurs if two consecutive depths yield no new pubkeys.
|
|
func (d *D) TraverseFollows(seedPubkey []byte, maxDepth int) (*GraphResult, error) {
|
|
result := NewGraphResult()
|
|
|
|
if len(seedPubkey) != 32 {
|
|
return result, ErrPubkeyNotFound
|
|
}
|
|
|
|
// Get seed pubkey serial
|
|
seedSerial, err := d.GetPubkeySerial(seedPubkey)
|
|
if err != nil {
|
|
log.D.F("TraverseFollows: seed pubkey not in database: %s", hex.Enc(seedPubkey))
|
|
return result, nil // Not an error - just no results
|
|
}
|
|
|
|
// Track visited pubkeys by serial to avoid cycles
|
|
visited := make(map[uint64]bool)
|
|
visited[seedSerial.Get()] = true // Mark seed as visited but don't add to results
|
|
|
|
// Current frontier (pubkeys to process at this depth)
|
|
currentFrontier := []*types.Uint40{seedSerial}
|
|
|
|
// Track consecutive empty depths for early termination
|
|
consecutiveEmptyDepths := 0
|
|
|
|
for currentDepth := 1; currentDepth <= maxDepth; currentDepth++ {
|
|
var nextFrontier []*types.Uint40
|
|
newPubkeysAtDepth := 0
|
|
|
|
for _, pubkeySerial := range currentFrontier {
|
|
// Get follows for this pubkey
|
|
follows, err := d.GetFollowsFromPubkeySerial(pubkeySerial)
|
|
if err != nil {
|
|
log.D.F("TraverseFollows: error getting follows for serial %d: %v", pubkeySerial.Get(), err)
|
|
continue
|
|
}
|
|
|
|
for _, followSerial := range follows {
|
|
// Skip if already visited
|
|
if visited[followSerial.Get()] {
|
|
continue
|
|
}
|
|
visited[followSerial.Get()] = true
|
|
|
|
// Get pubkey hex for result
|
|
pubkeyHex, err := d.GetPubkeyHexFromSerial(followSerial)
|
|
if err != nil {
|
|
log.D.F("TraverseFollows: error getting pubkey hex for serial %d: %v", followSerial.Get(), err)
|
|
continue
|
|
}
|
|
|
|
// Add to results at this depth
|
|
result.AddPubkeyAtDepth(pubkeyHex, currentDepth)
|
|
newPubkeysAtDepth++
|
|
|
|
// Add to next frontier for further traversal
|
|
nextFrontier = append(nextFrontier, followSerial)
|
|
}
|
|
}
|
|
|
|
log.T.F("TraverseFollows: depth %d found %d new pubkeys", currentDepth, newPubkeysAtDepth)
|
|
|
|
// Check for early termination
|
|
if newPubkeysAtDepth == 0 {
|
|
consecutiveEmptyDepths++
|
|
if consecutiveEmptyDepths >= 2 {
|
|
log.T.F("TraverseFollows: early termination at depth %d (2 consecutive empty depths)", currentDepth)
|
|
break
|
|
}
|
|
} else {
|
|
consecutiveEmptyDepths = 0
|
|
}
|
|
|
|
// Move to next depth
|
|
currentFrontier = nextFrontier
|
|
}
|
|
|
|
log.D.F("TraverseFollows: completed with %d total pubkeys across %d depths",
|
|
result.TotalPubkeys, len(result.PubkeysByDepth))
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// TraverseFollowers performs BFS traversal to find who follows the seed pubkey.
|
|
// This is the reverse of TraverseFollows - it finds users whose kind-3 lists
|
|
// contain the target pubkey(s).
|
|
//
|
|
// At each depth:
|
|
// - Depth 1: Users who directly follow the seed
|
|
// - Depth 2: Users who follow anyone at depth 1 (followers of followers)
|
|
// - etc.
|
|
func (d *D) TraverseFollowers(seedPubkey []byte, maxDepth int) (*GraphResult, error) {
|
|
result := NewGraphResult()
|
|
|
|
if len(seedPubkey) != 32 {
|
|
return result, ErrPubkeyNotFound
|
|
}
|
|
|
|
// Get seed pubkey serial
|
|
seedSerial, err := d.GetPubkeySerial(seedPubkey)
|
|
if err != nil {
|
|
log.D.F("TraverseFollowers: seed pubkey not in database: %s", hex.Enc(seedPubkey))
|
|
return result, nil
|
|
}
|
|
|
|
// Track visited pubkeys
|
|
visited := make(map[uint64]bool)
|
|
visited[seedSerial.Get()] = true
|
|
|
|
// Current frontier
|
|
currentFrontier := []*types.Uint40{seedSerial}
|
|
|
|
consecutiveEmptyDepths := 0
|
|
|
|
for currentDepth := 1; currentDepth <= maxDepth; currentDepth++ {
|
|
var nextFrontier []*types.Uint40
|
|
newPubkeysAtDepth := 0
|
|
|
|
for _, targetSerial := range currentFrontier {
|
|
// Get followers of this pubkey
|
|
followers, err := d.GetFollowersOfPubkeySerial(targetSerial)
|
|
if err != nil {
|
|
log.D.F("TraverseFollowers: error getting followers for serial %d: %v", targetSerial.Get(), err)
|
|
continue
|
|
}
|
|
|
|
for _, followerSerial := range followers {
|
|
if visited[followerSerial.Get()] {
|
|
continue
|
|
}
|
|
visited[followerSerial.Get()] = true
|
|
|
|
pubkeyHex, err := d.GetPubkeyHexFromSerial(followerSerial)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
result.AddPubkeyAtDepth(pubkeyHex, currentDepth)
|
|
newPubkeysAtDepth++
|
|
nextFrontier = append(nextFrontier, followerSerial)
|
|
}
|
|
}
|
|
|
|
log.T.F("TraverseFollowers: depth %d found %d new pubkeys", currentDepth, newPubkeysAtDepth)
|
|
|
|
if newPubkeysAtDepth == 0 {
|
|
consecutiveEmptyDepths++
|
|
if consecutiveEmptyDepths >= 2 {
|
|
break
|
|
}
|
|
} else {
|
|
consecutiveEmptyDepths = 0
|
|
}
|
|
|
|
currentFrontier = nextFrontier
|
|
}
|
|
|
|
log.D.F("TraverseFollowers: completed with %d total pubkeys", result.TotalPubkeys)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// TraverseFollowsFromHex is a convenience wrapper that accepts hex-encoded pubkey.
|
|
func (d *D) TraverseFollowsFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error) {
|
|
seedPubkey, err := hex.Dec(seedPubkeyHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.TraverseFollows(seedPubkey, maxDepth)
|
|
}
|
|
|
|
// TraverseFollowersFromHex is a convenience wrapper that accepts hex-encoded pubkey.
|
|
func (d *D) TraverseFollowersFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error) {
|
|
seedPubkey, err := hex.Dec(seedPubkeyHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.TraverseFollowers(seedPubkey, maxDepth)
|
|
}
|