+ This will publish a deletion request to all relays. Note that not all relays
+ may honor this request, and the content may remain visible on some relays.
+
+ {#if deleteError}
+
{deleteError}
+ {/if}
+
+
+
+
+
+
+{/if}
+
+
diff --git a/src/lib/services/deletion.ts b/src/lib/services/deletion.ts
new file mode 100644
index 0000000..4550687
--- /dev/null
+++ b/src/lib/services/deletion.ts
@@ -0,0 +1,117 @@
+import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
+
+export interface DeletionOptions {
+ eventId?: string;
+ eventAddress?: string;
+ eventKind?: number;
+ reason?: string;
+ onSuccess?: (deletionEventId: string) => void;
+ onError?: (error: string) => void;
+}
+
+export interface DeletionResult {
+ success: boolean;
+ deletionEventId?: string;
+ error?: string;
+}
+
+/**
+ * Deletes a Nostr event by publishing a kind 5 deletion request (NIP-09)
+ * @param options - Deletion options
+ * @param ndk - NDK instance
+ * @returns Promise resolving to deletion result
+ */
+export async function deleteEvent(
+ options: DeletionOptions,
+ ndk: NDK,
+): Promise {
+ const { eventId, eventAddress, eventKind, reason = "", onSuccess, onError } = options;
+
+ if (!eventId && !eventAddress) {
+ const error = "Either eventId or eventAddress must be provided";
+ onError?.(error);
+ return { success: false, error };
+ }
+
+ if (!ndk?.activeUser) {
+ const error = "Please log in first";
+ onError?.(error);
+ return { success: false, error };
+ }
+
+ try {
+ // Create deletion event (kind 5)
+ const deletionEvent = new NDKEvent(ndk);
+ deletionEvent.kind = 5;
+ deletionEvent.created_at = Math.floor(Date.now() / 1000);
+ deletionEvent.content = reason;
+ deletionEvent.pubkey = ndk.activeUser.pubkey;
+
+ // Build tags based on what we have
+ const tags: string[][] = [];
+
+ if (eventId) {
+ // Add 'e' tag for event ID
+ tags.push(['e', eventId]);
+ }
+
+ if (eventAddress) {
+ // Add 'a' tag for replaceable event address
+ tags.push(['a', eventAddress]);
+ }
+
+ if (eventKind) {
+ // Add 'k' tag for event kind (recommended by NIP-09)
+ tags.push(['k', eventKind.toString()]);
+ }
+
+ deletionEvent.tags = tags;
+
+ // Sign the deletion event
+ await deletionEvent.sign();
+
+ // Publish to all available relays
+ const allRelayUrls = Array.from(ndk.pool?.relays.values() || []).map(
+ (r) => r.url,
+ );
+
+ if (allRelayUrls.length === 0) {
+ throw new Error("No relays available in NDK pool");
+ }
+
+ const relaySet = NDKRelaySet.fromRelayUrls(allRelayUrls, ndk);
+ const publishedToRelays = await deletionEvent.publish(relaySet);
+
+ if (publishedToRelays.size > 0) {
+ console.log(
+ `[deletion.ts] Published deletion request to ${publishedToRelays.size} relays`,
+ );
+ const result = { success: true, deletionEventId: deletionEvent.id };
+ onSuccess?.(deletionEvent.id);
+ return result;
+ } else {
+ throw new Error("Failed to publish deletion request to any relays");
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error";
+ console.error(`[deletion.ts] Error deleting event: ${errorMessage}`);
+ onError?.(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+}
+
+/**
+ * Checks if the current user has permission to delete an event
+ * @param event - The event to check
+ * @param ndk - NDK instance
+ * @returns True if the user can delete the event
+ */
+export function canDeleteEvent(event: NDKEvent | null, ndk: NDK): boolean {
+ if (!event || !ndk?.activeUser) {
+ return false;
+ }
+
+ // User can only delete their own events
+ return event.pubkey === ndk.activeUser.pubkey;
+}
diff --git a/tests/unit/deletion.test.ts b/tests/unit/deletion.test.ts
new file mode 100644
index 0000000..f483d8c
--- /dev/null
+++ b/tests/unit/deletion.test.ts
@@ -0,0 +1,129 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { deleteEvent, canDeleteEvent } from '$lib/services/deletion';
+import NDK, { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk';
+
+describe('Deletion Service', () => {
+ let mockNdk: NDK;
+ let mockEvent: NDKEvent;
+
+ beforeEach(() => {
+ // Create mock NDK instance
+ mockNdk = {
+ activeUser: {
+ pubkey: 'test-pubkey-123',
+ },
+ pool: {
+ relays: new Map([
+ ['wss://relay1.example.com', { url: 'wss://relay1.example.com' }],
+ ['wss://relay2.example.com', { url: 'wss://relay2.example.com' }],
+ ]),
+ },
+ } as unknown as NDK;
+
+ // Create mock event
+ mockEvent = {
+ id: 'event-id-123',
+ kind: 30041,
+ pubkey: 'test-pubkey-123',
+ tagAddress: () => '30041:test-pubkey-123:test-identifier',
+ } as unknown as NDKEvent;
+ });
+
+ describe('canDeleteEvent', () => {
+ it('should return true when user is the event author', () => {
+ const result = canDeleteEvent(mockEvent, mockNdk);
+ expect(result).toBe(true);
+ });
+
+ it('should return false when user is not the event author', () => {
+ const differentUserEvent = {
+ ...mockEvent,
+ pubkey: 'different-pubkey-456',
+ } as unknown as NDKEvent;
+
+ const result = canDeleteEvent(differentUserEvent, mockNdk);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when event is null', () => {
+ const result = canDeleteEvent(null, mockNdk);
+ expect(result).toBe(false);
+ });
+
+ it('should return false when ndk has no active user', () => {
+ const ndkWithoutUser = {
+ ...mockNdk,
+ activeUser: undefined,
+ } as unknown as NDK;
+
+ const result = canDeleteEvent(mockEvent, ndkWithoutUser);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('deleteEvent', () => {
+ it('should return error when no eventId or eventAddress provided', async () => {
+ const result = await deleteEvent({}, mockNdk);
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Either eventId or eventAddress must be provided');
+ });
+
+ it('should return error when user is not logged in', async () => {
+ const ndkWithoutUser = {
+ ...mockNdk,
+ activeUser: undefined,
+ } as unknown as NDK;
+
+ const result = await deleteEvent(
+ { eventId: 'test-id' },
+ ndkWithoutUser
+ );
+
+ expect(result.success).toBe(false);
+ expect(result.error).toBe('Please log in first');
+ });
+
+ it('should create deletion event with correct tags', async () => {
+ const mockSign = vi.fn();
+ const mockPublish = vi.fn().mockResolvedValue(new Set(['wss://relay1.example.com']));
+
+ // Mock NDKEvent constructor
+ const MockNDKEvent = vi.fn().mockImplementation(function(this: any) {
+ this.kind = 0;
+ this.created_at = 0;
+ this.tags = [];
+ this.content = '';
+ this.pubkey = '';
+ this.sign = mockSign;
+ this.publish = mockPublish;
+ return this;
+ });
+
+ // Mock NDKRelaySet
+ const mockRelaySet = {} as NDKRelaySet;
+ vi.spyOn(NDKRelaySet, 'fromRelayUrls').mockReturnValue(mockRelaySet);
+
+ // Replace global NDKEvent temporarily
+ const originalNDKEvent = global.NDKEvent;
+ (global as any).NDKEvent = MockNDKEvent;
+
+ const result = await deleteEvent(
+ {
+ eventId: 'event-123',
+ eventAddress: '30041:pubkey:identifier',
+ eventKind: 30041,
+ reason: 'Test deletion',
+ },
+ mockNdk
+ );
+
+ // Restore original
+ (global as any).NDKEvent = originalNDKEvent;
+
+ expect(MockNDKEvent).toHaveBeenCalled();
+ expect(mockSign).toHaveBeenCalled();
+ expect(mockPublish).toHaveBeenCalled();
+ });
+ });
+});