Browse Source

Enhance delete event handling and logging

- Improved logging for delete events in handle-delete.go, including detailed information about the event and its tags.
- Added checks for admin and owner deletions, with appropriate logging for each case.
- Updated HandleEvent to process delete events more robustly, including success and error logging.
- Introduced a new fetchEventById function in nostr.js to verify event deletion.
- Updated App.svelte to handle event deletion verification and state management.
- Changed favicon references in HTML files to use the new orly-favicon.png.
- Added orly-favicon.png to the public and docs directories for consistent branding.
main
mleku 3 months ago
parent
commit
67a74980f9
No known key found for this signature in database
  1. 51
      app/handle-delete.go
  2. 59
      app/handle-event.go
  3. 25
      app/main.go
  4. 11
      app/server.go
  5. 2
      app/web/dist/index.html
  6. 2
      app/web/public/index.html
  7. BIN
      app/web/public/orly-favicon.png
  8. 85
      app/web/src/App.svelte
  9. 2
      app/web/src/constants.js
  10. 53
      app/web/src/nostr.js
  11. BIN
      docs/orly-favicon.png
  12. 20
      pkg/database/delete-event.go

51
app/handle-delete.go

@ -24,23 +24,36 @@ func (l *Listener) GetSerialsFromFilter(f *filter.F) ( @@ -24,23 +24,36 @@ func (l *Listener) GetSerialsFromFilter(f *filter.F) (
}
func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
// log.I.C(
// func() string {
// return fmt.Sprintf(
// "delete event\n%s", env.E.Serialize(),
// )
// },
// )
log.I.F("HandleDelete: processing delete event %0x from pubkey %0x", env.E.ID, env.E.Pubkey)
log.I.F("HandleDelete: delete event tags: %d tags", len(*env.E.Tags))
for i, t := range *env.E.Tags {
log.I.F("HandleDelete: tag %d: %s = %s", i, string(t.Key()), string(t.Value()))
}
var ownerDelete bool
for _, pk := range l.Admins {
if utils.FastEqual(pk, env.E.Pubkey) {
ownerDelete = true
log.I.F("HandleDelete: delete event from admin/owner %0x", env.E.Pubkey)
break
}
}
if !ownerDelete {
for _, pk := range l.Owners {
if utils.FastEqual(pk, env.E.Pubkey) {
ownerDelete = true
log.I.F("HandleDelete: delete event from owner %0x", env.E.Pubkey)
break
}
}
}
if !ownerDelete {
log.I.F("HandleDelete: delete event from regular user %0x", env.E.Pubkey)
}
// process the tags in the delete event
var deleteErr error
var validDeletionFound bool
var deletionCount int
for _, t := range *env.E.Tags {
// first search for a tags, as these are the simplest to process
if utils.FastEqual(t.Key(), []byte("a")) {
@ -109,8 +122,10 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) { @@ -109,8 +122,10 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
if err = l.DeleteEventBySerial(
l.Ctx(), s, ev,
); chk.E(err) {
log.E.F("HandleDelete: failed to delete event %s: %v", hex.Enc(ev.ID), err)
continue
}
deletionCount++
}
}
}
@ -121,21 +136,27 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) { @@ -121,21 +136,27 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
if utils.FastEqual(t.Key(), []byte("e")) {
val := t.Value()
if len(val) == 0 {
log.W.F("HandleDelete: empty e-tag value")
continue
}
log.I.F("HandleDelete: processing e-tag with value: %s", string(val))
var dst []byte
if b, e := hex.Dec(string(val)); chk.E(e) {
log.E.F("HandleDelete: failed to decode hex event ID %s: %v", string(val), e)
continue
} else {
dst = b
log.I.F("HandleDelete: decoded event ID: %0x", dst)
}
f := &filter.F{
Ids: tag.NewFromBytesSlice(dst),
}
var sers types.Uint40s
if sers, err = l.GetSerialsFromFilter(f); chk.E(err) {
log.E.F("HandleDelete: failed to get serials from filter: %v", err)
continue
}
log.I.F("HandleDelete: found %d serials for event ID %s", len(sers), string(val))
// if found, delete them
if len(sers) > 0 {
// there should be only one event per serial, so we can just
@ -164,8 +185,10 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) { @@ -164,8 +185,10 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
hex.Enc(ev.ID), hex.Enc(env.E.Pubkey),
)
if err = l.DeleteEventBySerial(l.Ctx(), s, ev); chk.E(err) {
log.E.F("HandleDelete: failed to delete event %s: %v", hex.Enc(ev.ID), err)
continue
}
deletionCount++
}
continue
}
@ -204,17 +227,27 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) { @@ -204,17 +227,27 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
if !utils.FastEqual(env.E.Pubkey, ev.Pubkey) {
continue
}
validDeletionFound = true
log.I.F(
"HandleDelete: deleting event %s via k-tag by authorized user %s",
hex.Enc(ev.ID), hex.Enc(env.E.Pubkey),
)
if err = l.DeleteEventBySerial(l.Ctx(), s, ev); chk.E(err) {
log.E.F("HandleDelete: failed to delete event %s: %v", hex.Enc(ev.ID), err)
continue
}
deletionCount++
}
continue
}
}
continue
}
// If no valid deletions were found, return an error
if !validDeletionFound {
log.W.F("HandleDelete: no valid deletions found for event %0x", env.E.ID)
return fmt.Errorf("blocked: cannot delete events that belong to other users")
}
log.I.F("HandleDelete: successfully processed %d deletions for event %0x", deletionCount, env.E.ID)
return
}

59
app/handle-event.go

@ -167,6 +167,21 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { @@ -167,6 +167,21 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
// user has write access or better, continue
// log.D.F("user has %s access", accessLevel)
}
// check if event is ephemeral - if so, deliver and return early
if kind.IsEphemeral(env.E.Kind) {
log.D.F("handling ephemeral event %0x (kind %d)", env.E.ID, env.E.Kind)
// Send OK response for ephemeral events
if err = Ok.Ok(l, env, ""); chk.E(err) {
return
}
// Deliver the event to subscribers immediately
clonedEvent := env.E.Clone()
go l.publishers.Deliver(clonedEvent)
log.D.F("delivered ephemeral event %0x", env.E.ID)
return
}
// check for protected tag (NIP-70)
protectedTag := env.E.Tags.GetFirst([]byte("-"))
if protectedTag != nil && acl.Registry.Active.Load() != "none" {
@ -183,7 +198,9 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { @@ -183,7 +198,9 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
}
// if the event is a delete, process the delete
if env.E.Kind == kind.EventDeletion.K {
log.I.F("processing delete event %0x", env.E.ID)
if err = l.HandleDelete(env); err != nil {
log.E.F("HandleDelete failed for event %0x: %v", env.E.ID, err)
if strings.HasPrefix(err.Error(), "blocked:") {
errStr := err.Error()[len("blocked: "):len(err.Error())]
if err = Ok.Error(
@ -193,10 +210,41 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { @@ -193,10 +210,41 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
}
return
}
// For non-blocked errors, still send OK but log the error
log.W.F("Delete processing failed but continuing: %v", err)
} else {
log.I.F("HandleDelete completed successfully for event %0x", env.E.ID)
}
// Send OK response for delete events
if err = Ok.Ok(l, env, ""); chk.E(err) {
return
}
// Store the delete event itself
saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, _, err = l.SaveEvent(saveCtx, env.E); err != nil {
if strings.HasPrefix(err.Error(), "blocked:") {
errStr := err.Error()[len("blocked: "):len(err.Error())]
if err = Ok.Error(
l, env, errStr,
); chk.E(err) {
return
}
return
}
chk.E(err)
return
}
// Deliver the delete event to subscribers
clonedEvent := env.E.Clone()
go l.publishers.Deliver(clonedEvent)
log.D.F("processed delete event %0x", env.E.ID)
return
} else {
// check if the event was deleted
if err = l.CheckForDeleted(env.E, l.Admins); err != nil {
// Combine admins and owners for deletion checking
adminOwners := append(l.Admins, l.Owners...)
if err = l.CheckForDeleted(env.E, adminOwners); err != nil {
if strings.HasPrefix(err.Error(), "blocked:") {
errStr := err.Error()[len("blocked: "):len(err.Error())]
if err = Ok.Error(
@ -234,12 +282,21 @@ func (l *Listener) HandleEvent(msg []byte) (err error) { @@ -234,12 +282,21 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
go l.publishers.Deliver(clonedEvent)
log.D.F("saved event %0x", env.E.ID)
var isNewFromAdmin bool
// Check if event is from admin or owner
for _, admin := range l.Admins {
if utils.FastEqual(admin, env.E.Pubkey) {
isNewFromAdmin = true
break
}
}
if !isNewFromAdmin {
for _, owner := range l.Owners {
if utils.FastEqual(owner, env.E.Pubkey) {
isNewFromAdmin = true
break
}
}
}
if isNewFromAdmin {
log.I.F("new event from admin %0x", env.E.Pubkey)
// if a follow list was saved, reconfigure ACLs now that it is persisted

25
app/main.go

@ -36,6 +36,18 @@ func Run( @@ -36,6 +36,18 @@ func Run(
}
adminKeys = append(adminKeys, pk)
}
// get the owners
var ownerKeys [][]byte
for _, owner := range cfg.Owners {
if len(owner) == 0 {
continue
}
var pk []byte
if pk, err = bech32encoding.NpubOrHexToPublicKeyBinary(owner); chk.E(err) {
continue
}
ownerKeys = append(ownerKeys, pk)
}
// start listener
l := &Server{
Ctx: ctx,
@ -43,6 +55,7 @@ func Run( @@ -43,6 +55,7 @@ func Run(
D: db,
publishers: publish.New(NewPublisher(ctx)),
Admins: adminKeys,
Owners: ownerKeys,
}
// Initialize sprocket manager
@ -68,6 +81,18 @@ func Run( @@ -68,6 +81,18 @@ func Run(
cfg.Admins = append(cfg.Admins, pk)
log.I.F("added relay identity to admins for follow-list whitelisting")
}
// also ensure relay identity pubkey is considered an owner for full control
found = false
for _, o := range cfg.Owners {
if o == pk {
found = true
break
}
}
if !found {
cfg.Owners = append(cfg.Owners, pk)
log.I.F("added relay identity to owners for full control")
}
}
}

11
app/server.go

@ -34,6 +34,7 @@ type Server struct { @@ -34,6 +34,7 @@ type Server struct {
remote string
publishers *publish.S
Admins [][]byte
Owners [][]byte
*database.D
// optional reverse proxy for dev web server
@ -179,7 +180,7 @@ func (s *Server) UserInterface() { @@ -179,7 +180,7 @@ func (s *Server) UserInterface() {
s.challengeMutex.Unlock()
}
// Serve favicon.ico by serving orly.png
// Serve favicon.ico by serving orly-favicon.png
s.mux.HandleFunc("/favicon.ico", s.handleFavicon)
// Serve the main login interface (and static assets) or proxy in dev mode
@ -206,7 +207,7 @@ func (s *Server) UserInterface() { @@ -206,7 +207,7 @@ func (s *Server) UserInterface() {
s.mux.HandleFunc("/api/sprocket/config", s.handleSprocketConfig)
}
// handleFavicon serves orly.png as favicon.ico
// handleFavicon serves orly-favicon.png as favicon.ico
func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) {
// In dev mode with proxy configured, forward to dev server
if s.devProxy != nil {
@ -214,14 +215,14 @@ func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) { @@ -214,14 +215,14 @@ func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) {
return
}
// Serve orly.png as favicon.ico from embedded web app
// Serve orly-favicon.png as favicon.ico from embedded web app
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day
// Create a request for orly.png and serve it
// Create a request for orly-favicon.png and serve it
faviconReq := &http.Request{
Method: "GET",
URL: &url.URL{Path: "/orly.png"},
URL: &url.URL{Path: "/orly-favicon.png"},
}
ServeEmbeddedWeb(w, faviconReq)
}

2
app/web/dist/index.html vendored

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Next Orly</title>
<link rel="icon" href="/orly.png" type="image/png" />
<link rel="icon" href="/orly-favicon.png" type="image/png" />
<link rel="stylesheet" href="/bundle.css" />
</head>
<body>

2
app/web/public/index.html

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
<title>ORLY?</title>
<link rel="icon" type="image/png" href="/orly.png" />
<link rel="icon" type="image/png" href="/orly-favicon.png" />
<link rel="stylesheet" href="/global.css" />
<link rel="stylesheet" href="/build/bundle.css" />

BIN
app/web/public/orly-favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

85
app/web/src/App.svelte

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
<script>
import LoginModal from './LoginModal.svelte';
import { initializeNostrClient, fetchUserProfile, fetchAllEvents, fetchUserEvents, searchEvents, nostrClient, NostrClient } from './nostr.js';
import { initializeNostrClient, fetchUserProfile, fetchAllEvents, fetchUserEvents, searchEvents, fetchEventById, nostrClient, NostrClient } from './nostr.js';
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
let isDarkTheme = false;
@ -40,6 +40,13 @@ @@ -40,6 +40,13 @@
// Events filter toggle
let showOnlyMyEvents = false;
// My Events state
let myEvents = [];
let isLoadingMyEvents = false;
let hasMoreMyEvents = true;
let oldestMyEventTimestamp = null;
let newestMyEventTimestamp = null;
// Sprocket management state
let sprocketScript = '';
@ -232,8 +239,40 @@ @@ -232,8 +239,40 @@
console.log('Delete event published:', result);
if (result.success && result.okCount > 0) {
// Remove from local list
// Wait a moment for the deletion to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify the event was actually deleted by trying to fetch it
try {
const deletedEvent = await fetchEventById(eventId, { timeout: 5000 });
if (deletedEvent) {
console.warn('Event still exists after deletion attempt:', deletedEvent);
alert(`Warning: Delete event was accepted by ${result.okCount} relay(s), but the event still exists on the relay. This may indicate the relay does not properly handle delete events.`);
} else {
console.log('Event successfully deleted and verified');
}
} catch (fetchError) {
console.log('Could not fetch event after deletion (likely deleted):', fetchError.message);
}
// Remove from local lists
allEvents = allEvents.filter(event => event.id !== eventId);
myEvents = myEvents.filter(event => event.id !== eventId);
// Remove from global cache
globalEventsCache = globalEventsCache.filter(event => event.id !== eventId);
// Remove from search results cache
for (const [tabId, searchResult] of searchResults) {
if (searchResult.events) {
searchResult.events = searchResult.events.filter(event => event.id !== eventId);
searchResults.set(tabId, searchResult);
}
}
// Update persistent state
savePersistentState();
alert(`Event deleted successfully (accepted by ${result.okCount} relay(s))`);
} else {
throw new Error('No relays accepted the delete event');
@ -241,7 +280,7 @@ @@ -241,7 +280,7 @@
} else {
// Admin/owner deleting someone else's event - only publish to local relay
// We need to publish only to the local relay, not external ones
const localRelayUrl = `wss://${window.location.host}/ws`;
const localRelayUrl = `wss://${window.location.host}/`;
// Create a modified client that only connects to the local relay
const localClient = new NostrClient();
@ -251,8 +290,40 @@ @@ -251,8 +290,40 @@
console.log('Delete event published to local relay only:', result);
if (result.success && result.okCount > 0) {
// Remove from local list
// Wait a moment for the deletion to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify the event was actually deleted by trying to fetch it
try {
const deletedEvent = await fetchEventById(eventId, { timeout: 5000 });
if (deletedEvent) {
console.warn('Event still exists after deletion attempt:', deletedEvent);
alert(`Warning: Delete event was accepted by ${result.okCount} relay(s), but the event still exists on the relay. This may indicate the relay does not properly handle delete events.`);
} else {
console.log('Event successfully deleted and verified');
}
} catch (fetchError) {
console.log('Could not fetch event after deletion (likely deleted):', fetchError.message);
}
// Remove from local lists
allEvents = allEvents.filter(event => event.id !== eventId);
myEvents = myEvents.filter(event => event.id !== eventId);
// Remove from global cache
globalEventsCache = globalEventsCache.filter(event => event.id !== eventId);
// Remove from search results cache
for (const [tabId, searchResult] of searchResults) {
if (searchResult.events) {
searchResult.events = searchResult.events.filter(event => event.id !== eventId);
searchResults.set(tabId, searchResult);
}
}
// Update persistent state
savePersistentState();
alert(`Event deleted successfully (local relay only - admin/owner deleting other user's event)`);
} else {
throw new Error('Local relay did not accept the delete event');
@ -1102,12 +1173,8 @@ @@ -1102,12 +1173,8 @@
if (reset) {
myEvents = events;
// Update cache
updateCache(userPubkey, events);
} else {
myEvents = [...myEvents, ...events];
// Update cache with all events
updateCache(userPubkey, myEvents);
}
// Update oldest timestamp for next pagination
@ -1355,7 +1422,7 @@ @@ -1355,7 +1422,7 @@
<!-- Header -->
<header class="main-header" class:dark-theme={isDarkTheme}>
<div class="header-content">
<img src="/orly.png" alt="Orly Logo" class="logo"/>
<img src="/orly-favicon.png" alt="Orly Logo" class="logo"/>
{#if isSearchMode}
<div class="search-input-container">
<input

2
app/web/src/constants.js

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
// Default Nostr relays for searching
export const DEFAULT_RELAYS = [
// Use the local relay WebSocket endpoint
`wss://${window.location.host}/ws`,
`wss://${window.location.host}/`,
];

53
app/web/src/nostr.js

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import NDK, { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import NDK, { NDKPrivateKeySigner, NDKEvent } from '@nostr-dev-kit/ndk';
import { DEFAULT_RELAYS } from "./constants.js";
// NDK-based Nostr client wrapper
@ -30,7 +30,13 @@ class NostrClient { @@ -30,7 +30,13 @@ class NostrClient {
console.log(`Adding relay to NDK: ${relayUrl}`);
try {
await this.ndk.addRelay(relayUrl);
// For now, just update the DEFAULT_RELAYS array and reconnect
// This is a simpler approach that avoids replacing the NDK instance
DEFAULT_RELAYS.push(relayUrl);
// Reconnect with the updated relay list
await this.connect();
console.log(`✓ Successfully added relay ${relayUrl}`);
return true;
} catch (error) {
@ -68,7 +74,10 @@ class NostrClient { @@ -68,7 +74,10 @@ class NostrClient {
disconnect() {
console.log("Disconnecting NDK");
this.ndk.destroy();
// Note: NDK doesn't have a destroy method, just disconnect
if (this.ndk && typeof this.ndk.disconnect === 'function') {
this.ndk.disconnect();
}
this.isConnected = false;
}
@ -77,7 +86,7 @@ class NostrClient { @@ -77,7 +86,7 @@ class NostrClient {
console.log("Publishing event via NDK:", event);
try {
const ndkEvent = this.ndk.createEvent(event);
const ndkEvent = new NDKEvent(this.ndk, event);
await ndkEvent.publish();
console.log("✓ Event published successfully via NDK");
return { success: true, okCount: 1, errorCount: 0 };
@ -364,6 +373,42 @@ export async function searchEvents(searchQuery, options = {}) { @@ -364,6 +373,42 @@ export async function searchEvents(searchQuery, options = {}) {
return events;
}
// Fetch a specific event by ID
export async function fetchEventById(eventId, options = {}) {
const {
timeout = 10000,
relays = null
} = options;
console.log(`Fetching event by ID: ${eventId}`);
try {
const ndk = nostrClient.getNDK();
const filters = {
ids: [eventId]
};
console.log('Fetching event via NDK with filters:', filters);
// Use NDK's fetchEvents method
const events = await ndk.fetchEvents(filters, {
timeout
});
console.log(`Fetched ${events.size} events via NDK`);
// Convert NDK events to raw events
const rawEvents = Array.from(events).map(event => event.rawEvent());
// Return the first event if found, null otherwise
return rawEvents.length > 0 ? rawEvents[0] : null;
} catch (error) {
console.error("Failed to fetch event by ID via NDK:", error);
throw error;
}
}
// Initialize client connection
export async function initializeNostrClient() {

BIN
docs/orly-favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

20
pkg/database/delete-event.go

@ -45,32 +45,50 @@ func (d *D) DeleteEvent(c context.Context, eid []byte) (err error) { @@ -45,32 +45,50 @@ func (d *D) DeleteEvent(c context.Context, eid []byte) (err error) {
func (d *D) DeleteEventBySerial(
c context.Context, ser *types.Uint40, ev *event.E,
) (err error) {
d.Logger.Infof("DeleteEventBySerial: deleting event %0x (serial %d)", ev.ID, ser.Get())
// Get all indexes for the event
var idxs [][]byte
idxs, err = GetIndexesForEvent(ev, ser.Get())
if chk.E(err) {
d.Logger.Errorf("DeleteEventBySerial: failed to get indexes for event %0x: %v", ev.ID, err)
return
}
d.Logger.Infof("DeleteEventBySerial: found %d indexes for event %0x", len(idxs), ev.ID)
// Get the event key
eventKey := new(bytes.Buffer)
if err = indexes.EventEnc(ser).MarshalWrite(eventKey); chk.E(err) {
d.Logger.Errorf("DeleteEventBySerial: failed to create event key for %0x: %v", ev.ID, err)
return
}
// Delete the event and all its indexes in a transaction
err = d.Update(
func(txn *badger.Txn) (err error) {
// Delete the event
if err = txn.Delete(eventKey.Bytes()); chk.E(err) {
d.Logger.Errorf("DeleteEventBySerial: failed to delete event %0x: %v", ev.ID, err)
return
}
d.Logger.Infof("DeleteEventBySerial: deleted event %0x", ev.ID)
// Delete all indexes
for _, key := range idxs {
for i, key := range idxs {
if err = txn.Delete(key); chk.E(err) {
d.Logger.Errorf("DeleteEventBySerial: failed to delete index %d for event %0x: %v", i, ev.ID, err)
return
}
}
d.Logger.Infof("DeleteEventBySerial: deleted %d indexes for event %0x", len(idxs), ev.ID)
return
},
)
if chk.E(err) {
d.Logger.Errorf("DeleteEventBySerial: transaction failed for event %0x: %v", ev.ID, err)
return
}
d.Logger.Infof("DeleteEventBySerial: successfully deleted event %0x and all indexes", ev.ID)
return
}

Loading…
Cancel
Save