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 \"Event\" .\n", eventID)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", eventID, eventID)) nquads.WriteString(fmt.Sprintf("_:%s \"%d\"^^ .\n", eventID, serial)) nquads.WriteString(fmt.Sprintf("_:%s \"%d\"^^ .\n", eventID, ev.Kind)) nquads.WriteString(fmt.Sprintf("_:%s \"%d\"^^ .\n", eventID, int64(ev.CreatedAt))) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", eventID, ev.Content)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", eventID, hex.Enc(ev.Sig[:]))) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", eventID, authorPubkey)) // Serialize tags as JSON string for storage tagsJSON, _ := json.Marshal(ev.Tags) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", eventID, string(tagsJSON))) // Author relationship nquads.WriteString(fmt.Sprintf("_:%s _:%s .\n", eventID, authorPubkey)) nquads.WriteString(fmt.Sprintf("_:%s \"Author\" .\n", authorPubkey)) nquads.WriteString(fmt.Sprintf("_:%s %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 _:%s .\n", eventID, tagValue)) case "p": // Pubkey mention nquads.WriteString(fmt.Sprintf("_:%s _:%s .\n", eventID, tagValue)) // Ensure mentioned author exists nquads.WriteString(fmt.Sprintf("_:%s \"Author\" .\n", tagValue)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", tagValue, tagValue)) case "t": // Hashtag tagID := "tag_" + tagType + "_" + tagValue nquads.WriteString(fmt.Sprintf("_:%s _:%s .\n", eventID, tagID)) nquads.WriteString(fmt.Sprintf("_:%s \"Tag\" .\n", tagID)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", tagID, tagType)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", tagID, tagValue)) default: // Store other tag types tagID := "tag_" + tagType + "_" + tagValue nquads.WriteString(fmt.Sprintf("_:%s _:%s .\n", eventID, tagID)) nquads.WriteString(fmt.Sprintf("_:%s \"Tag\" .\n", tagID)) nquads.WriteString(fmt.Sprintf("_:%s %q .\n", tagID, tagType)) nquads.WriteString(fmt.Sprintf("_:%s %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 }