package dgraph import ( "context" "encoding/json" "fmt" "time" "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/hex" ) // DeleteEvent deletes an event by its ID func (d *D) DeleteEvent(c context.Context, eid []byte) error { idStr := hex.Enc(eid) // Find the event's UID query := fmt.Sprintf(`{ event(func: eq(event.id, %q)) { uid } }`, idStr) resp, err := d.Query(c, query) if err != nil { return fmt.Errorf("failed to find event for deletion: %w", err) } // Parse UID var result struct { Event []struct { UID string `json:"uid"` } `json:"event"` } if err = unmarshalJSON(resp.Json, &result); err != nil { return err } if len(result.Event) == 0 { return nil // Event doesn't exist } // Delete the event node mutation := &api.Mutation{ DelNquads: []byte(fmt.Sprintf("<%s> * * .", result.Event[0].UID)), CommitNow: true, } if _, err = d.Mutate(c, mutation); err != nil { return fmt.Errorf("failed to delete event: %w", err) } return nil } // DeleteEventBySerial deletes an event by its serial number func (d *D) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) error { serial := ser.Get() // Find the event's UID query := fmt.Sprintf(`{ event(func: eq(event.serial, %d)) { uid } }`, serial) resp, err := d.Query(c, query) if err != nil { return fmt.Errorf("failed to find event for deletion: %w", err) } // Parse UID var result struct { Event []struct { UID string `json:"uid"` } `json:"event"` } if err = unmarshalJSON(resp.Json, &result); err != nil { return err } if len(result.Event) == 0 { return nil // Event doesn't exist } // Delete the event node mutation := &api.Mutation{ DelNquads: []byte(fmt.Sprintf("<%s> * * .", result.Event[0].UID)), CommitNow: true, } if _, err = d.Mutate(c, mutation); err != nil { return fmt.Errorf("failed to delete event: %w", err) } return nil } // DeleteExpired removes events that have passed their expiration time (NIP-40) func (d *D) DeleteExpired() { // Query for events that have an "expiration" tag // NIP-40: events should have a tag ["expiration", ""] query := `{ events(func: has(event.tags)) { uid event.id event.tags event.created_at } }` resp, err := d.Query(context.Background(), query) if err != nil { d.Logger.Errorf("failed to query events for expiration: %v", err) return } var result struct { Events []struct { UID string `json:"uid"` ID string `json:"event.id"` Tags string `json:"event.tags"` CreatedAt int64 `json:"event.created_at"` } `json:"events"` } if err = unmarshalJSON(resp.Json, &result); err != nil { d.Logger.Errorf("failed to parse events for expiration: %v", err) return } now := time.Now().Unix() deletedCount := 0 for _, ev := range result.Events { // Parse tags if ev.Tags == "" { continue } var tags [][]string if err := json.Unmarshal([]byte(ev.Tags), &tags); err != nil { continue } // Look for expiration tag var expirationTime int64 for _, tag := range tags { if len(tag) >= 2 && tag[0] == "expiration" { // Parse expiration timestamp if _, err := fmt.Sscanf(tag[1], "%d", &expirationTime); err != nil { continue } break } } // If expiration time found and passed, delete the event if expirationTime > 0 && now > expirationTime { mutation := &api.Mutation{ DelNquads: []byte(fmt.Sprintf("<%s> * * .", ev.UID)), CommitNow: true, } if _, err := d.Mutate(context.Background(), mutation); err != nil { d.Logger.Warningf("failed to delete expired event %s: %v", ev.ID, err) } else { deletedCount++ } } } if deletedCount > 0 { d.Logger.Infof("deleted %d expired events", deletedCount) } } // ProcessDelete processes a kind 5 deletion event func (d *D) ProcessDelete(ev *event.E, admins [][]byte) (err error) { if ev.Kind != 5 { return fmt.Errorf("event is not a deletion event (kind 5)") } // Extract event IDs to delete from tags for _, tag := range *ev.Tags { if len(tag.T) >= 2 && string(tag.T[0]) == "e" { eventID := tag.T[1] // Verify the deletion is authorized (author must match or be admin) if err = d.CheckForDeleted(ev, admins); err != nil { continue } // Delete the event if err = d.DeleteEvent(context.Background(), eventID); err != nil { // Log error but continue with other deletions d.Logger.Errorf("failed to delete event %s: %v", hex.Enc(eventID), err) } } } return nil } // CheckForDeleted checks if an event has been deleted func (d *D) CheckForDeleted(ev *event.E, admins [][]byte) (err error) { // Query for delete events (kind 5) that reference this event evID := hex.Enc(ev.ID[:]) query := fmt.Sprintf(`{ deletes(func: eq(event.kind, 5)) @filter(eq(event.pubkey, %q)) { uid event.pubkey references @filter(eq(event.id, %q)) { event.id } } }`, hex.Enc(ev.Pubkey), evID) resp, err := d.Query(context.Background(), query) if err != nil { return fmt.Errorf("failed to check for deletions: %w", err) } var result struct { Deletes []struct { UID string `json:"uid"` Pubkey string `json:"event.pubkey"` References []struct { ID string `json:"event.id"` } `json:"references"` } `json:"deletes"` } if err = unmarshalJSON(resp.Json, &result); err != nil { return err } // Check if any delete events reference this event for _, del := range result.Deletes { if len(del.References) > 0 { // Check if deletion is from the author or an admin delPubkey, _ := hex.Dec(del.Pubkey) if string(delPubkey) == string(ev.Pubkey) { return fmt.Errorf("event has been deleted by author") } // Check admins for _, admin := range admins { if string(delPubkey) == string(admin) { return fmt.Errorf("event has been deleted by admin") } } } } return nil }