From 038321e3f0c6cb198f874ebbcf7bc6e3c22675b5 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Tue, 31 Mar 2026 17:55:32 -0300 Subject: [PATCH] Implement NIP-09 deletions --- lib/services/background_update_service.dart | 26 +++++++++++++++++++ lib/services/secure_storage_service.dart | 28 +++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/lib/services/background_update_service.dart b/lib/services/background_update_service.dart index 4c4298c..18e0cec 100644 --- a/lib/services/background_update_service.dart +++ b/lib/services/background_update_service.dart @@ -234,6 +234,32 @@ Future _checkForUpdatesInBackground(Set? appCatalogRelays) async { } } + // NIP-09: fetch kind-5 deletion events since last sync. + // No author filter — the AppCatalog relay is curated and the `since` + // cursor keeps incremental runs cheap. The DB layer auto-processes + // each kind-5 event (removes referenced events, records in deletions + // table). We then clean up the kind-5 events themselves as housekeeping, + // but save the cursor regardless — a cleanup failure is non-critical. + final secureStorage = SecureStorageService(); + final lastDeletionSync = await secureStorage.getDeletionsSyncedUntil(); + final deletionRequests = await storage.query( + RequestFilter( + since: lastDeletionSync, + limit: 99, + ).toRequest(subscriptionPrefix: 'app-bg-deletions'), + source: const RemoteSource(relays: 'AppCatalog', stream: false), + ); + await secureStorage.setDeletionsSyncedUntil(DateTime.now()); + if (deletionRequests.isNotEmpty) { + try { + await (storage as PurplebaseStorageNotifier) + .delete(deletionRequests.map((d) => d.id).toSet()); + } catch (_) { + // Orphaned kind-5 rows are harmless; referenced events are + // already removed by the DB layer during save. + } + } + // Re-query apps from local to ensure relationships are loaded final appsWithRelations = await storage.query( RequestFilter( diff --git a/lib/services/secure_storage_service.dart b/lib/services/secure_storage_service.dart index 4496c51..3c48569 100644 --- a/lib/services/secure_storage_service.dart +++ b/lib/services/secure_storage_service.dart @@ -116,6 +116,34 @@ class SecureStorageService { } } + // ========================================================================= + // Deletion Sync Timestamp (for NIP-09 incremental checks) + // ========================================================================= + + static const _deletionSyncedUntilKey = 'deletion_synced_until'; + + /// Get the timestamp of the last successful NIP-09 deletion sync. + /// Used as `since` on the next kind-5 query so checks are incremental. + Future getDeletionsSyncedUntil() async { + final value = await _storage.read(key: _deletionSyncedUntilKey); + if (int.tryParse(value ?? '') case final ms?) { + return DateTime.fromMillisecondsSinceEpoch(ms); + } + return null; + } + + /// Persist the deletion sync cursor after a successful kind-5 fetch. + Future setDeletionsSyncedUntil(DateTime time) async { + await _storage.write( + key: _deletionSyncedUntilKey, + value: '${time.millisecondsSinceEpoch}', + ); + } + + // ========================================================================= + // App Catalog Relays + // ========================================================================= + /// Store app catalog relay URLs. /// /// This is the local source of truth for relay configuration,