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.
560 lines
16 KiB
560 lines
16 KiB
//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) |
|
}
|
|
|