From 39bef4c528cfbbe51cf5681fdca8b6b65770a96e Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 29 Dec 2024 08:57:20 -0600 Subject: [PATCH 1/4] added support for decrypting private tags in kind 30000 lists --- CHANGELOG.md | 1 + Nos.xcodeproj/project.pbxproj | 8 +- .../CoreData/AuthorList+CoreDataClass.swift | 29 ++++- .../AuthorList+CoreDataProperties.swift | 15 +++ .../Nos.xcdatamodeld/.xccurrentversion | 2 +- .../Nos 23.xcdatamodel/.xccurrentversion | 8 ++ .../Nos.xcdatamodel/contents | 56 +++++++++ .../Nos 23.xcdatamodel/contents | 116 ++++++++++++++++++ Nos/Service/EventProcessor.swift | 20 ++- Nos/Service/Relay/RelayService.swift | 3 +- ...tProcessorIntegrationTests+FollowSet.swift | 37 ++++++ .../Fixtures/follow_set_private.json | 22 ++++ 12 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/.xccurrentversion create mode 100644 Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/Nos.xcdatamodel/contents create mode 100644 Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/contents create mode 100644 NosTests/IntegrationTests/Fixtures/follow_set_private.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b4f99b2..6450ef61f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added feed source customizer drop-down view. [#102](https://github.com/verse-pbc/issues/issues/102) - Make feed source selector work. - Add empty state for lists/relays drop-down. +- Added support for decrypting private tags in kind 30000 lists. ### Internal Changes - Upgraded to Xcode 16. [#1570](https://github.com/planetary-social/nos/issues/1570) diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 7f36dd904..67b910e27 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 50089A0D2C97182200834588 /* CurrentUser+PublishEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */; }; 50089A172C98678600834588 /* View+ListRowGradientBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */; }; 501728B42D16EFB000CF2A07 /* FeedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501728B32D16EFAC00CF2A07 /* FeedPicker.swift */; }; + 5022F9462D2186380012FF4B /* follow_set_private.json in Resources */ = {isa = PBXBuildFile; fileRef = 5022F9452D2186300012FF4B /* follow_set_private.json */; }; 502B6C3D2C9462A400446316 /* PushNotificationRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */; }; 503CA9532D19ACCC00805EF8 /* HorizontalLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503CA9522D19ACC800805EF8 /* HorizontalLine.swift */; }; 503CA9792D19C39F00805EF8 /* FeedCustomizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503CA9782D19C39800805EF8 /* FeedCustomizerView.swift */; }; @@ -763,6 +764,8 @@ 50089A0B2C97182200834588 /* CurrentUser+PublishEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUser+PublishEvents.swift"; sourceTree = ""; }; 50089A162C98678600834588 /* View+ListRowGradientBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ListRowGradientBackground.swift"; sourceTree = ""; }; 501728B32D16EFAC00CF2A07 /* FeedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPicker.swift; sourceTree = ""; }; + 5022F9452D2186300012FF4B /* follow_set_private.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = follow_set_private.json; sourceTree = ""; }; + 5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 23.xcdatamodel"; sourceTree = ""; }; 502B6C3C2C9462A400446316 /* PushNotificationRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationRegistrar.swift; sourceTree = ""; }; 503CA9522D19ACC800805EF8 /* HorizontalLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalLine.swift; sourceTree = ""; }; 503CA9782D19C39800805EF8 /* FeedCustomizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCustomizerView.swift; sourceTree = ""; }; @@ -1271,6 +1274,7 @@ C9BD91882B61BBEF00FDA083 /* bad_contact_list.json */, 0350F1162C0A47B20024CC15 /* contact_list.json */, 03C853C52D03A50900164D6C /* follow_set.json */, + 5022F9452D2186300012FF4B /* follow_set_private.json */, 03FFCA582D075E2800D6F0F1 /* follow_set_updated.json */, 039C96282C48321E00A8EB39 /* long_form_data.json */, C95057B02CC6986E0024EC9C /* mute_list_2.json */, @@ -2286,6 +2290,7 @@ 3AEABEFE2B2BF850001BC933 /* ImagePicker.xcstrings in Resources */, 0350F1172C0A47B20024CC15 /* contact_list.json in Resources */, C944024D2C5BE6A600834568 /* Assets.xcassets in Resources */, + 5022F9462D2186380012FF4B /* follow_set_private.json in Resources */, 03C853C62D03A50900164D6C /* follow_set.json in Resources */, C987F83729BA951E00B44E7A /* ClarityCity-ExtraBold.otf in Resources */, C987F83329BA951E00B44E7A /* ClarityCity-ExtraLight.otf in Resources */, @@ -3917,6 +3922,7 @@ C936B4572A4C7B7C00DF1EB9 /* Nos.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */, 503CAB7B2D1DA6DB00805EF8 /* Nos 22.xcdatamodel */, 0303B11E2D0257D400077929 /* Nos 21.xcdatamodel */, 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */, @@ -3932,7 +3938,7 @@ C9C547562A4F1D1A006B0741 /* Nos 9.xcdatamodel */, 5BFF66AF2A4B55FC00AA79DD /* Nos 10.xcdatamodel */, ); - currentVersion = 503CAB7B2D1DA6DB00805EF8 /* Nos 22.xcdatamodel */; + currentVersion = 5022F9472D2188650012FF4B /* Nos 23.xcdatamodel */; path = Nos.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Nos/Models/CoreData/AuthorList+CoreDataClass.swift b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift index 9957cf1d0..d9e4570d0 100644 --- a/Nos/Models/CoreData/AuthorList+CoreDataClass.swift +++ b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift @@ -1,10 +1,23 @@ import Foundation import CoreData +import NostrSDK + +/// This class is needed only as a utility. The protocol functions only work on instances, +/// (as opposed to classes in static functions). +fileprivate final class TagInterpreter: PrivateTagInterpreting, DirectMessageEncrypting { +} + +extension Keypair { + static func withNosKeyPair(_ keyPair: KeyPair) -> Keypair? { + Keypair(nsec: keyPair.nsec) + } +} @objc(AuthorList) public class AuthorList: Event { static func createOrUpdate( from jsonEvent: JSONEvent, + keyPair: KeyPair? = nil, in context: NSManagedObjectContext ) throws -> AuthorList { guard jsonEvent.kind == EventKind.followSet.rawValue else { throw AuthorListError.invalidKind } @@ -45,7 +58,21 @@ public class AuthorList: Event { authorList.listDescription = tag[safe: 1] } } - + + if !jsonEvent.content.isEmpty, + let keyPair, + let nostrSDKKeypair = Keypair.withNosKeyPair(keyPair) { + let authorIDs = TagInterpreter().valuesForPrivateTags( + from: jsonEvent.content, + withName: .pubkey, + using: nostrSDKKeypair + ) + for authorID in authorIDs { + let author = try Author.findOrCreate(by: authorID, context: context) + authorList.addToPrivateAuthors(author) + } + } + return authorList } diff --git a/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift b/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift index 1cd75f9de..c4826674a 100644 --- a/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift +++ b/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift @@ -23,6 +23,9 @@ extension AuthorList { /// The set of unique authors in this list. @NSManaged public var authors: Set + /// The set of privately listed unique authors. + @NSManaged public var privateAuthors: Set + /// Whether or not this list should be visible in the ``FeedPicker``. @NSManaged public var isFeedEnabled: Bool } @@ -41,4 +44,16 @@ extension AuthorList { @objc(removeAuthors:) @NSManaged public func removeFromAuthors(_ values: NSSet) + + @objc(addPrivateAuthorsObject:) + @NSManaged public func addToPrivateAuthors(_ value: Author) + + @objc(removePrivateAuthorsObject:) + @NSManaged public func removeFromPrivateAuthors(_ value: Author) + + @objc(addPrivateAuthors:) + @NSManaged public func addToPrivateAuthors(_ values: NSSet) + + @objc(removePrivateAuthors:) + @NSManaged public func removeFromPrivateAuthors(_ values: NSSet) } diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion index 5b7d17353..559c03b9e 100644 --- a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Nos 22.xcdatamodel + Nos 23.xcdatamodel diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/.xccurrentversion new file mode 100644 index 000000000..6c8a1eef9 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Nos.xcdatamodel + + diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/Nos.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/Nos.xcdatamodel/contents new file mode 100644 index 000000000..1a418ef2c --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/Nos.xcdatamodel/contents @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/contents new file mode 100644 index 000000000..4361b0fe3 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 23.xcdatamodel/contents @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Service/EventProcessor.swift b/Nos/Service/EventProcessor.swift index e5ff4fc06..ad024fc5a 100644 --- a/Nos/Service/EventProcessor.swift +++ b/Nos/Service/EventProcessor.swift @@ -9,14 +9,16 @@ enum EventProcessor { jsonEvent: JSONEvent, from relay: Relay?, in parseContext: NSManagedObjectContext, - skipVerification: Bool = false + skipVerification: Bool = false, + keyPair: KeyPair? = nil ) throws -> Event? { if jsonEvent.kind == EventKind.followSet.rawValue { return try saveFollowSet( jsonEvent: jsonEvent, relay: relay, parseContext: parseContext, - skipVerification: skipVerification + skipVerification: skipVerification, + keyPair: keyPair ) } else if let event = try Event.createIfNecessary(jsonEvent: jsonEvent, relay: relay, context: parseContext) { try updateEvent( @@ -67,13 +69,23 @@ enum EventProcessor { } extension EventProcessor { + + /// Creates or updates a kind 30000 Follow Set event into an ``AuthorList``. + /// - Parameters: + /// - jsonEvent: The event to parse. + /// - relay: The relay the event came from, if needed. + /// - parseContext: The context to create or update the list in. + /// - skipVerification: If true, skips verifying the signature on the event. + /// - keyPair: The keypair to use for decrypting privately listed pubkeys. + /// - Returns: The list. private static func saveFollowSet( jsonEvent: JSONEvent, relay: Relay?, parseContext: NSManagedObjectContext, - skipVerification: Bool + skipVerification: Bool, + keyPair: KeyPair? ) throws -> AuthorList { - let authorList = try AuthorList.createOrUpdate(from: jsonEvent, in: parseContext) + let authorList = try AuthorList.createOrUpdate(from: jsonEvent, keyPair: keyPair, in: parseContext) if !skipVerification { guard try authorList.verifySignature() else { parseContext.delete(authorList) diff --git a/Nos/Service/Relay/RelayService.swift b/Nos/Service/Relay/RelayService.swift index 93537f642..b9a89e35a 100644 --- a/Nos/Service/Relay/RelayService.swift +++ b/Nos/Service/Relay/RelayService.swift @@ -391,13 +391,14 @@ extension RelayService { return false } else { let remainingEventCount = await parseQueue.count + let keyPair = await currentUser.keyPair try await persistenceController.parseContext.perform { var savedEvents = 0 for (event, socket) in eventData { let relay = self.relay(from: socket, in: self.persistenceController.parseContext) do { let context = self.persistenceController.parseContext - if try EventProcessor.parse(jsonEvent: event, from: relay, in: context) != nil { + if try EventProcessor.parse(jsonEvent: event, from: relay, in: context, keyPair: keyPair) != nil { savedEvents += 1 } } catch { diff --git a/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift b/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift index 4a40a8454..e59093dea 100644 --- a/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift +++ b/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift @@ -73,4 +73,41 @@ extension EventProcessorIntegrationTests { let authorPubKey = try XCTUnwrap(followSet.authors.first?.hexadecimalPublicKey) XCTAssertEqual(authorPubKey, "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549") } + + @MainActor func test_parse_kind_30000_with_private_tags() throws { + // Arrange + let replaceableID = "PrivateOnly" + let ownerPubKey = "ffd99f9e545b53e3291dab4b8cd6d25d12b9973c40f02e1938c0891d62e38e57" + let data = try jsonData(filename: "follow_set_private") + let jsonEvent = try JSONDecoder().decode(JSONEvent.self, from: data) + + // Act + _ = try EventProcessor.parse( + jsonEvent: jsonEvent, + from: nil, + in: testContext, + skipVerification: true, + keyPair: KeyPair(nsec: "nsec17vdaesh5tp6u5dy74vdy7a7e5x5ww4wfdnrn6ewgnsfxav8pcurqnlmj88") + ) + + // Assert + let fetchResults = try testContext.fetch( + AuthorList.authorList( + by: replaceableID, + owner: try Author.findOrCreate(by: ownerPubKey, context: testContext), + kind: EventKind.followSet.rawValue + ) + ) + XCTAssertEqual(fetchResults.count, 1) + + let followSet = try XCTUnwrap(fetchResults.first) + XCTAssertTrue(followSet.authors.isEmpty) + + XCTAssertEqual(followSet.privateAuthors.count, 3) + + let publicKeys = followSet.privateAuthors.map { $0.hexadecimalPublicKey } + XCTAssertTrue(publicKeys.contains("3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")) + XCTAssertTrue(publicKeys.contains("3743244390be53473a7e3b3b8d04dce83f6c9514b81a997fb3b123c072ef9f78")) + XCTAssertTrue(publicKeys.contains("fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52")) + } } diff --git a/NosTests/IntegrationTests/Fixtures/follow_set_private.json b/NosTests/IntegrationTests/Fixtures/follow_set_private.json new file mode 100644 index 000000000..d7e39f3c1 --- /dev/null +++ b/NosTests/IntegrationTests/Fixtures/follow_set_private.json @@ -0,0 +1,22 @@ +{ + "id": "642c786d6435f39833d5c045d76058b1607ed17e2ca44170da0a6d363e2b1797", + "pubkey": "ffd99f9e545b53e3291dab4b8cd6d25d12b9973c40f02e1938c0891d62e38e57", + "tags": [ + [ + "title", + "Private Only" + ], + [ + "d", + "PrivateOnly" + ], + [ + "description", + "This is a list for testing lists." + ] + ], + "content": "bKKE7/Xi/uBxRfCoAb0eQLUiwelbJrGVEkmL6JGHE2maGn6rp3QGQeU+pecz54B6aCUYKoyimzB0wSTXXuhirgeBtS4pTSH6QO4KHPSZJeRh8GDcr3e9bexATBb9X5R/tpuVb6Fgv0vYFAso3t9cmO5YCAM0O2H6fd/Fj/MyYp+3VVz0rOXi6MMuBmswmD2MbSq5OwmZqY7sC77F0BOkZceWQzUHmVXcmBSMz0t0DeVFq4DW395s9E0VKMTJPzejGYlF/+Eg/E9mBRVd7mLMEBZtf/3Rmq+hKGiladFFpE8=?iv=dL08cLM4R/gNzpoLlWWxdA==", + "sig": "7b95fce1ce83f4f116d74a105afc07984f39342fbbb483b1b99e0f757106538cf6c42e2d8b0837e4e6b40fc6fc37e82cc6c56d16ecab1ab109a11fd3640d7fb8", + "created_at": 1735477908, + "kind": 30000 +} From 3a4142190fa62fdac43ea4a986569987d547e223 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 29 Dec 2024 09:00:38 -0600 Subject: [PATCH 2/4] added privately listed authors to Home feed when list is selected --- Nos/Views/Home/HomeFeedView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Nos/Views/Home/HomeFeedView.swift b/Nos/Views/Home/HomeFeedView.swift index 135a3ecf7..819e5fd85 100644 --- a/Nos/Views/Home/HomeFeedView.swift +++ b/Nos/Views/Home/HomeFeedView.swift @@ -54,7 +54,8 @@ struct HomeFeedView: View { var filter = Filter(kinds: [.text, .delete, .repost, .longFormContent, .report]) if feedController.selectedRelay == nil { if let list = feedController.selectedList { - filter.authorKeys = list.authors.compactMap { $0.hexadecimalPublicKey }.filter { $0.isValid } + let authors = list.authors.union(list.privateAuthors) + filter.authorKeys = authors.compactMap { $0.hexadecimalPublicKey }.filter { $0.isValid } } else { filter.authorKeys = user.followedKeys.sorted() } From d3490d3701c9f0928fceee4c0c3e75b4dd2b5645 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 29 Dec 2024 09:08:26 -0600 Subject: [PATCH 3/4] improve access to lists' public and private authors --- Nos/Models/CoreData/AuthorList+CoreDataClass.swift | 4 ++++ Nos/Views/Home/HomeFeedView.swift | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Nos/Models/CoreData/AuthorList+CoreDataClass.swift b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift index d9e4570d0..debf4d2d3 100644 --- a/Nos/Models/CoreData/AuthorList+CoreDataClass.swift +++ b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift @@ -92,4 +92,8 @@ public class AuthorList: Event { fetchRequest.fetchLimit = 1 return fetchRequest } + + var allAuthors: Set { + authors.union(privateAuthors) + } } diff --git a/Nos/Views/Home/HomeFeedView.swift b/Nos/Views/Home/HomeFeedView.swift index 819e5fd85..397ac26a1 100644 --- a/Nos/Views/Home/HomeFeedView.swift +++ b/Nos/Views/Home/HomeFeedView.swift @@ -38,7 +38,7 @@ struct HomeFeedView: View { for: user, before: refreshController.lastRefreshDate, seenOn: feedController.selectedRelay, - from: feedController.selectedList?.authors + from: feedController.selectedList?.allAuthors ) } @@ -54,8 +54,7 @@ struct HomeFeedView: View { var filter = Filter(kinds: [.text, .delete, .repost, .longFormContent, .report]) if feedController.selectedRelay == nil { if let list = feedController.selectedList { - let authors = list.authors.union(list.privateAuthors) - filter.authorKeys = authors.compactMap { $0.hexadecimalPublicKey }.filter { $0.isValid } + filter.authorKeys = list.allAuthors.compactMap { $0.hexadecimalPublicKey }.filter { $0.isValid } } else { filter.authorKeys = user.followedKeys.sorted() } From 8e8a30913f19513efa268166e237197326a06d62 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Sun, 29 Dec 2024 09:10:04 -0600 Subject: [PATCH 4/4] lint fix --- Nos/Service/Relay/RelayService.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Nos/Service/Relay/RelayService.swift b/Nos/Service/Relay/RelayService.swift index b9a89e35a..d258270ff 100644 --- a/Nos/Service/Relay/RelayService.swift +++ b/Nos/Service/Relay/RelayService.swift @@ -398,7 +398,12 @@ extension RelayService { let relay = self.relay(from: socket, in: self.persistenceController.parseContext) do { let context = self.persistenceController.parseContext - if try EventProcessor.parse(jsonEvent: event, from: relay, in: context, keyPair: keyPair) != nil { + if try EventProcessor.parse( + jsonEvent: event, + from: relay, + in: context, + keyPair: keyPair + ) != nil { savedEvents += 1 } } catch {