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.
614 lines
16 KiB
614 lines
16 KiB
//go:build js && wasm |
|
|
|
package wasmdb |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"fmt" |
|
"sort" |
|
"strconv" |
|
"time" |
|
|
|
"github.com/aperturerobotics/go-indexeddb/idb" |
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/errorf" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
hexenc "git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/ints" |
|
"git.mleku.dev/mleku/nostr/encoders/kind" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/encoders/tag/atag" |
|
"next.orly.dev/pkg/database" |
|
"next.orly.dev/pkg/database/indexes" |
|
"next.orly.dev/pkg/database/indexes/types" |
|
"next.orly.dev/pkg/interfaces/store" |
|
"next.orly.dev/pkg/utils" |
|
) |
|
|
|
// DeleteEvent removes an event from the database identified by `eid`. |
|
func (w *W) DeleteEvent(c context.Context, eid []byte) (err error) { |
|
w.Logger.Warnf("deleting event %0x", eid) |
|
|
|
// Get the serial number for the event ID |
|
var ser *types.Uint40 |
|
ser, err = w.GetSerialById(eid) |
|
if chk.E(err) { |
|
return |
|
} |
|
if ser == nil { |
|
// Event wasn't found, nothing to delete |
|
return |
|
} |
|
|
|
// Fetch the event to get its data |
|
var ev *event.E |
|
ev, err = w.FetchEventBySerial(ser) |
|
if chk.E(err) { |
|
return |
|
} |
|
if ev == nil { |
|
// Event wasn't found, nothing to delete |
|
return |
|
} |
|
|
|
if err = w.DeleteEventBySerial(c, ser, ev); chk.E(err) { |
|
return |
|
} |
|
return |
|
} |
|
|
|
// DeleteEventBySerial removes an event and all its indexes by serial number. |
|
func (w *W) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) (err error) { |
|
w.Logger.Infof("DeleteEventBySerial: deleting event %0x (serial %d)", ev.ID, ser.Get()) |
|
|
|
// Get all indexes for the event |
|
var idxs [][]byte |
|
idxs, err = database.GetIndexesForEvent(ev, ser.Get()) |
|
if chk.E(err) { |
|
w.Logger.Errorf("DeleteEventBySerial: failed to get indexes for event %0x: %v", ev.ID, err) |
|
return |
|
} |
|
w.Logger.Infof("DeleteEventBySerial: found %d indexes for event %0x", len(idxs), ev.ID) |
|
|
|
// Collect all unique store names we need to access |
|
storeNames := make(map[string]struct{}) |
|
for _, key := range idxs { |
|
if len(key) >= 3 { |
|
storeNames[string(key[:3])] = struct{}{} |
|
} |
|
} |
|
|
|
// Also include event stores |
|
storeNames[string(indexes.EventPrefix)] = struct{}{} |
|
storeNames[string(indexes.SmallEventPrefix)] = struct{}{} |
|
|
|
// Convert to slice |
|
storeList := make([]string, 0, len(storeNames)) |
|
for name := range storeNames { |
|
storeList = append(storeList, name) |
|
} |
|
|
|
if len(storeList) == 0 { |
|
return nil |
|
} |
|
|
|
// Start a transaction to delete the event and all its indexes |
|
tx, err := w.db.Transaction(idb.TransactionReadWrite, storeList[0], storeList[1:]...) |
|
if err != nil { |
|
return fmt.Errorf("failed to start delete transaction: %w", err) |
|
} |
|
|
|
// Delete all indexes |
|
for _, key := range idxs { |
|
if len(key) < 3 { |
|
continue |
|
} |
|
storeName := string(key[:3]) |
|
objStore, storeErr := tx.ObjectStore(storeName) |
|
if storeErr != nil { |
|
w.Logger.Warnf("DeleteEventBySerial: failed to get object store %s: %v", storeName, storeErr) |
|
continue |
|
} |
|
|
|
keyJS := bytesToSafeValue(key) |
|
if _, delErr := objStore.Delete(keyJS); delErr != nil { |
|
w.Logger.Warnf("DeleteEventBySerial: failed to delete index from %s: %v", storeName, delErr) |
|
} |
|
} |
|
|
|
// Delete from small event store |
|
sevKeyBuf := new(bytes.Buffer) |
|
if err = indexes.SmallEventEnc(ser).MarshalWrite(sevKeyBuf); err == nil { |
|
if objStore, storeErr := tx.ObjectStore(string(indexes.SmallEventPrefix)); storeErr == nil { |
|
// For small events, the key includes size and data, so we need to scan |
|
w.deleteKeysByPrefix(objStore, sevKeyBuf.Bytes()) |
|
} |
|
} |
|
|
|
// Delete from large event store |
|
evtKeyBuf := new(bytes.Buffer) |
|
if err = indexes.EventEnc(ser).MarshalWrite(evtKeyBuf); err == nil { |
|
if objStore, storeErr := tx.ObjectStore(string(indexes.EventPrefix)); storeErr == nil { |
|
keyJS := bytesToSafeValue(evtKeyBuf.Bytes()) |
|
objStore.Delete(keyJS) |
|
} |
|
} |
|
|
|
// Commit transaction |
|
if err = tx.Await(c); err != nil { |
|
return fmt.Errorf("failed to commit delete transaction: %w", err) |
|
} |
|
|
|
w.Logger.Infof("DeleteEventBySerial: successfully deleted event %0x and all indexes", ev.ID) |
|
return nil |
|
} |
|
|
|
// deleteKeysByPrefix deletes all keys starting with the given prefix from an object store |
|
func (w *W) deleteKeysByPrefix(store *idb.ObjectStore, prefix []byte) { |
|
cursorReq, err := store.OpenCursor(idb.CursorNext) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var keysToDelete [][]byte |
|
cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error { |
|
keyVal, keyErr := cursor.Key() |
|
if keyErr != nil { |
|
return keyErr |
|
} |
|
|
|
keyBytes := safeValueToBytes(keyVal) |
|
if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) { |
|
keysToDelete = append(keysToDelete, keyBytes) |
|
} |
|
|
|
return cursor.Continue() |
|
}) |
|
|
|
// Delete collected keys |
|
for _, key := range keysToDelete { |
|
keyJS := bytesToSafeValue(key) |
|
store.Delete(keyJS) |
|
} |
|
} |
|
|
|
// DeleteExpired scans for events with expiration timestamps that have passed and deletes them. |
|
func (w *W) DeleteExpired() { |
|
now := time.Now().Unix() |
|
|
|
// Open read transaction to find expired events |
|
tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.ExpirationPrefix)) |
|
if err != nil { |
|
w.Logger.Warnf("DeleteExpired: failed to start transaction: %v", err) |
|
return |
|
} |
|
|
|
objStore, err := tx.ObjectStore(string(indexes.ExpirationPrefix)) |
|
if err != nil { |
|
w.Logger.Warnf("DeleteExpired: failed to get expiration store: %v", err) |
|
return |
|
} |
|
|
|
var expiredSerials types.Uint40s |
|
|
|
cursorReq, err := objStore.OpenCursor(idb.CursorNext) |
|
if err != nil { |
|
w.Logger.Warnf("DeleteExpired: failed to open cursor: %v", err) |
|
return |
|
} |
|
|
|
cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error { |
|
keyVal, keyErr := cursor.Key() |
|
if keyErr != nil { |
|
return keyErr |
|
} |
|
|
|
keyBytes := safeValueToBytes(keyVal) |
|
if len(keyBytes) < 8 { // exp prefix (3) + expiration (variable) + serial (5) |
|
return cursor.Continue() |
|
} |
|
|
|
// Parse expiration key: exp|expiration_timestamp|serial |
|
exp, ser := indexes.ExpirationVars() |
|
buf := bytes.NewBuffer(keyBytes) |
|
if err := indexes.ExpirationDec(exp, ser).UnmarshalRead(buf); err != nil { |
|
return cursor.Continue() |
|
} |
|
|
|
if int64(exp.Get()) > now { |
|
// Not expired yet |
|
return cursor.Continue() |
|
} |
|
|
|
expiredSerials = append(expiredSerials, ser) |
|
return cursor.Continue() |
|
}) |
|
|
|
// Delete expired events |
|
for _, ser := range expiredSerials { |
|
ev, fetchErr := w.FetchEventBySerial(ser) |
|
if fetchErr != nil || ev == nil { |
|
continue |
|
} |
|
if err := w.DeleteEventBySerial(context.Background(), ser, ev); err != nil { |
|
w.Logger.Warnf("DeleteExpired: failed to delete expired event: %v", err) |
|
} |
|
} |
|
} |
|
|
|
// ProcessDelete processes a kind 5 deletion event, deleting referenced events. |
|
func (w *W) ProcessDelete(ev *event.E, admins [][]byte) (err error) { |
|
eTags := ev.Tags.GetAll([]byte("e")) |
|
aTags := ev.Tags.GetAll([]byte("a")) |
|
kTags := ev.Tags.GetAll([]byte("k")) |
|
|
|
// Process e-tags: delete specific events by ID |
|
for _, eTag := range eTags { |
|
if eTag.Len() < 2 { |
|
continue |
|
} |
|
// Use ValueHex() to handle both binary and hex storage formats |
|
eventIdHex := eTag.ValueHex() |
|
if len(eventIdHex) != 64 { // hex encoded event ID |
|
continue |
|
} |
|
// Decode hex event ID |
|
var eid []byte |
|
if eid, err = hexenc.DecAppend(nil, eventIdHex); chk.E(err) { |
|
continue |
|
} |
|
// Fetch the event to verify ownership |
|
var ser *types.Uint40 |
|
if ser, err = w.GetSerialById(eid); chk.E(err) || ser == nil { |
|
continue |
|
} |
|
var targetEv *event.E |
|
if targetEv, err = w.FetchEventBySerial(ser); chk.E(err) || targetEv == nil { |
|
continue |
|
} |
|
// Only allow users to delete their own events |
|
if !utils.FastEqual(targetEv.Pubkey, ev.Pubkey) { |
|
continue |
|
} |
|
// Delete the event |
|
if err = w.DeleteEvent(context.Background(), eid); chk.E(err) { |
|
w.Logger.Warnf("failed to delete event %x via e-tag: %v", eid, err) |
|
continue |
|
} |
|
w.Logger.Debugf("deleted event %x via e-tag deletion", eid) |
|
} |
|
|
|
// Process a-tags: delete addressable events by kind:pubkey:d-tag |
|
for _, aTag := range aTags { |
|
if aTag.Len() < 2 { |
|
continue |
|
} |
|
// Parse the 'a' tag value: kind:pubkey:d-tag (for parameterized) or kind:pubkey (for regular) |
|
split := bytes.Split(aTag.Value(), []byte{':'}) |
|
if len(split) < 2 { |
|
continue |
|
} |
|
// Parse the kind |
|
kindStr := string(split[0]) |
|
kindInt, parseErr := strconv.Atoi(kindStr) |
|
if parseErr != nil { |
|
continue |
|
} |
|
kk := kind.New(uint16(kindInt)) |
|
// Parse the pubkey |
|
var pk []byte |
|
if pk, err = hexenc.DecAppend(nil, split[1]); chk.E(err) { |
|
continue |
|
} |
|
// Only allow users to delete their own events |
|
if !utils.FastEqual(pk, ev.Pubkey) { |
|
continue |
|
} |
|
|
|
// Build filter for events to delete |
|
delFilter := &filter.F{ |
|
Authors: tag.NewFromBytesSlice(pk), |
|
Kinds: kind.NewS(kk), |
|
} |
|
|
|
// For parameterized replaceable events, add d-tag filter |
|
if kind.IsParameterizedReplaceable(kk.K) && len(split) >= 3 { |
|
dValue := split[2] |
|
delFilter.Tags = tag.NewS(tag.NewFromAny([]byte("d"), dValue)) |
|
} |
|
|
|
// Find matching events |
|
var idxs []database.Range |
|
if idxs, err = database.GetIndexesFromFilter(delFilter); chk.E(err) { |
|
continue |
|
} |
|
var sers types.Uint40s |
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
continue |
|
} |
|
sers = append(sers, s...) |
|
} |
|
|
|
// Delete events older than the deletion event |
|
if len(sers) > 0 { |
|
var idPkTss []*store.IdPkTs |
|
var tmp []*store.IdPkTs |
|
if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) { |
|
continue |
|
} |
|
idPkTss = append(idPkTss, tmp...) |
|
// Sort by timestamp |
|
sort.Slice(idPkTss, func(i, j int) bool { |
|
return idPkTss[i].Ts > idPkTss[j].Ts |
|
}) |
|
for _, v := range idPkTss { |
|
if v.Ts < ev.CreatedAt { |
|
if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) { |
|
w.Logger.Warnf("failed to delete event %x via a-tag: %v", v.Id, err) |
|
continue |
|
} |
|
w.Logger.Debugf("deleted event %x via a-tag deletion", v.Id) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// If there are no e or a tags, delete all replaceable events of the kinds |
|
// specified by the k tags for the pubkey of the delete event. |
|
if len(eTags) == 0 && len(aTags) == 0 { |
|
// Parse the kind tags |
|
var kinds []*kind.K |
|
for _, k := range kTags { |
|
kv := k.Value() |
|
iv := ints.New(0) |
|
if _, err = iv.Unmarshal(kv); chk.E(err) { |
|
continue |
|
} |
|
kinds = append(kinds, kind.New(iv.N)) |
|
} |
|
|
|
var idxs []database.Range |
|
if idxs, err = database.GetIndexesFromFilter( |
|
&filter.F{ |
|
Authors: tag.NewFromBytesSlice(ev.Pubkey), |
|
Kinds: kind.NewS(kinds...), |
|
}, |
|
); chk.E(err) { |
|
return |
|
} |
|
|
|
var sers types.Uint40s |
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
return |
|
} |
|
sers = append(sers, s...) |
|
} |
|
|
|
if len(sers) > 0 { |
|
var idPkTss []*store.IdPkTs |
|
var tmp []*store.IdPkTs |
|
if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) { |
|
return |
|
} |
|
idPkTss = append(idPkTss, tmp...) |
|
// Sort by timestamp |
|
sort.Slice(idPkTss, func(i, j int) bool { |
|
return idPkTss[i].Ts > idPkTss[j].Ts |
|
}) |
|
for _, v := range idPkTss { |
|
if v.Ts < ev.CreatedAt { |
|
if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) { |
|
continue |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return |
|
} |
|
|
|
// CheckForDeleted checks if the event has been deleted, and returns an error with |
|
// prefix "blocked:" if it is. This function also allows designating admin |
|
// pubkeys that may also delete the event. |
|
func (w *W) CheckForDeleted(ev *event.E, admins [][]byte) (err error) { |
|
keys := append([][]byte{ev.Pubkey}, admins...) |
|
authors := tag.NewFromBytesSlice(keys...) |
|
|
|
// If the event is addressable, check for a deletion event with the same |
|
// kind/pubkey/dtag |
|
if kind.IsParameterizedReplaceable(ev.Kind) { |
|
var idxs []database.Range |
|
// Construct an a-tag |
|
t := ev.Tags.GetFirst([]byte("d")) |
|
var dTagValue []byte |
|
if t != nil { |
|
dTagValue = t.Value() |
|
} |
|
a := atag.T{ |
|
Kind: kind.New(ev.Kind), |
|
Pubkey: ev.Pubkey, |
|
DTag: dTagValue, |
|
} |
|
at := a.Marshal(nil) |
|
if idxs, err = database.GetIndexesFromFilter( |
|
&filter.F{ |
|
Authors: authors, |
|
Kinds: kind.NewS(kind.Deletion), |
|
Tags: tag.NewS(tag.NewFromAny("#a", at)), |
|
}, |
|
); chk.E(err) { |
|
return |
|
} |
|
|
|
var sers types.Uint40s |
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
return |
|
} |
|
sers = append(sers, s...) |
|
} |
|
|
|
if len(sers) > 0 { |
|
var idPkTss []*store.IdPkTs |
|
var tmp []*store.IdPkTs |
|
if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) { |
|
return |
|
} |
|
idPkTss = append(idPkTss, tmp...) |
|
// Find the newest deletion timestamp |
|
maxTs := idPkTss[0].Ts |
|
for i := 1; i < len(idPkTss); i++ { |
|
if idPkTss[i].Ts > maxTs { |
|
maxTs = idPkTss[i].Ts |
|
} |
|
} |
|
if ev.CreatedAt < maxTs { |
|
err = errorf.E( |
|
"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d", |
|
ev.ID, at, ev.CreatedAt, maxTs, |
|
) |
|
return |
|
} |
|
return |
|
} |
|
return |
|
} |
|
|
|
// If the event is replaceable, check if there is a deletion event newer |
|
// than the event |
|
if kind.IsReplaceable(ev.Kind) { |
|
var idxs []database.Range |
|
if idxs, err = database.GetIndexesFromFilter( |
|
&filter.F{ |
|
Authors: tag.NewFromBytesSlice(ev.Pubkey), |
|
Kinds: kind.NewS(kind.Deletion), |
|
Tags: tag.NewS( |
|
tag.NewFromAny("#k", fmt.Sprint(ev.Kind)), |
|
), |
|
}, |
|
); chk.E(err) { |
|
return |
|
} |
|
|
|
var sers types.Uint40s |
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
return |
|
} |
|
sers = append(sers, s...) |
|
} |
|
|
|
if len(sers) > 0 { |
|
var idPkTss []*store.IdPkTs |
|
var tmp []*store.IdPkTs |
|
if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) { |
|
return |
|
} |
|
idPkTss = append(idPkTss, tmp...) |
|
// Find the newest deletion |
|
maxTs := idPkTss[0].Ts |
|
maxId := idPkTss[0].Id |
|
for i := 1; i < len(idPkTss); i++ { |
|
if idPkTss[i].Ts > maxTs { |
|
maxTs = idPkTss[i].Ts |
|
maxId = idPkTss[i].Id |
|
} |
|
} |
|
if ev.CreatedAt < maxTs { |
|
err = fmt.Errorf( |
|
"blocked: %0x was deleted: the event is older than the delete event %0x: event: %d delete: %d", |
|
ev.ID, maxId, ev.CreatedAt, maxTs, |
|
) |
|
return |
|
} |
|
} |
|
|
|
// This type of delete can also use an a tag to specify kind and author |
|
idxs = nil |
|
a := atag.T{ |
|
Kind: kind.New(ev.Kind), |
|
Pubkey: ev.Pubkey, |
|
} |
|
at := a.Marshal(nil) |
|
if idxs, err = database.GetIndexesFromFilter( |
|
&filter.F{ |
|
Authors: authors, |
|
Kinds: kind.NewS(kind.Deletion), |
|
Tags: tag.NewS(tag.NewFromAny("#a", at)), |
|
}, |
|
); chk.E(err) { |
|
return |
|
} |
|
|
|
sers = nil |
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
return |
|
} |
|
sers = append(sers, s...) |
|
} |
|
|
|
if len(sers) > 0 { |
|
var idPkTss []*store.IdPkTs |
|
var tmp []*store.IdPkTs |
|
if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) { |
|
return |
|
} |
|
idPkTss = append(idPkTss, tmp...) |
|
// Find the newest deletion |
|
maxTs := idPkTss[0].Ts |
|
for i := 1; i < len(idPkTss); i++ { |
|
if idPkTss[i].Ts > maxTs { |
|
maxTs = idPkTss[i].Ts |
|
} |
|
} |
|
if ev.CreatedAt < maxTs { |
|
err = errorf.E( |
|
"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d", |
|
ev.ID, at, ev.CreatedAt, maxTs, |
|
) |
|
return |
|
} |
|
return |
|
} |
|
return |
|
} |
|
|
|
// Otherwise check for a delete by event id |
|
var idxs []database.Range |
|
if idxs, err = database.GetIndexesFromFilter( |
|
&filter.F{ |
|
Authors: authors, |
|
Kinds: kind.NewS(kind.Deletion), |
|
Tags: tag.NewS( |
|
tag.NewFromAny("e", hexenc.Enc(ev.ID)), |
|
), |
|
}, |
|
); chk.E(err) { |
|
return |
|
} |
|
|
|
for _, idx := range idxs { |
|
var s types.Uint40s |
|
if s, err = w.GetSerialsByRange(idx); chk.E(err) { |
|
return |
|
} |
|
if len(s) > 0 { |
|
// Any e-tag deletion found means the exact event was deleted |
|
err = errorf.E("blocked: %0x has been deleted", ev.ID) |
|
return |
|
} |
|
} |
|
|
|
return |
|
}
|
|
|