561 lines
16 KiB
Go
561 lines
16 KiB
Go
//go:build !(js && wasm)
|
|
|
|
package database
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
|
|
"github.com/dgraph-io/badger/v4"
|
|
"lol.mleku.dev/chk"
|
|
"lol.mleku.dev/log"
|
|
"next.orly.dev/pkg/database/indexes"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
)
|
|
|
|
// Graph traversal errors
|
|
var (
|
|
ErrPubkeyNotFound = errors.New("pubkey not found in database")
|
|
ErrEventNotFound = errors.New("event not found in database")
|
|
)
|
|
|
|
// GetPTagsFromEventSerial extracts p-tag pubkey serials from an event by its serial.
|
|
// This is a pure index-based operation - no event decoding required.
|
|
// It scans the epg (event-pubkey-graph) index for p-tag edges.
|
|
func (d *D) GetPTagsFromEventSerial(eventSerial *types.Uint40) ([]*types.Uint40, error) {
|
|
var pubkeySerials []*types.Uint40
|
|
|
|
// Build prefix: epg|event_serial
|
|
prefix := new(bytes.Buffer)
|
|
prefix.Write([]byte(indexes.EventPubkeyGraphPrefix))
|
|
if err := eventSerial.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Decode key: epg(3)|event_serial(5)|pubkey_serial(5)|kind(2)|direction(1)
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Extract direction to filter for p-tags only
|
|
direction := key[15]
|
|
if direction != types.EdgeDirectionPTagOut {
|
|
continue // Skip author edges, only want p-tag edges
|
|
}
|
|
|
|
// Extract pubkey serial (bytes 8-12)
|
|
pubkeySerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[8:13])
|
|
if err := pubkeySerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
pubkeySerials = append(pubkeySerials, pubkeySerial)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return pubkeySerials, err
|
|
}
|
|
|
|
// GetETagsFromEventSerial extracts e-tag event serials from an event by its serial.
|
|
// This is a pure index-based operation - no event decoding required.
|
|
// It scans the eeg (event-event-graph) index for outbound e-tag edges.
|
|
func (d *D) GetETagsFromEventSerial(eventSerial *types.Uint40) ([]*types.Uint40, error) {
|
|
var targetSerials []*types.Uint40
|
|
|
|
// Build prefix: eeg|source_event_serial
|
|
prefix := new(bytes.Buffer)
|
|
prefix.Write([]byte(indexes.EventEventGraphPrefix))
|
|
if err := eventSerial.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Decode key: eeg(3)|source_serial(5)|target_serial(5)|kind(2)|direction(1)
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Extract target serial (bytes 8-12)
|
|
targetSerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[8:13])
|
|
if err := targetSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
targetSerials = append(targetSerials, targetSerial)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return targetSerials, err
|
|
}
|
|
|
|
// GetReferencingEvents finds all events that reference a target event via e-tags.
|
|
// Optionally filters by event kinds. Uses the gee (reverse e-tag graph) index.
|
|
func (d *D) GetReferencingEvents(targetSerial *types.Uint40, kinds []uint16) ([]*types.Uint40, error) {
|
|
var sourceSerials []*types.Uint40
|
|
|
|
if len(kinds) == 0 {
|
|
// No kind filter - scan all kinds
|
|
prefix := new(bytes.Buffer)
|
|
prefix.Write([]byte(indexes.GraphEventEventPrefix))
|
|
if err := targetSerial.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Decode key: gee(3)|target_serial(5)|kind(2)|direction(1)|source_serial(5)
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Extract source serial (bytes 11-15)
|
|
sourceSerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[11:16])
|
|
if err := sourceSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
sourceSerials = append(sourceSerials, sourceSerial)
|
|
}
|
|
return nil
|
|
})
|
|
return sourceSerials, err
|
|
}
|
|
|
|
// With kind filter - scan each kind's prefix
|
|
for _, k := range kinds {
|
|
kind := new(types.Uint16)
|
|
kind.Set(k)
|
|
|
|
direction := new(types.Letter)
|
|
direction.Set(types.EdgeDirectionETagIn)
|
|
|
|
prefix := new(bytes.Buffer)
|
|
if err := indexes.GraphEventEventEnc(targetSerial, kind, direction, nil).MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Extract source serial (last 5 bytes)
|
|
if len(key) < 5 {
|
|
continue
|
|
}
|
|
sourceSerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[len(key)-5:])
|
|
if err := sourceSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
sourceSerials = append(sourceSerials, sourceSerial)
|
|
}
|
|
return nil
|
|
})
|
|
if chk.E(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return sourceSerials, nil
|
|
}
|
|
|
|
// FindEventByAuthorAndKind finds the most recent event of a specific kind by an author.
|
|
// This is used to find kind-3 contact lists for follow graph traversal.
|
|
// Returns nil, nil if no matching event is found.
|
|
func (d *D) FindEventByAuthorAndKind(authorSerial *types.Uint40, kind uint16) (*types.Uint40, error) {
|
|
var eventSerial *types.Uint40
|
|
|
|
// First, get the full pubkey from the serial
|
|
pubkey, err := d.GetPubkeyBySerial(authorSerial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build prefix for kind-pubkey index: kpc|kind|pubkey_hash
|
|
pubHash := new(types.PubHash)
|
|
if err := pubHash.FromPubkey(pubkey); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
|
|
kindType := new(types.Uint16)
|
|
kindType.Set(kind)
|
|
|
|
prefix := new(bytes.Buffer)
|
|
prefix.Write([]byte(indexes.KindPubkeyPrefix))
|
|
if err := kindType.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
if err := pubHash.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err = d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
opts.Reverse = true // Most recent first (highest created_at)
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
// Seek to end of prefix range for reverse iteration
|
|
seekKey := make([]byte, len(searchPrefix)+8+5) // prefix + max timestamp + max serial
|
|
copy(seekKey, searchPrefix)
|
|
for i := len(searchPrefix); i < len(seekKey); i++ {
|
|
seekKey[i] = 0xFF
|
|
}
|
|
|
|
it.Seek(seekKey)
|
|
if !it.ValidForPrefix(searchPrefix) {
|
|
// Try going to the first valid key if seek went past
|
|
it.Rewind()
|
|
it.Seek(searchPrefix)
|
|
}
|
|
|
|
if it.ValidForPrefix(searchPrefix) {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Decode key: kpc(3)|kind(2)|pubkey_hash(8)|created_at(8)|serial(5)
|
|
// Total: 26 bytes
|
|
if len(key) < 26 {
|
|
return nil
|
|
}
|
|
|
|
// Extract serial (last 5 bytes)
|
|
eventSerial = new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[len(key)-5:])
|
|
if err := eventSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return eventSerial, err
|
|
}
|
|
|
|
// GetPubkeyHexFromSerial converts a pubkey serial to its hex string representation.
|
|
func (d *D) GetPubkeyHexFromSerial(serial *types.Uint40) (string, error) {
|
|
pubkey, err := d.GetPubkeyBySerial(serial)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hex.Enc(pubkey), nil
|
|
}
|
|
|
|
// GetEventIDFromSerial converts an event serial to its hex ID string.
|
|
func (d *D) GetEventIDFromSerial(serial *types.Uint40) (string, error) {
|
|
eventID, err := d.GetEventIdBySerial(serial)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hex.Enc(eventID), nil
|
|
}
|
|
|
|
// GetEventsReferencingPubkey finds all events that reference a pubkey via p-tags.
|
|
// Uses the peg (pubkey-event-graph) index with direction filter for inbound p-tags.
|
|
// Optionally filters by event kinds.
|
|
func (d *D) GetEventsReferencingPubkey(pubkeySerial *types.Uint40, kinds []uint16) ([]*types.Uint40, error) {
|
|
var eventSerials []*types.Uint40
|
|
|
|
if len(kinds) == 0 {
|
|
// No kind filter - we need to scan common kinds since direction comes after kind in the key
|
|
// Use same approach as QueryPTagGraph
|
|
commonKinds := []uint16{1, 6, 7, 9735, 10002, 3, 4, 5, 30023}
|
|
kinds = commonKinds
|
|
}
|
|
|
|
for _, k := range kinds {
|
|
kind := new(types.Uint16)
|
|
kind.Set(k)
|
|
|
|
direction := new(types.Letter)
|
|
direction.Set(types.EdgeDirectionPTagIn) // Inbound p-tags
|
|
|
|
prefix := new(bytes.Buffer)
|
|
if err := indexes.PubkeyEventGraphEnc(pubkeySerial, kind, direction, nil).MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Key format: peg(3)|pubkey_serial(5)|kind(2)|direction(1)|event_serial(5) = 16 bytes
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Extract event serial (last 5 bytes)
|
|
eventSerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[11:16])
|
|
if err := eventSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
eventSerials = append(eventSerials, eventSerial)
|
|
}
|
|
return nil
|
|
})
|
|
if chk.E(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return eventSerials, nil
|
|
}
|
|
|
|
// GetEventsByAuthor finds all events authored by a pubkey.
|
|
// Uses the peg (pubkey-event-graph) index with direction filter for author edges.
|
|
// Optionally filters by event kinds.
|
|
func (d *D) GetEventsByAuthor(authorSerial *types.Uint40, kinds []uint16) ([]*types.Uint40, error) {
|
|
var eventSerials []*types.Uint40
|
|
|
|
if len(kinds) == 0 {
|
|
// No kind filter - scan for author direction across common kinds
|
|
// This is less efficient but necessary without kind filter
|
|
commonKinds := []uint16{0, 1, 3, 6, 7, 30023, 10002}
|
|
kinds = commonKinds
|
|
}
|
|
|
|
for _, k := range kinds {
|
|
kind := new(types.Uint16)
|
|
kind.Set(k)
|
|
|
|
direction := new(types.Letter)
|
|
direction.Set(types.EdgeDirectionAuthor) // Author edges
|
|
|
|
prefix := new(bytes.Buffer)
|
|
if err := indexes.PubkeyEventGraphEnc(authorSerial, kind, direction, nil).MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Key format: peg(3)|pubkey_serial(5)|kind(2)|direction(1)|event_serial(5) = 16 bytes
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Extract event serial (last 5 bytes)
|
|
eventSerial := new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[11:16])
|
|
if err := eventSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
eventSerials = append(eventSerials, eventSerial)
|
|
}
|
|
return nil
|
|
})
|
|
if chk.E(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return eventSerials, nil
|
|
}
|
|
|
|
// GetFollowsFromPubkeySerial returns the pubkey serials that a user follows.
|
|
// This extracts p-tags from the user's kind-3 contact list event.
|
|
// Returns an empty slice if no kind-3 event is found.
|
|
func (d *D) GetFollowsFromPubkeySerial(pubkeySerial *types.Uint40) ([]*types.Uint40, error) {
|
|
// Find the kind-3 event for this pubkey
|
|
contactEventSerial, err := d.FindEventByAuthorAndKind(pubkeySerial, 3)
|
|
if err != nil {
|
|
log.D.F("GetFollowsFromPubkeySerial: error finding kind-3 for serial %d: %v", pubkeySerial.Get(), err)
|
|
return nil, nil // No kind-3 event found is not an error
|
|
}
|
|
if contactEventSerial == nil {
|
|
log.T.F("GetFollowsFromPubkeySerial: no kind-3 event found for serial %d", pubkeySerial.Get())
|
|
return nil, nil
|
|
}
|
|
|
|
// Extract p-tags from the contact list event
|
|
follows, err := d.GetPTagsFromEventSerial(contactEventSerial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.T.F("GetFollowsFromPubkeySerial: found %d follows for serial %d", len(follows), pubkeySerial.Get())
|
|
return follows, nil
|
|
}
|
|
|
|
// GetFollowersOfPubkeySerial returns the pubkey serials of users who follow a given pubkey.
|
|
// This finds all kind-3 events that have a p-tag referencing the target pubkey.
|
|
func (d *D) GetFollowersOfPubkeySerial(targetSerial *types.Uint40) ([]*types.Uint40, error) {
|
|
// Find all kind-3 events that reference this pubkey via p-tag
|
|
kind3Events, err := d.GetEventsReferencingPubkey(targetSerial, []uint16{3})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract the author serials from these events
|
|
var followerSerials []*types.Uint40
|
|
seen := make(map[uint64]bool)
|
|
|
|
for _, eventSerial := range kind3Events {
|
|
// Get the author of this kind-3 event
|
|
// We need to look up the event to get its author
|
|
// Use the epg index to find the author edge
|
|
authorSerial, err := d.GetEventAuthorSerial(eventSerial)
|
|
if err != nil {
|
|
log.D.F("GetFollowersOfPubkeySerial: couldn't get author for event %d: %v", eventSerial.Get(), err)
|
|
continue
|
|
}
|
|
|
|
// Deduplicate (a user might have multiple kind-3 events)
|
|
if seen[authorSerial.Get()] {
|
|
continue
|
|
}
|
|
seen[authorSerial.Get()] = true
|
|
followerSerials = append(followerSerials, authorSerial)
|
|
}
|
|
|
|
log.T.F("GetFollowersOfPubkeySerial: found %d followers for serial %d", len(followerSerials), targetSerial.Get())
|
|
return followerSerials, nil
|
|
}
|
|
|
|
// GetEventAuthorSerial finds the author pubkey serial for an event.
|
|
// Uses the epg (event-pubkey-graph) index with author direction.
|
|
func (d *D) GetEventAuthorSerial(eventSerial *types.Uint40) (*types.Uint40, error) {
|
|
var authorSerial *types.Uint40
|
|
|
|
// Build prefix: epg|event_serial
|
|
prefix := new(bytes.Buffer)
|
|
prefix.Write([]byte(indexes.EventPubkeyGraphPrefix))
|
|
if err := eventSerial.MarshalWrite(prefix); chk.E(err) {
|
|
return nil, err
|
|
}
|
|
searchPrefix := prefix.Bytes()
|
|
|
|
err := d.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = searchPrefix
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek(searchPrefix); it.ValidForPrefix(searchPrefix); it.Next() {
|
|
key := it.Item().KeyCopy(nil)
|
|
|
|
// Decode key: epg(3)|event_serial(5)|pubkey_serial(5)|kind(2)|direction(1)
|
|
if len(key) != 16 {
|
|
continue
|
|
}
|
|
|
|
// Check direction - we want author (0)
|
|
direction := key[15]
|
|
if direction != types.EdgeDirectionAuthor {
|
|
continue
|
|
}
|
|
|
|
// Extract pubkey serial (bytes 8-12)
|
|
authorSerial = new(types.Uint40)
|
|
serialReader := bytes.NewReader(key[8:13])
|
|
if err := authorSerial.UnmarshalRead(serialReader); chk.E(err) {
|
|
continue
|
|
}
|
|
|
|
return nil // Found the author
|
|
}
|
|
return ErrEventNotFound
|
|
})
|
|
|
|
return authorSerial, err
|
|
}
|
|
|
|
// PubkeyHexToSerial converts a pubkey hex string to its serial, if it exists.
|
|
// Returns an error if the pubkey is not in the database.
|
|
func (d *D) PubkeyHexToSerial(pubkeyHex string) (*types.Uint40, error) {
|
|
pubkeyBytes, err := hex.Dec(pubkeyHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pubkeyBytes) != 32 {
|
|
return nil, errors.New("invalid pubkey length")
|
|
}
|
|
return d.GetPubkeySerial(pubkeyBytes)
|
|
}
|
|
|
|
// EventIDHexToSerial converts an event ID hex string to its serial, if it exists.
|
|
// Returns an error if the event is not in the database.
|
|
func (d *D) EventIDHexToSerial(eventIDHex string) (*types.Uint40, error) {
|
|
eventIDBytes, err := hex.Dec(eventIDHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(eventIDBytes) != 32 {
|
|
return nil, errors.New("invalid event ID length")
|
|
}
|
|
return d.GetSerialById(eventIDBytes)
|
|
}
|