delete-event.go raw

   1  //go:build js && wasm
   2  
   3  package wasmdb
   4  
   5  import (
   6  	"bytes"
   7  	"context"
   8  	"fmt"
   9  	"sort"
  10  	"strconv"
  11  	"time"
  12  
  13  	"github.com/aperturerobotics/go-indexeddb/idb"
  14  	"next.orly.dev/pkg/lol/chk"
  15  	"next.orly.dev/pkg/lol/errorf"
  16  
  17  	"next.orly.dev/pkg/nostr/encoders/event"
  18  	"next.orly.dev/pkg/nostr/encoders/filter"
  19  	hexenc "next.orly.dev/pkg/nostr/encoders/hex"
  20  	"next.orly.dev/pkg/nostr/encoders/ints"
  21  	"next.orly.dev/pkg/nostr/encoders/kind"
  22  	"next.orly.dev/pkg/nostr/encoders/tag"
  23  	"next.orly.dev/pkg/nostr/encoders/tag/atag"
  24  	"next.orly.dev/pkg/database"
  25  	"next.orly.dev/pkg/database/indexes"
  26  	"next.orly.dev/pkg/database/indexes/types"
  27  	"next.orly.dev/pkg/interfaces/store"
  28  	"next.orly.dev/pkg/utils"
  29  )
  30  
  31  // DeleteEvent removes an event from the database identified by `eid`.
  32  func (w *W) DeleteEvent(c context.Context, eid []byte) (err error) {
  33  	w.Logger.Warnf("deleting event %0x", eid)
  34  
  35  	// Get the serial number for the event ID
  36  	var ser *types.Uint40
  37  	ser, err = w.GetSerialById(eid)
  38  	if chk.E(err) {
  39  		return
  40  	}
  41  	if ser == nil {
  42  		// Event wasn't found, nothing to delete
  43  		return
  44  	}
  45  
  46  	// Fetch the event to get its data
  47  	var ev *event.E
  48  	ev, err = w.FetchEventBySerial(ser)
  49  	if chk.E(err) {
  50  		return
  51  	}
  52  	if ev == nil {
  53  		// Event wasn't found, nothing to delete
  54  		return
  55  	}
  56  
  57  	if err = w.DeleteEventBySerial(c, ser, ev); chk.E(err) {
  58  		return
  59  	}
  60  	return
  61  }
  62  
  63  // DeleteEventBySerial removes an event and all its indexes by serial number.
  64  func (w *W) DeleteEventBySerial(c context.Context, ser *types.Uint40, ev *event.E) (err error) {
  65  	w.Logger.Infof("DeleteEventBySerial: deleting event %0x (serial %d)", ev.ID, ser.Get())
  66  
  67  	// Get all indexes for the event
  68  	var idxs [][]byte
  69  	idxs, err = database.GetIndexesForEvent(ev, ser.Get())
  70  	if chk.E(err) {
  71  		w.Logger.Errorf("DeleteEventBySerial: failed to get indexes for event %0x: %v", ev.ID, err)
  72  		return
  73  	}
  74  	w.Logger.Infof("DeleteEventBySerial: found %d indexes for event %0x", len(idxs), ev.ID)
  75  
  76  	// Collect all unique store names we need to access
  77  	storeNames := make(map[string]struct{})
  78  	for _, key := range idxs {
  79  		if len(key) >= 3 {
  80  			storeNames[string(key[:3])] = struct{}{}
  81  		}
  82  	}
  83  
  84  	// Also include event stores
  85  	storeNames[string(indexes.EventPrefix)] = struct{}{}
  86  	storeNames[string(indexes.SmallEventPrefix)] = struct{}{}
  87  
  88  	// Convert to slice
  89  	storeList := make([]string, 0, len(storeNames))
  90  	for name := range storeNames {
  91  		storeList = append(storeList, name)
  92  	}
  93  
  94  	if len(storeList) == 0 {
  95  		return nil
  96  	}
  97  
  98  	// Start a transaction to delete the event and all its indexes
  99  	tx, err := w.db.Transaction(idb.TransactionReadWrite, storeList[0], storeList[1:]...)
 100  	if err != nil {
 101  		return fmt.Errorf("failed to start delete transaction: %w", err)
 102  	}
 103  
 104  	// Delete all indexes
 105  	for _, key := range idxs {
 106  		if len(key) < 3 {
 107  			continue
 108  		}
 109  		storeName := string(key[:3])
 110  		objStore, storeErr := tx.ObjectStore(storeName)
 111  		if storeErr != nil {
 112  			w.Logger.Warnf("DeleteEventBySerial: failed to get object store %s: %v", storeName, storeErr)
 113  			continue
 114  		}
 115  
 116  		keyJS := bytesToSafeValue(key)
 117  		if _, delErr := objStore.Delete(keyJS); delErr != nil {
 118  			w.Logger.Warnf("DeleteEventBySerial: failed to delete index from %s: %v", storeName, delErr)
 119  		}
 120  	}
 121  
 122  	// Delete from small event store
 123  	sevKeyBuf := new(bytes.Buffer)
 124  	if err = indexes.SmallEventEnc(ser).MarshalWrite(sevKeyBuf); err == nil {
 125  		if objStore, storeErr := tx.ObjectStore(string(indexes.SmallEventPrefix)); storeErr == nil {
 126  			// For small events, the key includes size and data, so we need to scan
 127  			w.deleteKeysByPrefix(objStore, sevKeyBuf.Bytes())
 128  		}
 129  	}
 130  
 131  	// Delete from large event store
 132  	evtKeyBuf := new(bytes.Buffer)
 133  	if err = indexes.EventEnc(ser).MarshalWrite(evtKeyBuf); err == nil {
 134  		if objStore, storeErr := tx.ObjectStore(string(indexes.EventPrefix)); storeErr == nil {
 135  			keyJS := bytesToSafeValue(evtKeyBuf.Bytes())
 136  			objStore.Delete(keyJS)
 137  		}
 138  	}
 139  
 140  	// Commit transaction
 141  	if err = tx.Await(c); err != nil {
 142  		return fmt.Errorf("failed to commit delete transaction: %w", err)
 143  	}
 144  
 145  	w.Logger.Infof("DeleteEventBySerial: successfully deleted event %0x and all indexes", ev.ID)
 146  	return nil
 147  }
 148  
 149  // deleteKeysByPrefix deletes all keys starting with the given prefix from an object store
 150  func (w *W) deleteKeysByPrefix(store *idb.ObjectStore, prefix []byte) {
 151  	cursorReq, err := store.OpenCursor(idb.CursorNext)
 152  	if err != nil {
 153  		return
 154  	}
 155  
 156  	var keysToDelete [][]byte
 157  	cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
 158  		keyVal, keyErr := cursor.Key()
 159  		if keyErr != nil {
 160  			return keyErr
 161  		}
 162  
 163  		keyBytes := safeValueToBytes(keyVal)
 164  		if len(keyBytes) >= len(prefix) && bytes.HasPrefix(keyBytes, prefix) {
 165  			keysToDelete = append(keysToDelete, keyBytes)
 166  		}
 167  
 168  		return cursor.Continue()
 169  	})
 170  
 171  	// Delete collected keys
 172  	for _, key := range keysToDelete {
 173  		keyJS := bytesToSafeValue(key)
 174  		store.Delete(keyJS)
 175  	}
 176  }
 177  
 178  // DeleteExpired scans for events with expiration timestamps that have passed and deletes them.
 179  func (w *W) DeleteExpired() {
 180  	now := time.Now().Unix()
 181  
 182  	// Open read transaction to find expired events
 183  	tx, err := w.db.Transaction(idb.TransactionReadOnly, string(indexes.ExpirationPrefix))
 184  	if err != nil {
 185  		w.Logger.Warnf("DeleteExpired: failed to start transaction: %v", err)
 186  		return
 187  	}
 188  
 189  	objStore, err := tx.ObjectStore(string(indexes.ExpirationPrefix))
 190  	if err != nil {
 191  		w.Logger.Warnf("DeleteExpired: failed to get expiration store: %v", err)
 192  		return
 193  	}
 194  
 195  	var expiredSerials types.Uint40s
 196  
 197  	cursorReq, err := objStore.OpenCursor(idb.CursorNext)
 198  	if err != nil {
 199  		w.Logger.Warnf("DeleteExpired: failed to open cursor: %v", err)
 200  		return
 201  	}
 202  
 203  	cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
 204  		keyVal, keyErr := cursor.Key()
 205  		if keyErr != nil {
 206  			return keyErr
 207  		}
 208  
 209  		keyBytes := safeValueToBytes(keyVal)
 210  		if len(keyBytes) < 8 { // exp prefix (3) + expiration (variable) + serial (5)
 211  			return cursor.Continue()
 212  		}
 213  
 214  		// Parse expiration key: exp|expiration_timestamp|serial
 215  		exp, ser := indexes.ExpirationVars()
 216  		buf := bytes.NewBuffer(keyBytes)
 217  		if err := indexes.ExpirationDec(exp, ser).UnmarshalRead(buf); err != nil {
 218  			return cursor.Continue()
 219  		}
 220  
 221  		if int64(exp.Get()) > now {
 222  			// Not expired yet
 223  			return cursor.Continue()
 224  		}
 225  
 226  		expiredSerials = append(expiredSerials, ser)
 227  		return cursor.Continue()
 228  	})
 229  
 230  	// Delete expired events
 231  	for _, ser := range expiredSerials {
 232  		ev, fetchErr := w.FetchEventBySerial(ser)
 233  		if fetchErr != nil || ev == nil {
 234  			continue
 235  		}
 236  		if err := w.DeleteEventBySerial(context.Background(), ser, ev); err != nil {
 237  			w.Logger.Warnf("DeleteExpired: failed to delete expired event: %v", err)
 238  		}
 239  	}
 240  }
 241  
 242  // ProcessDelete processes a kind 5 deletion event, deleting referenced events.
 243  func (w *W) ProcessDelete(ev *event.E, admins [][]byte) (err error) {
 244  	eTags := ev.Tags.GetAll([]byte("e"))
 245  	aTags := ev.Tags.GetAll([]byte("a"))
 246  	kTags := ev.Tags.GetAll([]byte("k"))
 247  
 248  	// Process e-tags: delete specific events by ID
 249  	for _, eTag := range eTags {
 250  		if eTag.Len() < 2 {
 251  			continue
 252  		}
 253  		// Use ValueHex() to handle both binary and hex storage formats
 254  		eventIdHex := eTag.ValueHex()
 255  		if len(eventIdHex) != 64 { // hex encoded event ID
 256  			continue
 257  		}
 258  		// Decode hex event ID
 259  		var eid []byte
 260  		if eid, err = hexenc.DecAppend(nil, eventIdHex); chk.E(err) {
 261  			continue
 262  		}
 263  		// Fetch the event to verify ownership
 264  		var ser *types.Uint40
 265  		if ser, err = w.GetSerialById(eid); chk.E(err) || ser == nil {
 266  			continue
 267  		}
 268  		var targetEv *event.E
 269  		if targetEv, err = w.FetchEventBySerial(ser); chk.E(err) || targetEv == nil {
 270  			continue
 271  		}
 272  		// Only allow users to delete their own events
 273  		if !utils.FastEqual(targetEv.Pubkey, ev.Pubkey) {
 274  			continue
 275  		}
 276  		// Delete the event
 277  		if err = w.DeleteEvent(context.Background(), eid); chk.E(err) {
 278  			w.Logger.Warnf("failed to delete event %x via e-tag: %v", eid, err)
 279  			continue
 280  		}
 281  		w.Logger.Debugf("deleted event %x via e-tag deletion", eid)
 282  	}
 283  
 284  	// Process a-tags: delete addressable events by kind:pubkey:d-tag
 285  	for _, aTag := range aTags {
 286  		if aTag.Len() < 2 {
 287  			continue
 288  		}
 289  		// Parse the 'a' tag value: kind:pubkey:d-tag (for parameterized) or kind:pubkey (for regular)
 290  		split := bytes.Split(aTag.Value(), []byte{':'})
 291  		if len(split) < 2 {
 292  			continue
 293  		}
 294  		// Parse the kind
 295  		kindStr := string(split[0])
 296  		kindInt, parseErr := strconv.Atoi(kindStr)
 297  		if parseErr != nil {
 298  			continue
 299  		}
 300  		kk := kind.New(uint16(kindInt))
 301  		// Parse the pubkey
 302  		var pk []byte
 303  		if pk, err = hexenc.DecAppend(nil, split[1]); chk.E(err) {
 304  			continue
 305  		}
 306  		// Only allow users to delete their own events
 307  		if !utils.FastEqual(pk, ev.Pubkey) {
 308  			continue
 309  		}
 310  
 311  		// Build filter for events to delete
 312  		delFilter := &filter.F{
 313  			Authors: tag.NewFromBytesSlice(pk),
 314  			Kinds:   kind.NewS(kk),
 315  		}
 316  
 317  		// For parameterized replaceable events, add d-tag filter
 318  		if kind.IsParameterizedReplaceable(kk.K) && len(split) >= 3 {
 319  			dValue := split[2]
 320  			delFilter.Tags = tag.NewS(tag.NewFromAny([]byte("d"), dValue))
 321  		}
 322  
 323  		// Find matching events
 324  		var idxs []database.Range
 325  		if idxs, err = database.GetIndexesFromFilter(delFilter); chk.E(err) {
 326  			continue
 327  		}
 328  		var sers types.Uint40s
 329  		for _, idx := range idxs {
 330  			var s types.Uint40s
 331  			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 332  				continue
 333  			}
 334  			sers = append(sers, s...)
 335  		}
 336  
 337  		// Delete events older than the deletion event
 338  		if len(sers) > 0 {
 339  			var idPkTss []*store.IdPkTs
 340  			var tmp []*store.IdPkTs
 341  			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
 342  				continue
 343  			}
 344  			idPkTss = append(idPkTss, tmp...)
 345  			// Sort by timestamp
 346  			sort.Slice(idPkTss, func(i, j int) bool {
 347  				return idPkTss[i].Ts > idPkTss[j].Ts
 348  			})
 349  			for _, v := range idPkTss {
 350  				if v.Ts < ev.CreatedAt {
 351  					if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) {
 352  						w.Logger.Warnf("failed to delete event %x via a-tag: %v", v.Id, err)
 353  						continue
 354  					}
 355  					w.Logger.Debugf("deleted event %x via a-tag deletion", v.Id)
 356  				}
 357  			}
 358  		}
 359  	}
 360  
 361  	// If there are no e or a tags, delete all replaceable events of the kinds
 362  	// specified by the k tags for the pubkey of the delete event.
 363  	if len(eTags) == 0 && len(aTags) == 0 {
 364  		// Parse the kind tags
 365  		var kinds []*kind.K
 366  		for _, k := range kTags {
 367  			kv := k.Value()
 368  			iv := ints.New(0)
 369  			if _, err = iv.Unmarshal(kv); chk.E(err) {
 370  				continue
 371  			}
 372  			kinds = append(kinds, kind.New(iv.N))
 373  		}
 374  
 375  		var idxs []database.Range
 376  		if idxs, err = database.GetIndexesFromFilter(
 377  			&filter.F{
 378  				Authors: tag.NewFromBytesSlice(ev.Pubkey),
 379  				Kinds:   kind.NewS(kinds...),
 380  			},
 381  		); chk.E(err) {
 382  			return
 383  		}
 384  
 385  		var sers types.Uint40s
 386  		for _, idx := range idxs {
 387  			var s types.Uint40s
 388  			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 389  				return
 390  			}
 391  			sers = append(sers, s...)
 392  		}
 393  
 394  		if len(sers) > 0 {
 395  			var idPkTss []*store.IdPkTs
 396  			var tmp []*store.IdPkTs
 397  			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
 398  				return
 399  			}
 400  			idPkTss = append(idPkTss, tmp...)
 401  			// Sort by timestamp
 402  			sort.Slice(idPkTss, func(i, j int) bool {
 403  				return idPkTss[i].Ts > idPkTss[j].Ts
 404  			})
 405  			for _, v := range idPkTss {
 406  				if v.Ts < ev.CreatedAt {
 407  					if err = w.DeleteEvent(context.Background(), v.Id); chk.E(err) {
 408  						continue
 409  					}
 410  				}
 411  			}
 412  		}
 413  	}
 414  	return
 415  }
 416  
 417  // CheckForDeleted checks if the event has been deleted, and returns an error with
 418  // prefix "blocked:" if it is. This function also allows designating admin
 419  // pubkeys that may also delete the event.
 420  func (w *W) CheckForDeleted(ev *event.E, admins [][]byte) (err error) {
 421  	keys := append([][]byte{ev.Pubkey}, admins...)
 422  	authors := tag.NewFromBytesSlice(keys...)
 423  
 424  	// If the event is addressable, check for a deletion event with the same
 425  	// kind/pubkey/dtag
 426  	if kind.IsParameterizedReplaceable(ev.Kind) {
 427  		var idxs []database.Range
 428  		// Construct an a-tag
 429  		t := ev.Tags.GetFirst([]byte("d"))
 430  		var dTagValue []byte
 431  		if t != nil {
 432  			dTagValue = t.Value()
 433  		}
 434  		a := atag.T{
 435  			Kind:   kind.New(ev.Kind),
 436  			Pubkey: ev.Pubkey,
 437  			DTag:   dTagValue,
 438  		}
 439  		at := a.Marshal(nil)
 440  		if idxs, err = database.GetIndexesFromFilter(
 441  			&filter.F{
 442  				Authors: authors,
 443  				Kinds:   kind.NewS(kind.Deletion),
 444  				Tags:    tag.NewS(tag.NewFromAny("#a", at)),
 445  			},
 446  		); chk.E(err) {
 447  			return
 448  		}
 449  
 450  		var sers types.Uint40s
 451  		for _, idx := range idxs {
 452  			var s types.Uint40s
 453  			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 454  				return
 455  			}
 456  			sers = append(sers, s...)
 457  		}
 458  
 459  		if len(sers) > 0 {
 460  			var idPkTss []*store.IdPkTs
 461  			var tmp []*store.IdPkTs
 462  			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
 463  				return
 464  			}
 465  			idPkTss = append(idPkTss, tmp...)
 466  			// Find the newest deletion timestamp
 467  			maxTs := idPkTss[0].Ts
 468  			for i := 1; i < len(idPkTss); i++ {
 469  				if idPkTss[i].Ts > maxTs {
 470  					maxTs = idPkTss[i].Ts
 471  				}
 472  			}
 473  			if ev.CreatedAt < maxTs {
 474  				err = errorf.E(
 475  					"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d",
 476  					ev.ID, at, ev.CreatedAt, maxTs,
 477  				)
 478  				return
 479  			}
 480  			return
 481  		}
 482  		return
 483  	}
 484  
 485  	// If the event is replaceable, check if there is a deletion event newer
 486  	// than the event
 487  	if kind.IsReplaceable(ev.Kind) {
 488  		var idxs []database.Range
 489  		if idxs, err = database.GetIndexesFromFilter(
 490  			&filter.F{
 491  				Authors: tag.NewFromBytesSlice(ev.Pubkey),
 492  				Kinds:   kind.NewS(kind.Deletion),
 493  				Tags: tag.NewS(
 494  					tag.NewFromAny("#k", fmt.Sprint(ev.Kind)),
 495  				),
 496  			},
 497  		); chk.E(err) {
 498  			return
 499  		}
 500  
 501  		var sers types.Uint40s
 502  		for _, idx := range idxs {
 503  			var s types.Uint40s
 504  			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 505  				return
 506  			}
 507  			sers = append(sers, s...)
 508  		}
 509  
 510  		if len(sers) > 0 {
 511  			var idPkTss []*store.IdPkTs
 512  			var tmp []*store.IdPkTs
 513  			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
 514  				return
 515  			}
 516  			idPkTss = append(idPkTss, tmp...)
 517  			// Find the newest deletion
 518  			maxTs := idPkTss[0].Ts
 519  			maxId := idPkTss[0].Id
 520  			for i := 1; i < len(idPkTss); i++ {
 521  				if idPkTss[i].Ts > maxTs {
 522  					maxTs = idPkTss[i].Ts
 523  					maxId = idPkTss[i].Id
 524  				}
 525  			}
 526  			if ev.CreatedAt < maxTs {
 527  				err = fmt.Errorf(
 528  					"blocked: %0x was deleted: the event is older than the delete event %0x: event: %d delete: %d",
 529  					ev.ID, maxId, ev.CreatedAt, maxTs,
 530  				)
 531  				return
 532  			}
 533  		}
 534  
 535  		// This type of delete can also use an a tag to specify kind and author
 536  		idxs = nil
 537  		a := atag.T{
 538  			Kind:   kind.New(ev.Kind),
 539  			Pubkey: ev.Pubkey,
 540  		}
 541  		at := a.Marshal(nil)
 542  		if idxs, err = database.GetIndexesFromFilter(
 543  			&filter.F{
 544  				Authors: authors,
 545  				Kinds:   kind.NewS(kind.Deletion),
 546  				Tags:    tag.NewS(tag.NewFromAny("#a", at)),
 547  			},
 548  		); chk.E(err) {
 549  			return
 550  		}
 551  
 552  		sers = nil
 553  		for _, idx := range idxs {
 554  			var s types.Uint40s
 555  			if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 556  				return
 557  			}
 558  			sers = append(sers, s...)
 559  		}
 560  
 561  		if len(sers) > 0 {
 562  			var idPkTss []*store.IdPkTs
 563  			var tmp []*store.IdPkTs
 564  			if tmp, err = w.GetFullIdPubkeyBySerials(sers); chk.E(err) {
 565  				return
 566  			}
 567  			idPkTss = append(idPkTss, tmp...)
 568  			// Find the newest deletion
 569  			maxTs := idPkTss[0].Ts
 570  			for i := 1; i < len(idPkTss); i++ {
 571  				if idPkTss[i].Ts > maxTs {
 572  					maxTs = idPkTss[i].Ts
 573  				}
 574  			}
 575  			if ev.CreatedAt < maxTs {
 576  				err = errorf.E(
 577  					"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d",
 578  					ev.ID, at, ev.CreatedAt, maxTs,
 579  				)
 580  				return
 581  			}
 582  			return
 583  		}
 584  		return
 585  	}
 586  
 587  	// Otherwise check for a delete by event id
 588  	var idxs []database.Range
 589  	if idxs, err = database.GetIndexesFromFilter(
 590  		&filter.F{
 591  			Authors: authors,
 592  			Kinds:   kind.NewS(kind.Deletion),
 593  			Tags: tag.NewS(
 594  				tag.NewFromAny("e", hexenc.Enc(ev.ID)),
 595  			),
 596  		},
 597  	); chk.E(err) {
 598  		return
 599  	}
 600  
 601  	for _, idx := range idxs {
 602  		var s types.Uint40s
 603  		if s, err = w.GetSerialsByRange(idx); chk.E(err) {
 604  			return
 605  		}
 606  		if len(s) > 0 {
 607  			// Any e-tag deletion found means the exact event was deleted
 608  			err = errorf.E("blocked: %0x has been deleted", ev.ID)
 609  			return
 610  		}
 611  	}
 612  
 613  	return
 614  }
 615