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