package nostr import ( "errors" "sort" "strconv" "strings" ) // Graph query edge families. They mirror orly's graph extension: pp is the // pubkey↔pubkey graph, pe the pubkey↔event bipartite graph, and ee the // event↔event thread graph. const ( GraphEdgePubkeyPubkey = "pp" GraphEdgePubkeyEvent = "pe" GraphEdgeEventEvent = "ee" ) // Graph directions. const ( GraphDirOut = "out" GraphDirIn = "in" GraphDirBoth = "both" ) // GraphMaxDepth is the largest accepted traversal depth. const GraphMaxDepth = 16 // KindGraphResult is the relay-signed event kind returned for a _graph query. const KindGraphResult = 39000 var ( errGraphSeed = errors.New("graph: seed must be 64 hexadecimal characters") errGraphDepth = errors.New("graph: depth must be between 1 and 16") errGraphEdge = errors.New("graph: edge must be pp, pe or ee") errGraphDirection = errors.New("graph: direction must be out, in or both") ) // GraphQuery is the parsed value of a filter's _graph extension. // // The canonical wire form is a four-element array: // // ["_graph", ["", , "", ""]] // // The object form accepted by orly is also decoded: // // {"pubkey":"","depth":1,"edge":"pp","direction":"out"} type GraphQuery struct { Seed string Depth int Edge string Direction string } // Validate checks a normalized graph query without changing it. func (q *GraphQuery) Validate() error { if q == nil { return errors.New("graph: nil query") } if !IsHex64(q.Seed) { return errGraphSeed } if q.Depth < 1 || q.Depth > GraphMaxDepth { return errGraphDepth } switch q.Edge { case GraphEdgePubkeyPubkey, GraphEdgePubkeyEvent, GraphEdgeEventEvent: default: return errGraphEdge } switch q.Direction { case GraphDirOut, GraphDirIn, GraphDirBoth: default: return errGraphDirection } return nil } // normalize applies the defaults and canonical spelling the decoder promises: // a lowercase seed, depth 1 when omitted, and lowercase edge/direction names. func (q *GraphQuery) normalize() error { q.Seed = strings.ToLower(q.Seed) q.Edge = strings.ToLower(q.Edge) q.Direction = strings.ToLower(q.Direction) if q.Depth == 0 { q.Depth = 1 } return q.Validate() } // IsOutbound reports whether traversal follows outbound edges. func (q *GraphQuery) IsOutbound() bool { return q.Direction == GraphDirOut || q.Direction == GraphDirBoth } // IsInbound reports whether traversal follows inbound edges. func (q *GraphQuery) IsInbound() bool { return q.Direction == GraphDirIn || q.Direction == GraphDirBoth } // GraphResult is a depth-organized traversal result. Index 0 of each slice // holds the nodes first discovered at depth 1. // // A node appears only at the depth where it was first seen. The response form // keeps one entry per depth up to the deepest non-empty level, including empty // slices for gaps. type GraphResult struct { PubkeysByDepth [][]string EventsByDepth [][]string TotalPubkeys int TotalEvents int Elapsed string seenPub map[string]struct{} seenEv map[string]struct{} } // NewGraphResult returns an empty result ready for Add calls. func NewGraphResult() *GraphResult { return &GraphResult{ seenPub: make(map[string]struct{}), seenEv: make(map[string]struct{}), } } // AddPubkeyAtDepth records a pubkey if it has not been seen before. func (r *GraphResult) AddPubkeyAtDepth(pubkey string, depth int) bool { if depth < 1 { depth = 1 } if r.seenPub == nil { r.seenPub = make(map[string]struct{}) } if _, ok := r.seenPub[pubkey]; ok { return false } r.seenPub[pubkey] = struct{}{} r.PubkeysByDepth = growDepth(r.PubkeysByDepth, depth) r.PubkeysByDepth[depth-1] = append(r.PubkeysByDepth[depth-1], pubkey) r.TotalPubkeys++ return true } // AddEventAtDepth records an event id if it has not been seen before. func (r *GraphResult) AddEventAtDepth(id string, depth int) bool { if depth < 1 { depth = 1 } if r.seenEv == nil { r.seenEv = make(map[string]struct{}) } if _, ok := r.seenEv[id]; ok { return false } r.seenEv[id] = struct{}{} r.EventsByDepth = growDepth(r.EventsByDepth, depth) r.EventsByDepth[depth-1] = append(r.EventsByDepth[depth-1], id) r.TotalEvents++ return true } func growDepth(dst [][]string, depth int) [][]string { for len(dst) < depth { dst = append(dst, nil) } return dst } // Sort orders every depth level lexicographically so relay responses are // deterministic even though the underlying index scans depend on insertion // order. func (r *GraphResult) Sort() { for _, level := range r.PubkeysByDepth { sort.Strings(level) } for _, level := range r.EventsByDepth { sort.Strings(level) } } // AppendContentJSON appends the graph result's event content. Only the // non-empty side of the result is emitted, matching orly's response shape. func (r *GraphResult) AppendContentJSON(dst []byte) []byte { dst = append(dst, '{') first := true field := func() { if !first { dst = append(dst, ',') } first = false } if len(r.PubkeysByDepth) > 0 { field() dst = append(dst, `"pubkeys_by_depth":`...) dst = appendDepthArrays(dst, r.PubkeysByDepth) dst = append(dst, `,"total_pubkeys":`...) dst = strconv.AppendInt(dst, int64(r.TotalPubkeys), 10) } if len(r.EventsByDepth) > 0 { field() dst = append(dst, `"events_by_depth":`...) dst = appendDepthArrays(dst, r.EventsByDepth) dst = append(dst, `,"total_events":`...) dst = strconv.AppendInt(dst, int64(r.TotalEvents), 10) } if r.Elapsed != "" { field() dst = append(dst, `"elapsed":`...) dst = AppendJSONString(dst, r.Elapsed) } return append(dst, '}') } func appendDepthArrays(dst []byte, levels [][]string) []byte { dst = append(dst, '[') for i, level := range levels { if i > 0 { dst = append(dst, ',') } dst = append(dst, '[') for j, id := range level { if j > 0 { dst = append(dst, ',') } dst = AppendJSONString(dst, id) } dst = append(dst, ']') } return append(dst, ']') }