You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.0 KiB
202 lines
6.0 KiB
//go:build !(js && wasm) |
|
|
|
// Package graph implements NIP-XX Graph Query protocol support. |
|
// This file contains the executor that runs graph traversal queries. |
|
package graph |
|
|
|
import ( |
|
"encoding/json" |
|
"strconv" |
|
"time" |
|
|
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/log" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/interfaces/signer" |
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
|
) |
|
|
|
// Response kinds for graph queries (ephemeral range, relay-signed) |
|
const ( |
|
KindGraphFollows = 39000 // Response for follows/followers queries |
|
KindGraphMentions = 39001 // Response for mentions queries |
|
KindGraphThread = 39002 // Response for thread traversal queries |
|
) |
|
|
|
// GraphResultI is the interface that database.GraphResult implements. |
|
// This allows the executor to work with the database result without importing it. |
|
type GraphResultI interface { |
|
ToDepthArrays() [][]string |
|
ToEventDepthArrays() [][]string |
|
GetAllPubkeys() []string |
|
GetAllEvents() []string |
|
GetPubkeysByDepth() map[int][]string |
|
GetEventsByDepth() map[int][]string |
|
GetTotalPubkeys() int |
|
GetTotalEvents() int |
|
} |
|
|
|
// GraphDatabase defines the interface for graph traversal operations. |
|
// This is implemented by the database package. |
|
type GraphDatabase interface { |
|
// TraverseFollows performs BFS traversal of follow graph |
|
TraverseFollows(seedPubkey []byte, maxDepth int) (GraphResultI, error) |
|
// TraverseFollowers performs BFS traversal to find followers |
|
TraverseFollowers(seedPubkey []byte, maxDepth int) (GraphResultI, error) |
|
// FindMentions finds events mentioning a pubkey |
|
FindMentions(pubkey []byte, kinds []uint16) (GraphResultI, error) |
|
// TraverseThread performs BFS traversal of thread structure |
|
TraverseThread(seedEventID []byte, maxDepth int, direction string) (GraphResultI, error) |
|
} |
|
|
|
// Executor handles graph query execution and response generation. |
|
type Executor struct { |
|
db GraphDatabase |
|
relaySigner signer.I |
|
relayPubkey []byte |
|
} |
|
|
|
// NewExecutor creates a new graph query executor. |
|
// The secretKey should be the 32-byte relay identity secret key. |
|
func NewExecutor(db GraphDatabase, secretKey []byte) (*Executor, error) { |
|
s, err := p8k.New() |
|
if err != nil { |
|
return nil, err |
|
} |
|
if err = s.InitSec(secretKey); err != nil { |
|
return nil, err |
|
} |
|
return &Executor{ |
|
db: db, |
|
relaySigner: s, |
|
relayPubkey: s.Pub(), |
|
}, nil |
|
} |
|
|
|
// Execute runs a graph query and returns a relay-signed event with results. |
|
func (e *Executor) Execute(q *Query) (*event.E, error) { |
|
var result GraphResultI |
|
var err error |
|
var responseKind uint16 |
|
|
|
// Decode seed (hex string to bytes) |
|
seedBytes, err := hex.Dec(q.Seed) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Execute the appropriate traversal |
|
switch q.Method { |
|
case "follows": |
|
responseKind = KindGraphFollows |
|
result, err = e.db.TraverseFollows(seedBytes, q.Depth) |
|
if err != nil { |
|
return nil, err |
|
} |
|
log.D.F("graph executor: follows traversal returned %d pubkeys", result.GetTotalPubkeys()) |
|
|
|
case "followers": |
|
responseKind = KindGraphFollows |
|
result, err = e.db.TraverseFollowers(seedBytes, q.Depth) |
|
if err != nil { |
|
return nil, err |
|
} |
|
log.D.F("graph executor: followers traversal returned %d pubkeys", result.GetTotalPubkeys()) |
|
|
|
case "mentions": |
|
responseKind = KindGraphMentions |
|
// Mentions don't use depth traversal, just find direct mentions |
|
// Convert RefSpec kinds to uint16 for the database call |
|
var kinds []uint16 |
|
if len(q.InboundRefs) > 0 { |
|
for _, rs := range q.InboundRefs { |
|
for _, k := range rs.Kinds { |
|
kinds = append(kinds, uint16(k)) |
|
} |
|
} |
|
} else { |
|
kinds = []uint16{1} // Default to kind 1 (notes) |
|
} |
|
result, err = e.db.FindMentions(seedBytes, kinds) |
|
if err != nil { |
|
return nil, err |
|
} |
|
log.D.F("graph executor: mentions query returned %d events", result.GetTotalEvents()) |
|
|
|
case "thread": |
|
responseKind = KindGraphThread |
|
result, err = e.db.TraverseThread(seedBytes, q.Depth, "both") |
|
if err != nil { |
|
return nil, err |
|
} |
|
log.D.F("graph executor: thread traversal returned %d events", result.GetTotalEvents()) |
|
|
|
default: |
|
return nil, ErrInvalidMethod |
|
} |
|
|
|
// Generate response event |
|
return e.generateResponse(q, result, responseKind) |
|
} |
|
|
|
// generateResponse creates a relay-signed event containing the query results. |
|
func (e *Executor) generateResponse(q *Query, result GraphResultI, responseKind uint16) (*event.E, error) { |
|
// Build content as JSON with depth arrays |
|
var content ResponseContent |
|
|
|
if q.Method == "follows" || q.Method == "followers" { |
|
// For pubkey-based queries, use pubkeys_by_depth |
|
content.PubkeysByDepth = result.ToDepthArrays() |
|
content.TotalPubkeys = result.GetTotalPubkeys() |
|
} else { |
|
// For event-based queries, use events_by_depth |
|
content.EventsByDepth = result.ToEventDepthArrays() |
|
content.TotalEvents = result.GetTotalEvents() |
|
} |
|
|
|
contentBytes, err := json.Marshal(content) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Build tags |
|
tags := tag.NewS( |
|
tag.NewFromAny("method", q.Method), |
|
tag.NewFromAny("seed", q.Seed), |
|
tag.NewFromAny("depth", strconv.Itoa(q.Depth)), |
|
) |
|
|
|
// Create event |
|
ev := &event.E{ |
|
Kind: responseKind, |
|
CreatedAt: time.Now().Unix(), |
|
Tags: tags, |
|
Content: contentBytes, |
|
} |
|
|
|
// Sign with relay identity |
|
if err = ev.Sign(e.relaySigner); chk.E(err) { |
|
return nil, err |
|
} |
|
|
|
return ev, nil |
|
} |
|
|
|
// ResponseContent is the JSON structure for graph query responses. |
|
type ResponseContent struct { |
|
// PubkeysByDepth contains arrays of pubkeys at each depth (1-indexed) |
|
// Each pubkey appears ONLY at the depth where it was first discovered. |
|
PubkeysByDepth [][]string `json:"pubkeys_by_depth,omitempty"` |
|
|
|
// EventsByDepth contains arrays of event IDs at each depth (1-indexed) |
|
EventsByDepth [][]string `json:"events_by_depth,omitempty"` |
|
|
|
// TotalPubkeys is the total count of unique pubkeys discovered |
|
TotalPubkeys int `json:"total_pubkeys,omitempty"` |
|
|
|
// TotalEvents is the total count of unique events discovered |
|
TotalEvents int `json:"total_events,omitempty"` |
|
}
|
|
|