184 lines
6.0 KiB
Go
184 lines
6.0 KiB
Go
package dgraph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/dgraph-io/dgo/v230/protos/api"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
|
"git.mleku.dev/mleku/nostr/encoders/filter"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
)
|
|
|
|
// SaveEvent stores a Nostr event in the Dgraph database.
|
|
// It creates event nodes and relationships for authors, tags, and references.
|
|
func (d *D) SaveEvent(c context.Context, ev *event.E) (exists bool, err error) {
|
|
eventID := hex.Enc(ev.ID[:])
|
|
|
|
// Check if event already exists
|
|
query := fmt.Sprintf(`{
|
|
event(func: eq(event.id, %q)) {
|
|
uid
|
|
event.id
|
|
}
|
|
}`, eventID)
|
|
|
|
resp, err := d.Query(c, query)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check event existence: %w", err)
|
|
}
|
|
|
|
// Parse response to check if event exists
|
|
var result struct {
|
|
Event []map[string]interface{} `json:"event"`
|
|
}
|
|
if err = json.Unmarshal(resp.Json, &result); err != nil {
|
|
return false, fmt.Errorf("failed to parse query response: %w", err)
|
|
}
|
|
|
|
if len(result.Event) > 0 {
|
|
return true, nil // Event already exists
|
|
}
|
|
|
|
// Get next serial number
|
|
serial, err := d.getNextSerial()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get serial number: %w", err)
|
|
}
|
|
|
|
// Build N-Quads for the event with serial number
|
|
nquads := d.buildEventNQuads(ev, serial)
|
|
|
|
// Store the event
|
|
mutation := &api.Mutation{
|
|
SetNquads: []byte(nquads),
|
|
CommitNow: true,
|
|
}
|
|
|
|
if _, err = d.Mutate(c, mutation); err != nil {
|
|
return false, fmt.Errorf("failed to save event: %w", err)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// buildEventNQuads constructs RDF triples for a Nostr event
|
|
func (d *D) buildEventNQuads(ev *event.E, serial uint64) string {
|
|
var nquads strings.Builder
|
|
|
|
eventID := hex.Enc(ev.ID[:])
|
|
authorPubkey := hex.Enc(ev.Pubkey)
|
|
|
|
// Event node
|
|
nquads.WriteString(fmt.Sprintf("_:%s <dgraph.type> \"Event\" .\n", eventID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.id> %q .\n", eventID, eventID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.serial> \"%d\"^^<xs:int> .\n", eventID, serial))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.kind> \"%d\"^^<xs:int> .\n", eventID, ev.Kind))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.created_at> \"%d\"^^<xs:int> .\n", eventID, int64(ev.CreatedAt)))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.content> %q .\n", eventID, ev.Content))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.sig> %q .\n", eventID, hex.Enc(ev.Sig[:])))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.pubkey> %q .\n", eventID, authorPubkey))
|
|
|
|
// Serialize tags as JSON string for storage
|
|
tagsJSON, _ := json.Marshal(ev.Tags)
|
|
nquads.WriteString(fmt.Sprintf("_:%s <event.tags> %q .\n", eventID, string(tagsJSON)))
|
|
|
|
// Author relationship
|
|
nquads.WriteString(fmt.Sprintf("_:%s <authored_by> _:%s .\n", eventID, authorPubkey))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <dgraph.type> \"Author\" .\n", authorPubkey))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <author.pubkey> %q .\n", authorPubkey, authorPubkey))
|
|
|
|
// Tag relationships
|
|
for _, tag := range *ev.Tags {
|
|
if len(tag.T) >= 2 {
|
|
tagType := string(tag.T[0])
|
|
tagValue := string(tag.T[1])
|
|
|
|
switch tagType {
|
|
case "e": // Event reference
|
|
nquads.WriteString(fmt.Sprintf("_:%s <references> _:%s .\n", eventID, tagValue))
|
|
case "p": // Pubkey mention
|
|
nquads.WriteString(fmt.Sprintf("_:%s <mentions> _:%s .\n", eventID, tagValue))
|
|
// Ensure mentioned author exists
|
|
nquads.WriteString(fmt.Sprintf("_:%s <dgraph.type> \"Author\" .\n", tagValue))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <author.pubkey> %q .\n", tagValue, tagValue))
|
|
case "t": // Hashtag
|
|
tagID := "tag_" + tagType + "_" + tagValue
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tagged_with> _:%s .\n", eventID, tagID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <dgraph.type> \"Tag\" .\n", tagID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tag.type> %q .\n", tagID, tagType))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tag.value> %q .\n", tagID, tagValue))
|
|
default:
|
|
// Store other tag types
|
|
tagID := "tag_" + tagType + "_" + tagValue
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tagged_with> _:%s .\n", eventID, tagID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <dgraph.type> \"Tag\" .\n", tagID))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tag.type> %q .\n", tagID, tagType))
|
|
nquads.WriteString(fmt.Sprintf("_:%s <tag.value> %q .\n", tagID, tagValue))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nquads.String()
|
|
}
|
|
|
|
// GetSerialsFromFilter returns event serials matching a filter
|
|
func (d *D) GetSerialsFromFilter(f *filter.F) (serials types.Uint40s, err error) {
|
|
// Use QueryForSerials which already implements the proper filter logic
|
|
return d.QueryForSerials(context.Background(), f)
|
|
}
|
|
|
|
// WouldReplaceEvent checks if an event would replace existing events
|
|
func (d *D) WouldReplaceEvent(ev *event.E) (bool, types.Uint40s, error) {
|
|
// Check for replaceable events (kinds 0, 3, and 10000-19999)
|
|
isReplaceable := ev.Kind == 0 || ev.Kind == 3 || (ev.Kind >= 10000 && ev.Kind < 20000)
|
|
if !isReplaceable {
|
|
return false, nil, nil
|
|
}
|
|
|
|
// Query for existing events with same kind and pubkey
|
|
authorPubkey := hex.Enc(ev.Pubkey)
|
|
query := fmt.Sprintf(`{
|
|
events(func: eq(event.pubkey, %q)) @filter(eq(event.kind, %d)) {
|
|
uid
|
|
event.serial
|
|
event.created_at
|
|
}
|
|
}`, authorPubkey, ev.Kind)
|
|
|
|
resp, err := d.Query(context.Background(), query)
|
|
if err != nil {
|
|
return false, nil, fmt.Errorf("failed to query replaceable events: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
Events []struct {
|
|
UID string `json:"uid"`
|
|
Serial int64 `json:"event.serial"`
|
|
CreatedAt int64 `json:"event.created_at"`
|
|
} `json:"events"`
|
|
}
|
|
if err = json.Unmarshal(resp.Json, &result); err != nil {
|
|
return false, nil, fmt.Errorf("failed to parse query response: %w", err)
|
|
}
|
|
|
|
// Check if our event is newer
|
|
evTime := int64(ev.CreatedAt)
|
|
var serials types.Uint40s
|
|
wouldReplace := false
|
|
|
|
for _, existing := range result.Events {
|
|
if existing.CreatedAt < evTime {
|
|
wouldReplace = true
|
|
serial := types.Uint40{}
|
|
serial.Set(uint64(existing.Serial))
|
|
serials = append(serials, &serial)
|
|
}
|
|
}
|
|
|
|
return wouldReplace, serials, nil
|
|
}
|