diff --git a/CHANGELOG.md b/CHANGELOG.md index dca546360..f5cbd127a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Internal Changes - Upgraded to Xcode 16. [#1570](https://github.com/planetary-social/nos/issues/1570) +- Download and parse an author’s lists when viewing their profile. [#49](https://github.com/verse-pbc/issues/issues/49) ## [1.0.3] - 2024-12-04Z diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index bcaa8ede6..b1cf16735 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 030036942C5D3AD3002C71F5 /* RefreshController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030036842C5D39DD002C71F5 /* RefreshController.swift */; }; 030036AB2C5D872B002C71F5 /* NewNotesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030036AA2C5D872B002C71F5 /* NewNotesButton.swift */; }; 0301495C2CFFA8B7000A0152 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0301495B2CFFA8B7000A0152 /* TabBarController.swift */; }; + 0303B1532D025C9A00077929 /* AuthorList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0303B13E2D025BDD00077929 /* AuthorList+CoreDataProperties.swift */; }; + 0303B1542D025C9A00077929 /* AuthorList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0303B13E2D025BDD00077929 /* AuthorList+CoreDataProperties.swift */; }; 0304D0A72C9B4BF2001D16C7 /* OpenGraphMetatdata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304D0A62C9B4BF2001D16C7 /* OpenGraphMetatdata.swift */; }; 0304D0A82C9B4BF2001D16C7 /* OpenGraphMetatdata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304D0A62C9B4BF2001D16C7 /* OpenGraphMetatdata.swift */; }; 0304D0B22C9B731F001D16C7 /* MockOpenGraphService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304D0B12C9B731F001D16C7 /* MockOpenGraphService.swift */; }; @@ -41,6 +43,9 @@ 032634702C10C40B00E489B5 /* NostrBuildAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0326346F2C10C40B00E489B5 /* NostrBuildAPIClientTests.swift */; }; 0326347A2C10C57A00E489B5 /* FileStorageAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032634792C10C57A00E489B5 /* FileStorageAPIClient.swift */; }; 0326347B2C10C57A00E489B5 /* FileStorageAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032634792C10C57A00E489B5 /* FileStorageAPIClient.swift */; }; + 033C19DC2D03A34F00B5529D /* EventProcessorIntegrationTests+FollowSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033C19DB2D03A34F00B5529D /* EventProcessorIntegrationTests+FollowSet.swift */; }; + 033E940B2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E940A2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift */; }; + 033E940C2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E940A2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift */; }; 0348342A2C9A02FC0050CF51 /* MockOpenGraphParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034834292C9A02FC0050CF51 /* MockOpenGraphParser.swift */; }; 034EBDBA2C24895E006BA35A /* CurrentUserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034EBDB92C24895E006BA35A /* CurrentUserError.swift */; }; 034EBDC72C2489B4006BA35A /* CurrentUserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034EBDB92C24895E006BA35A /* CurrentUserError.swift */; }; @@ -116,6 +121,7 @@ 03C7E7922CB9C0B30054624C /* WelcomeToFeedTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C7E7912CB9C0AF0054624C /* WelcomeToFeedTip.swift */; }; 03C7E7982CB9C16C0054624C /* InlineTipViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C7E7972CB9C1600054624C /* InlineTipViewStyle.swift */; }; 03C7E7A22CB9CD150054624C /* PointDownEmojiTipViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C7E7A12CB9CD0B0054624C /* PointDownEmojiTipViewStyle.swift */; }; + 03C853C62D03A50900164D6C /* follow_set.json in Resources */ = {isa = PBXBuildFile; fileRef = 03C853C52D03A50900164D6C /* follow_set.json */; }; 03C8B4962C6D065900A07CCD /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C8B4952C6D065900A07CCD /* ImageViewer.swift */; }; 03D1B4282C3C1A5D001778CD /* NostrIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1B4272C3C1A5D001778CD /* NostrIdentifier.swift */; }; 03D1B4292C3C1AC9001778CD /* NostrIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1B4272C3C1A5D001778CD /* NostrIdentifier.swift */; }; @@ -140,6 +146,10 @@ 03FE3F7C2C87AC9900D25810 /* Event+InlineMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */; }; 03FE3F7D2C87AC9900D25810 /* Event+InlineMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */; }; 03FE3F8C2C87BC9500D25810 /* text_note_multiple_media.json in Resources */ = {isa = PBXBuildFile; fileRef = 03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */; }; + 03FFCA592D075E2800D6F0F1 /* follow_set_updated.json in Resources */ = {isa = PBXBuildFile; fileRef = 03FFCA582D075E2800D6F0F1 /* follow_set_updated.json */; }; + 03FFCA7B2D07721100D6F0F1 /* AuthorListError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FFCA7A2D07720C00D6F0F1 /* AuthorListError.swift */; }; + 03FFCA7C2D07721100D6F0F1 /* AuthorListError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FFCA7A2D07720C00D6F0F1 /* AuthorListError.swift */; }; + 03FFCA7E2D07729400D6F0F1 /* AuthorListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FFCA7D2D07729200D6F0F1 /* AuthorListTests.swift */; }; 041C56C42CA1B48E007D3BB2 /* UserFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041C56C32CA1B48E007D3BB2 /* UserFlagView.swift */; }; 042406F32C907A15008F2A21 /* NosToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042406F22C907A15008F2A21 /* NosToggle.swift */; }; 04368D2B2C99A2C400DEAA2E /* FlagOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04368D2A2C99A2C400DEAA2E /* FlagOption.swift */; }; @@ -604,7 +614,9 @@ 030024182CC00DF70073ED56 /* SplashScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; 030036842C5D39DD002C71F5 /* RefreshController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshController.swift; sourceTree = ""; }; 030036AA2C5D872B002C71F5 /* NewNotesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNotesButton.swift; sourceTree = ""; }; + 0303B11E2D0257D400077929 /* Nos 21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 21.xcdatamodel"; sourceTree = ""; }; 0301495B2CFFA8B7000A0152 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; + 0303B13E2D025BDD00077929 /* AuthorList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthorList+CoreDataProperties.swift"; sourceTree = ""; }; 0304D0A62C9B4BF2001D16C7 /* OpenGraphMetatdata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGraphMetatdata.swift; sourceTree = ""; }; 0304D0B12C9B731F001D16C7 /* MockOpenGraphService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOpenGraphService.swift; sourceTree = ""; }; 030AE4282BE3D63C004DEE02 /* FeaturedAuthor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedAuthor.swift; sourceTree = ""; }; @@ -629,6 +641,8 @@ 0326346F2C10C40B00E489B5 /* NostrBuildAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrBuildAPIClientTests.swift; sourceTree = ""; }; 032634792C10C57A00E489B5 /* FileStorageAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorageAPIClient.swift; sourceTree = ""; }; 033B288D2C419E7600E325E8 /* Nos 16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Nos 16.xcdatamodel"; sourceTree = ""; }; + 033C19DB2D03A34F00B5529D /* EventProcessorIntegrationTests+FollowSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventProcessorIntegrationTests+FollowSet.swift"; sourceTree = ""; }; + 033E940A2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthorList+CoreDataClass.swift"; sourceTree = ""; }; 034834292C9A02FC0050CF51 /* MockOpenGraphParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOpenGraphParser.swift; sourceTree = ""; }; 034EBDB92C24895E006BA35A /* CurrentUserError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUserError.swift; sourceTree = ""; }; 0350F10B2C0A46760024CC15 /* new_contact_list.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = new_contact_list.json; sourceTree = ""; }; @@ -684,6 +698,7 @@ 03C7E7912CB9C0AF0054624C /* WelcomeToFeedTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeToFeedTip.swift; sourceTree = ""; }; 03C7E7972CB9C1600054624C /* InlineTipViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTipViewStyle.swift; sourceTree = ""; }; 03C7E7A12CB9CD0B0054624C /* PointDownEmojiTipViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointDownEmojiTipViewStyle.swift; sourceTree = ""; }; + 03C853C52D03A50900164D6C /* follow_set.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = follow_set.json; sourceTree = ""; }; 03C8B4952C6D065900A07CCD /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = ""; }; 03D1B4272C3C1A5D001778CD /* NostrIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrIdentifier.swift; sourceTree = ""; }; 03D1B42B2C3C1B0D001778CD /* TLVElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVElement.swift; sourceTree = ""; }; @@ -699,6 +714,9 @@ 03FE3F782C87A9D900D25810 /* EventError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventError.swift; sourceTree = ""; }; 03FE3F7B2C87AC9900D25810 /* Event+InlineMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+InlineMetadata.swift"; sourceTree = ""; }; 03FE3F8A2C87BC9500D25810 /* text_note_multiple_media.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = text_note_multiple_media.json; sourceTree = ""; }; + 03FFCA582D075E2800D6F0F1 /* follow_set_updated.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = follow_set_updated.json; sourceTree = ""; }; + 03FFCA7A2D07720C00D6F0F1 /* AuthorListError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorListError.swift; sourceTree = ""; }; + 03FFCA7D2D07729200D6F0F1 /* AuthorListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorListTests.swift; sourceTree = ""; }; 041C56C32CA1B48E007D3BB2 /* UserFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlagView.swift; sourceTree = ""; }; 042406F22C907A15008F2A21 /* NosToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NosToggle.swift; sourceTree = ""; }; 04368D2A2C99A2C400DEAA2E /* FlagOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagOption.swift; sourceTree = ""; }; @@ -1077,6 +1095,7 @@ isa = PBXGroup; children = ( C9DEC04329894BED0078B43A /* Author+CoreDataClass.swift */, + 033E940A2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift */, 3F43C47529A9625700E896A0 /* AuthorReference+CoreDataClass.swift */, C9DEC03F29894BED0078B43A /* Event+CoreDataClass.swift */, 5044546D2C90726A00251A7E /* Event+Fetching.swift */, @@ -1166,6 +1185,7 @@ isa = PBXGroup; children = ( 0350F1202C0A490E0024CC15 /* EventProcessorIntegrationTests.swift */, + 033C19DB2D03A34F00B5529D /* EventProcessorIntegrationTests+FollowSet.swift */, 031D0BB32C826F8400D95342 /* EventProcessorIntegrationTests+InlineMetadata.swift */, 03618AF72C82583D00BCBC55 /* Fixtures */, ); @@ -1233,12 +1253,14 @@ 03618AF72C82583D00BCBC55 /* Fixtures */ = { isa = PBXGroup; children = ( - C95057C42CC69A770024EC9C /* mute_list_self.json */, - C95057B02CC6986E0024EC9C /* mute_list_2.json */, - C95057A62CC692320024EC9C /* mute_list.json */, C9BD91882B61BBEF00FDA083 /* bad_contact_list.json */, 0350F1162C0A47B20024CC15 /* contact_list.json */, + 03C853C52D03A50900164D6C /* follow_set.json */, + 03FFCA582D075E2800D6F0F1 /* follow_set_updated.json */, 039C96282C48321E00A8EB39 /* long_form_data.json */, + C95057B02CC6986E0024EC9C /* mute_list_2.json */, + C95057C42CC69A770024EC9C /* mute_list_self.json */, + C95057A62CC692320024EC9C /* mute_list.json */, 0350F10B2C0A46760024CC15 /* new_contact_list.json */, 0373CE7F2C08DBC40027C856 /* old_contact_list.json */, C9DEC005298947900078B43A /* sample_data.json */, @@ -1379,6 +1401,7 @@ 03618C8D2C826AB300BCBC55 /* CoreData */ = { isa = PBXGroup; children = ( + 03FFCA7D2D07729200D6F0F1 /* AuthorListTests.swift */, 035729A12BE4167E005FEE85 /* AuthorTests.swift */, 035729A32BE4167E005FEE85 /* EventTests.swift */, 035729A42BE4167E005FEE85 /* FollowTests.swift */, @@ -1416,6 +1439,7 @@ isa = PBXGroup; children = ( C973AB582A323167002AED16 /* Author+CoreDataProperties.swift */, + 0303B13E2D025BDD00077929 /* AuthorList+CoreDataProperties.swift */, C973AB572A323167002AED16 /* AuthorReference+CoreDataProperties.swift */, C973AB562A323167002AED16 /* Event+CoreDataProperties.swift */, C973AB5A2A323167002AED16 /* EventReference+CoreDataProperties.swift */, @@ -1906,6 +1930,7 @@ C9DEC02C29894BB20078B43A /* Models */ = { isa = PBXGroup; children = ( + 03FFCA7A2D07720C00D6F0F1 /* AuthorListError.swift */, C9F2047F2AE029D90029A858 /* AppDestination.swift */, 03FE3F782C87A9D900D25810 /* EventError.swift */, 0365CD862C4016A200622A1A /* EventKind.swift */, @@ -2239,6 +2264,7 @@ 3AEABEFE2B2BF850001BC933 /* ImagePicker.xcstrings in Resources */, 0350F1172C0A47B20024CC15 /* contact_list.json in Resources */, C944024D2C5BE6A600834568 /* Assets.xcassets in Resources */, + 03C853C62D03A50900164D6C /* follow_set.json in Resources */, C987F83729BA951E00B44E7A /* ClarityCity-ExtraBold.otf in Resources */, C987F83329BA951E00B44E7A /* ClarityCity-ExtraLight.otf in Resources */, C987F83D29BA951E00B44E7A /* ClarityCity-Bold.otf in Resources */, @@ -2260,6 +2286,7 @@ 03B4E6A22C125CA1006E5F59 /* nostr_build_nip96_upload_response.json in Resources */, 0314CF742C9C7DD00001A53B /* youTube_fortnight_short.html in Resources */, 5095330C2C625B5D00E0BACA /* zap_request_no_amount.json in Resources */, + 03FFCA592D075E2800D6F0F1 /* follow_set_updated.json in Resources */, C987F84F29BA951E00B44E7A /* ClarityCity-RegularItalic.otf in Resources */, CD27177629A7C8B200AE8888 /* sample_replies.json in Resources */, C987F83B29BA951E00B44E7A /* ClarityCity-BoldItalic.otf in Resources */, @@ -2404,12 +2431,14 @@ 030E571B2CC2ADDB00A4A51E /* SaveProfileError.swift in Sources */, C94A5E182A72C84200B6EC5D /* ReportCategory.swift in Sources */, C9A8015E2BD0177D006E29B2 /* ReportPublisher.swift in Sources */, + 03FFCA7B2D07721100D6F0F1 /* AuthorListError.swift in Sources */, C9F0BB6B29A503D6000547FC /* PublicKey.swift in Sources */, C9EF84CF2C24D63000182B6F /* MockRelayService.swift in Sources */, 5B79F6192B98B24C002DA9BE /* DeleteUsernameWizard.swift in Sources */, 5044546E2C90726A00251A7E /* Event+Fetching.swift in Sources */, C9EE3E602A0538B7008A7491 /* ExpirationTimeButton.swift in Sources */, 03FE3F7C2C87AC9900D25810 /* Event+InlineMetadata.swift in Sources */, + 0303B1532D025C9A00077929 /* AuthorList+CoreDataProperties.swift in Sources */, A303AF8329A9153A005DC8FC /* FollowButton.swift in Sources */, 65D066992BD558690011C5CD /* DirectMessageWrapper.swift in Sources */, 504454702C90728500251A7E /* Event+Hydration.swift in Sources */, @@ -2571,6 +2600,7 @@ CD09A74629A50F750063464F /* SideMenuContent.swift in Sources */, 030E56F32CC2836D00A4A51E /* CopyKeyView.swift in Sources */, C9DFA971299BF8CD006929C1 /* NoteView.swift in Sources */, + 033E940B2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift in Sources */, 037071272C90C5FA00BEAEC4 /* OpenGraphService.swift in Sources */, C974652E2A3B86600031226F /* NoteCardHeader.swift in Sources */, C9CF23172A38A58B00EBEC31 /* ParseQueue.swift in Sources */, @@ -2631,6 +2661,7 @@ CD09A75F29A521FD0063464F /* RelayService.swift in Sources */, C9EF84D02C24D63000182B6F /* MockRelayService.swift in Sources */, CD09A76029A521FD0063464F /* Filter.swift in Sources */, + 033C19DC2D03A34F00B5529D /* EventProcessorIntegrationTests+FollowSet.swift in Sources */, C9B71DC32A9003670031ED9F /* CrashReporting.swift in Sources */, C9C2B78329E0735400548B4A /* RelaySubscriptionManager.swift in Sources */, C9C2B78029E0731600548B4A /* AsyncTimer.swift in Sources */, @@ -2648,6 +2679,7 @@ 5BD813A32C8BA7CC00E65F4D /* PreviewEventRepository.swift in Sources */, C9EE3E642A053910008A7491 /* ExpirationTimeOption.swift in Sources */, 504454712C90728E00251A7E /* Event+Fetching.swift in Sources */, + 03FFCA7C2D07721100D6F0F1 /* AuthorListError.swift in Sources */, 65D066AA2BD55E160011C5CD /* DirectMessageWrapper.swift in Sources */, C973AB5E2A323167002AED16 /* Event+CoreDataProperties.swift in Sources */, C9F64D8D29ED840700563F2B /* Zipper.swift in Sources */, @@ -2658,6 +2690,7 @@ 5B88051D2A2104CC00E21F06 /* SHA256Key.swift in Sources */, 0365CD902C40171100622A1A /* EventKind.swift in Sources */, C9CF23182A38A58B00EBEC31 /* ParseQueue.swift in Sources */, + 03FFCA7E2D07729400D6F0F1 /* AuthorListTests.swift in Sources */, 037975C72C0E26FC00ADDF37 /* Font+Clarity.swift in Sources */, 5B39E64429EDBF8100464830 /* NoteParser.swift in Sources */, 035729CA2BE4173E005FEE85 /* PreviewData.swift in Sources */, @@ -2766,6 +2799,7 @@ 0315B5F02C7E451C0020E707 /* MockMediaService.swift in Sources */, C9646EAA29B7A506007239A4 /* Analytics.swift in Sources */, C9736E5E2C13B718005BCE70 /* EventFixture.swift in Sources */, + 033E940C2D08F14900C6FB03 /* AuthorList+CoreDataClass.swift in Sources */, 035729AF2BE4167E005FEE85 /* KeyPairTests.swift in Sources */, 0383CD722C767966007DB0E4 /* LinearGradient+Planetary.swift in Sources */, 035729AE2BE4167E005FEE85 /* FollowTests.swift in Sources */, @@ -2791,6 +2825,7 @@ C93005602A6AF8320098CA9E /* LoadingContent.swift in Sources */, C97A1C8F29E58EC7009D9E8D /* NSManagedObjectContext+Nos.swift in Sources */, C9ADB135299288230075E7F8 /* KeyFixture.swift in Sources */, + 0303B1542D025C9A00077929 /* AuthorList+CoreDataProperties.swift in Sources */, C9C547552A4F1CDB006B0741 /* SearchController.swift in Sources */, 3FFB1D9429A6BBCE002A755D /* EventReference+CoreDataClass.swift in Sources */, C9BD919B2B61C4FB00FDA083 /* RawNostrID+Random.swift in Sources */, @@ -3853,6 +3888,7 @@ C936B4572A4C7B7C00DF1EB9 /* Nos.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 0303B11E2D0257D400077929 /* Nos 21.xcdatamodel */, 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */, C95057C62CC69FD70024EC9C /* Nos 19.xcdatamodel */, C9BB9FE32CBEFF560045DC5A /* Nos 18.xcdatamodel */, @@ -3866,7 +3902,7 @@ C9C547562A4F1D1A006B0741 /* Nos 9.xcdatamodel */, 5BFF66AF2A4B55FC00AA79DD /* Nos 10.xcdatamodel */, ); - currentVersion = 2D3C71A52CEE6F7100625BCB /* Nos 20.xcdatamodel */; + currentVersion = 0303B11E2D0257D400077929 /* Nos 21.xcdatamodel */; path = Nos.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Nos/Models/AuthorListError.swift b/Nos/Models/AuthorListError.swift new file mode 100644 index 000000000..d64502d81 --- /dev/null +++ b/Nos/Models/AuthorListError.swift @@ -0,0 +1,13 @@ +import Foundation + +/// Errors for an ``AuthorList``. +enum AuthorListError: LocalizedError, Equatable { + /// The event kind is invalid; that is, an ``AuthorList`` can't be created with the given kind. + case invalidKind + + /// The signature is invalid. + case invalidSignature(AuthorList) + + /// The replaceable ID is missing (the `d` tag from the JSON event). + case missingReplaceableID +} diff --git a/Nos/Models/CoreData/AuthorList+CoreDataClass.swift b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift new file mode 100644 index 000000000..9957cf1d0 --- /dev/null +++ b/Nos/Models/CoreData/AuthorList+CoreDataClass.swift @@ -0,0 +1,68 @@ +import Foundation +import CoreData + +@objc(AuthorList) +public class AuthorList: Event { + static func createOrUpdate( + from jsonEvent: JSONEvent, + in context: NSManagedObjectContext + ) throws -> AuthorList { + guard jsonEvent.kind == EventKind.followSet.rawValue else { throw AuthorListError.invalidKind } + guard let replaceableID = jsonEvent.replaceableID else { throw AuthorListError.missingReplaceableID } + let owner = try Author.findOrCreate(by: jsonEvent.pubKey, context: context) + + // Fetch existing AuthorList if it exists + let fetchRequest = AuthorList.authorList(by: replaceableID, owner: owner, kind: EventKind.followSet.rawValue) + let existingAuthorList = try context.fetch(fetchRequest).first + existingAuthorList?.authors = Set() + + let authorList = existingAuthorList ?? AuthorList(context: context) + authorList.createdAt = jsonEvent.createdDate + authorList.author = owner + authorList.owner = owner + authorList.identifier = jsonEvent.id + authorList.replaceableIdentifier = replaceableID + authorList.kind = jsonEvent.kind + authorList.signature = jsonEvent.signature + authorList.allTags = jsonEvent.tags as NSObject + authorList.content = jsonEvent.content + + let tags = jsonEvent.tags + + for tag in tags { + if tag[safe: 0] == "p", let authorID = tag[safe: 1] { + let author = try Author.findOrCreate(by: authorID, context: context) + authorList.addToAuthors(author) + } else if tag[safe: 0] == "title" { + authorList.title = tag[safe: 1] + } else if tag[safe: 0] == "image" { + if let urlString = tag[safe: 1] { + authorList.image = URL(string: urlString) + } else { + authorList.image = nil + } + } else if tag[safe: 0] == "description" { + authorList.listDescription = tag[safe: 1] + } + } + + return authorList + } + + @nonobjc public class func authorList( + by replaceableID: RawReplaceableID, + owner: Author, + kind: Int64 + ) -> NSFetchRequest { + let fetchRequest = NSFetchRequest(entityName: "AuthorList") + fetchRequest.predicate = NSPredicate( + format: "replaceableIdentifier = %@ AND owner = %@ AND kind = %i", + replaceableID, + owner, + kind + ) + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AuthorList.identifier, ascending: true)] + fetchRequest.fetchLimit = 1 + return fetchRequest + } +} diff --git a/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift b/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift new file mode 100644 index 000000000..71c58f3e7 --- /dev/null +++ b/Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift @@ -0,0 +1,41 @@ +import Foundation +import CoreData + +extension AuthorList { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "AuthorList") + } + + /// The URL of an image representing the list. + @NSManaged public var image: URL? + + /// The description of the list. + @NSManaged public var listDescription: String? + + /// The title of the list. + @NSManaged public var title: String? + + /// The owner of the list; the ``Author`` who created it. + /// Duplicates ``author`` but Core Data won't allow for multiple relationships to have the same inverse. + @NSManaged public var owner: Author? + + /// The set of unique authors in this list. + @NSManaged public var authors: Set +} + +// MARK: Generated accessors for authors +extension AuthorList { + + @objc(addAuthorsObject:) + @NSManaged public func addToAuthors(_ value: Author) + + @objc(removeAuthorsObject:) + @NSManaged public func removeFromAuthors(_ value: Author) + + @objc(addAuthors:) + @NSManaged public func addToAuthors(_ values: NSSet) + + @objc(removeAuthors:) + @NSManaged public func removeFromAuthors(_ values: NSSet) +} diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion index d7c309e78..eb66b2858 100644 --- a/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Nos 20.xcdatamodel + Nos 21.xcdatamodel diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/.xccurrentversion b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/.xccurrentversion new file mode 100644 index 000000000..6c8a1eef9 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Nos.xcdatamodel + + diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/Nos.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/Nos.xcdatamodel/contents new file mode 100644 index 000000000..1a418ef2c --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/Nos.xcdatamodel/contents @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/contents b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/contents new file mode 100644 index 000000000..b85c6c528 --- /dev/null +++ b/Nos/Models/CoreData/Nos.xcdatamodeld/Nos 21.xcdatamodel/contents @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nos/Models/EventKind.swift b/Nos/Models/EventKind.swift index b5e21eeae..b4be1ce86 100644 --- a/Nos/Models/EventKind.swift +++ b/Nos/Models/EventKind.swift @@ -35,7 +35,7 @@ public enum EventKind: Int64, CaseIterable, Hashable { /// Request to Vanish case requestToVanish = 62 - + /// Gift Wrap case giftWrap = 1059 @@ -50,19 +50,26 @@ public enum EventKind: Int64, CaseIterable, Hashable { /// Zap Request case zapRequest = 9734 - + /// Zap Receipt case zapReceipt = 9735 - + + // swiftlint:disable number_separator + /// Mute List - case mute = 10_000 + case mute = 10000 + + /// NIP-42 Relay Authentication + case relayAuth = 22242 /// NIP-98 HTTP Authentication - case httpAuth = 27_235 - - // NIP-42 Relay Authentication - case relayAuth = 22_242 + case httpAuth = 27235 + + /// NIP-51 Follow Set + case followSet = 30000 /// Long-form Content - case longFormContent = 30_023 + case longFormContent = 30023 + + // swiftlint:enable number_separator } diff --git a/Nos/Service/EventProcessor.swift b/Nos/Service/EventProcessor.swift index d0e132d69..e5ff4fc06 100644 --- a/Nos/Service/EventProcessor.swift +++ b/Nos/Service/EventProcessor.swift @@ -11,40 +11,22 @@ enum EventProcessor { in parseContext: NSManagedObjectContext, skipVerification: Bool = false ) throws -> Event? { - if let event = try Event.createIfNecessary(jsonEvent: jsonEvent, relay: relay, context: parseContext) { - relay.unwrap { - do { - try event.trackDelete(on: $0, context: parseContext) - } catch { - Log.error(error.localizedDescription) - } - } - - guard let publicKey = event.author?.publicKey else { - throw EventError.missingAuthor - } - - if skipVerification == false { - guard try event.verifySignature(for: publicKey) else { - parseContext.delete(event) - Log.info("Invalid signature on event: \(jsonEvent) from \(relay?.address ?? "error")") - throw EventError.invalidSignature(event) - } - event.isVerified = true - } - - // if this is a zap receipt, pull the zap request out of the description tag and parse it as well - if event.kind == EventKind.zapReceipt.rawValue, - let tags = event.allTags as? [[String]], - let descriptionTag = tags.first(where: { $0.first == "description" }), - let zapRequestJSONData = descriptionTag.last?.data(using: .utf8) { - let zapRequest = try JSONDecoder().decode(JSONEvent.self, from: zapRequestJSONData) - try parse(jsonEvent: zapRequest, from: relay, in: parseContext, skipVerification: skipVerification) - } - + if jsonEvent.kind == EventKind.followSet.rawValue { + return try saveFollowSet( + jsonEvent: jsonEvent, + relay: relay, + parseContext: parseContext, + skipVerification: skipVerification + ) + } else if let event = try Event.createIfNecessary(jsonEvent: jsonEvent, relay: relay, context: parseContext) { + try updateEvent( + event: event, + jsonEvent: jsonEvent, + relay: relay, + parseContext: parseContext, + skipVerification: skipVerification + ) return event - - // Verify that this event has been marked seen on the given relay. } else if let relay, try parseContext.count(for: Event.event(by: jsonEvent.id, seenOn: relay)) == 0, let event = Event.find(by: jsonEvent.id, context: parseContext) { @@ -83,3 +65,61 @@ enum EventProcessor { return try parse(jsonData: jsonData, from: relay, in: parseContext) } } + +extension EventProcessor { + private static func saveFollowSet( + jsonEvent: JSONEvent, + relay: Relay?, + parseContext: NSManagedObjectContext, + skipVerification: Bool + ) throws -> AuthorList { + let authorList = try AuthorList.createOrUpdate(from: jsonEvent, in: parseContext) + if !skipVerification { + guard try authorList.verifySignature() else { + parseContext.delete(authorList) + Log.info("Invalid signature on author list: \(jsonEvent) from \(relay?.address ?? "error")") + throw AuthorListError.invalidSignature(authorList) + } + authorList.isVerified = true + } + return authorList + } + + private static func updateEvent( + event: Event, + jsonEvent: JSONEvent, + relay: Relay?, + parseContext: NSManagedObjectContext, + skipVerification: Bool + ) throws { + relay.unwrap { + do { + try event.trackDelete(on: $0, context: parseContext) + } catch { + Log.error(error.localizedDescription) + } + } + + guard let publicKey = event.author?.publicKey else { + throw EventError.missingAuthor + } + + if !skipVerification { + guard try event.verifySignature(for: publicKey) else { + parseContext.delete(event) + Log.info("Invalid signature on event: \(jsonEvent) from \(relay?.address ?? "error")") + throw EventError.invalidSignature(event) + } + event.isVerified = true + } + + // if this is a zap receipt, pull the zap request out of the description tag and parse it as well + if event.kind == EventKind.zapReceipt.rawValue, + let tags = event.allTags as? [[String]], + let descriptionTag = tags.first(where: { $0.first == "description" }), + let zapRequestJSONData = descriptionTag.last?.data(using: .utf8) { + let zapRequest = try JSONDecoder().decode(JSONEvent.self, from: zapRequestJSONData) + try parse(jsonEvent: zapRequest, from: relay, in: parseContext, skipVerification: skipVerification) + } + } +} diff --git a/Nos/Service/Relay/RelayService.swift b/Nos/Service/Relay/RelayService.swift index 2d218841d..93537f642 100644 --- a/Nos/Service/Relay/RelayService.swift +++ b/Nos/Service/Relay/RelayService.swift @@ -271,10 +271,27 @@ extension RelayService { return await fetchEvents(matching: contactFilter) } + func requestAuthorLists( + for authorKey: RawAuthorID?, + since: Date? + ) async -> SubscriptionCancellable { + guard let authorKey else { + return SubscriptionCancellable.empty() + } + + let followSetFilter = Filter( + authorKeys: [authorKey], + kinds: [.followSet], + since: since + ) + return await fetchEvents(matching: followSetFilter) + } + func requestProfileData( for authorKey: RawAuthorID?, lastUpdateMetadata: Date?, - lastUpdatedContactList: Date? + lastUpdatedContactList: Date?, + lastUpdatedFollowSets: Date? ) async -> SubscriptionCancellable { var subscriptions = SubscriptionCancellables() guard let authorKey else { @@ -283,7 +300,8 @@ extension RelayService { subscriptions.append(await requestMetadata(for: authorKey, since: lastUpdateMetadata)) subscriptions.append(await requestContactList(for: authorKey, since: lastUpdatedContactList)) - + subscriptions.append(await requestAuthorLists(for: authorKey, since: lastUpdatedFollowSets)) + return SubscriptionCancellable(cancellables: subscriptions, relayService: self) } diff --git a/Nos/Views/Profile/ProfileView.swift b/Nos/Views/Profile/ProfileView.swift index c233b4a99..5a6075375 100644 --- a/Nos/Views/Profile/ProfileView.swift +++ b/Nos/Views/Profile/ProfileView.swift @@ -49,7 +49,8 @@ struct ProfileView: View { await relayService.requestProfileData( for: authorKey, lastUpdateMetadata: author.lastUpdatedMetadata, - lastUpdatedContactList: nil // always grab contact list because we purge follows aggressively + lastUpdatedContactList: nil, // always grab contact list because we purge follows aggressively + lastUpdatedFollowSets: nil // TODO: consider how we want to do this ) ) diff --git a/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift b/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift new file mode 100644 index 000000000..4a40a8454 --- /dev/null +++ b/NosTests/IntegrationTests/EventProcessorIntegrationTests+FollowSet.swift @@ -0,0 +1,76 @@ +import XCTest + +extension EventProcessorIntegrationTests { + @MainActor func test_parse_kind_30000_creates_follow_set() throws { + // Arrange + let replaceableID = "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465" + let ownerPubKey = "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549" + let jsonData = try jsonData(filename: "follow_set") + let jsonEvent = try JSONDecoder().decode(JSONEvent.self, from: jsonData) + + // Act + _ = try EventProcessor.parse(jsonEvent: jsonEvent, from: nil, in: testContext, skipVerification: true) + + // 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) + // swiftlint:disable:next number_separator + XCTAssertEqual(followSet.createdAt, Date(timeIntervalSince1970: 1733516879)) + XCTAssertEqual(followSet.replaceableIdentifier, replaceableID) + XCTAssertEqual(followSet.title, "A few good people") + XCTAssertEqual(followSet.listDescription, "They're great. Trust me.") + XCTAssertEqual(followSet.authors.count, 2) + + let authorPubKeys = followSet.authors.map { $0.hexadecimalPublicKey } + XCTAssertTrue(authorPubKeys.contains( + where: { $0 == "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549" } + )) + XCTAssertTrue(authorPubKeys.contains( + where: { $0 == "1112cad6ffadb22c4d505e9b9f53322052e05a834822cf9368dc754cabbc7ba9" } + )) + } + + @MainActor func test_parse_kind_30000_updates_existing_follow_set() throws { + // Arrange + let replaceableID = "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465" + let ownerPubKey = "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549" + let data = try jsonData(filename: "follow_set") + let jsonEvent = try JSONDecoder().decode(JSONEvent.self, from: data) + + // Act + _ = try EventProcessor.parse(jsonEvent: jsonEvent, from: nil, in: testContext, skipVerification: true) + let updatedJsonData = try jsonData(filename: "follow_set_updated") + let updatedJsonEvent = try JSONDecoder().decode(JSONEvent.self, from: updatedJsonData) + _ = try EventProcessor.parse(jsonEvent: updatedJsonEvent, from: nil, in: testContext, skipVerification: true) + + // 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) + + // swiftlint:disable:next number_separator + XCTAssertEqual(followSet.createdAt, Date(timeIntervalSince1970: 1733765380)) + XCTAssertEqual(followSet.replaceableIdentifier, replaceableID) + XCTAssertEqual(followSet.title, "A few good people") + XCTAssertEqual(followSet.listDescription, "They're great. Trust me.") + XCTAssertEqual(followSet.authors.count, 1) + + let authorPubKey = try XCTUnwrap(followSet.authors.first?.hexadecimalPublicKey) + XCTAssertEqual(authorPubKey, "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549") + } +} diff --git a/NosTests/IntegrationTests/EventProcessorIntegrationTests.swift b/NosTests/IntegrationTests/EventProcessorIntegrationTests.swift index 4c6cd3862..d4d2297f0 100644 --- a/NosTests/IntegrationTests/EventProcessorIntegrationTests.swift +++ b/NosTests/IntegrationTests/EventProcessorIntegrationTests.swift @@ -28,7 +28,7 @@ class EventProcessorIntegrationTests: CoreDataTestCase { let sampleEvent = try XCTUnwrap(events.first(where: { $0.identifier == sampleEventID })) // Assert - XCTAssertEqual(events.count, 115) + XCTAssertEqual(events.count, 140) XCTAssertEqual(sampleEvent.signature, sampleEventSignature) XCTAssertEqual(sampleEvent.kind, 1) XCTAssertEqual(sampleEvent.author?.hexadecimalPublicKey, sampleEventPubKey) diff --git a/NosTests/IntegrationTests/Fixtures/follow_set.json b/NosTests/IntegrationTests/Fixtures/follow_set.json new file mode 100644 index 000000000..eee81d3eb --- /dev/null +++ b/NosTests/IntegrationTests/Fixtures/follow_set.json @@ -0,0 +1,15 @@ +{ + "kind": 30000, + "id": "85e1542678164c321c413706b9c029da2355809884902dbbfd6879917148c221", + "pubkey": "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549", + "created_at": 1733516879, + "tags": [ + ["p", "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549"], + ["p", "1112cad6ffadb22c4d505e9b9f53322052e05a834822cf9368dc754cabbc7ba9"], + ["title", "A few good people"], + ["description", "They're great. Trust me."], + ["d", "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465"] + ], + "content": "", + "sig": "acdf769441a6644e3ae64f8aa1e5f4175a1045e2129e31ae806508515cb65fb5d3207a1f5f05094e09e46cb28c5a3dad5bb2886ab6f5b1faa1f4da8a9f202b04" +} diff --git a/NosTests/IntegrationTests/Fixtures/follow_set_updated.json b/NosTests/IntegrationTests/Fixtures/follow_set_updated.json new file mode 100644 index 000000000..5347054cb --- /dev/null +++ b/NosTests/IntegrationTests/Fixtures/follow_set_updated.json @@ -0,0 +1,14 @@ +{ + "kind": 30000, + "id": "5115b27d32361a09ebd8746cd5f17806c5aa7010a2ac151cc5a1e3c516f05311", + "pubkey": "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549", + "created_at": 1733765380, + "tags": [ + ["p", "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549"], + ["title", "A few good people"], + ["description", "They're great. Trust me."], + ["d", "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465"] + ], + "content": "", + "sig": "a49bb7f2797ea41334643500c0f8809ace7f0379d7a57d6629a2c4b2a9b76db92cfc3585e43fa0c67c8ae71e26d81df96ba1582e30c3edca709e2b1aa1dcdd28" +} diff --git a/NosTests/IntegrationTests/Fixtures/unsupported_kinds.json b/NosTests/IntegrationTests/Fixtures/unsupported_kinds.json index 9a6a066dd..9add4046c 100644 --- a/NosTests/IntegrationTests/Fixtures/unsupported_kinds.json +++ b/NosTests/IntegrationTests/Fixtures/unsupported_kinds.json @@ -1,5 +1,4 @@ [ -{"id":"c9e46f00ddeef7623133bb2060ae250b14093f2f1be145809209455d5c29197d","sig":"2798c790a7d151ef4973e5d02c155847b2588f90d3053771c3369d0dd60f1530e3ee5da88845c7678a5d83c5f988c8760e8f5daece0809ce9b43180af2360bfa","kind":30000,"tags":[["d","chats/226433c9a25b9e1acdea2e824d253a00360ea2b0058536aa14944d675ca3dfaf/lastOpened"]],"pubkey":"d0a1ffb8761b974cec4a3be8cbcb2e96a7090dcf465ffeac839aa4ca20c9a59e","content":"1675111846","created_at":1675111846}, {"kind":30311,"id":"d680ab07a5220282418c30d7695d5fcb2c44ae36957b523e7cddd001d8be4bf1","pubkey":"cf45a6ba1363ad7ed213a078e710d24115ae721c9b47bd1ebf4458eaefb4c2a5","created_at":1718516407,"tags":[["d","8554564b-98dd-488f-9339-af0417d981a2"],["title","🫂⚡️🍻Bartending🍻⚡️🫂"],["summary","The boat show is in town. "],["image","https://dvr.zap.stream/zap-stream-dvr/8554564b-98dd-488f-9339-af0417d981a2/thumb.jpg?AWSAccessKeyId=2gmV0suJz4lt5zZq6I5J\u0026Expires=33275425035\u0026Signature=aHXSsKRjZauDojDNOcnXpJx%2BFYo%3D"],["status","ended"],["p","d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b","","host"],["relays","wss://relay.snort.social","wss://nos.lol","wss://relay.damus.io","wss://relay.nostr.band","wss://nostr.land","wss://nostr-pub.wellorder.net","wss://nostr.wine","wss://relay.nostr.bg","wss://nostr.oxtr.dev"],["starts","1718488448"],["service","https://api.zap.stream/api/nostr"],["recording","https://data.zap.stream/recording/8554564b-98dd-488f-9339-af0417d981a2.m3u8"],["ends","1718516407"],["t","bar"],["t","bartending"],["t","cocktails"],["t","Alcohol"],["t","overhead"],["goal","20754a5acf328dceef491ccc8504729961ad27bb7c29ad2786cb40502a74a281"]],"content":"","sig":"4b2b5221240b990d5873d0bf5c82ea103f8d750baa4ea6bf2471790e2aa6476c1d1e72e514c73c01328306695621cc7f32221d1d40e632b25210c79daf713312"}, {"kind":31890,"id":"25ad65b8c4f2bdc4778e7312251c38a24e36e6afbcea1ff665ec02b648c54e9b","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1716328441,"tags":[["d","3355903008550074"],["title","You might have missed"],["description",""],["feed","[\"intersection\",[\"dvm\",{\"kind\":5300,\"tags\":[[\"p\",\"6b37d5dc88c1cbd32d75b713f6d4c2f7766276f51c9337af9d32c8d715cc1b93\"]]}]]"]],"content":"","sig":"64e18eff761b906720387ac43fdcfb7faac65564b5684015fef2c9a6b2bcd1d8966851f396416358eff903057861c5419dde951c7d7d8165ad281438ab115a45"} ] diff --git a/NosTests/Models/CoreData/AuthorListTests.swift b/NosTests/Models/CoreData/AuthorListTests.swift new file mode 100644 index 000000000..ff7e2393e --- /dev/null +++ b/NosTests/Models/CoreData/AuthorListTests.swift @@ -0,0 +1,68 @@ +import CoreData +import XCTest + +/// Tests for the `AuthorList` model. +/// - Note: There are additional tests for `AuthorList` in `EventProcessorIntegrationTests+FollowSet.swift`. +final class AuthorListTests: CoreDataTestCase { + @MainActor func test_createOrUpdate_throws_when_json_event_is_the_wrong_kind() throws { + // Arrange + let data = try jsonData(filename: "long_form_data") + let events = try JSONDecoder().decode([JSONEvent].self, from: data) + let jsonEvent = try XCTUnwrap(events.first) + + // Act & Assert + XCTAssertThrowsError(try AuthorList.createOrUpdate(from: jsonEvent, in: testContext)) { error in + XCTAssertEqual(error as? AuthorListError, AuthorListError.invalidKind) + } + } + + @MainActor func test_createOrUpdate_throws_when_json_event_is_missing_replaceableID() throws { + // Arrange + let data = try jsonData(filename: "follow_set") + var event = try JSONDecoder().decode(JSONEvent.self, from: data) + event.tags = [[]] + + // Act & Assert + XCTAssertThrowsError(try AuthorList.createOrUpdate(from: event, in: testContext)) { error in + XCTAssertEqual(error as? AuthorListError, AuthorListError.missingReplaceableID) + } + } + + @MainActor func test_createOrUpdate_includes_all_data() throws { + // Arrange + let data = try jsonData(filename: "follow_set") + let event = try JSONDecoder().decode(JSONEvent.self, from: data) + + // Act + let list = try AuthorList.createOrUpdate(from: event, in: testContext) + + // Assert + XCTAssertEqual(list.kind, EventKind.followSet.rawValue) + XCTAssertEqual(list.identifier, "85e1542678164c321c413706b9c029da2355809884902dbbfd6879917148c221") + XCTAssertEqual( + list.author?.hexadecimalPublicKey, + "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549" + ) + // swiftlint:disable:next number_separator + XCTAssertEqual(list.createdAt, Date(timeIntervalSince1970: 1733516879)) + XCTAssertEqual(list.authors.count, 2) + XCTAssertEqual(list.title, "A few good people") + XCTAssertEqual(list.listDescription, "They're great. Trust me.") + XCTAssertEqual(list.replaceableIdentifier, "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465") + XCTAssertEqual(list.content, "") + XCTAssertEqual(list.signature, "acdf769441a6644e3ae64f8aa1e5f4175a1045e2129e31ae806508515cb65fb5d3207a1f5f05094e09e46cb28c5a3dad5bb2886ab6f5b1faa1f4da8a9f202b04") // swiftlint:disable:this line_length + } + + @MainActor func test_signature() throws { + // Arrange + let data = try jsonData(filename: "follow_set") + let event = try JSONDecoder().decode(JSONEvent.self, from: data) + let list = try AuthorList.createOrUpdate(from: event, in: testContext) + + // Act + let verified = try list.verifySignature() + + // Assert + XCTAssertTrue(verified) + } +} diff --git a/NosTests/Service/Relay/RelayServiceTests.swift b/NosTests/Service/Relay/RelayServiceTests.swift index 7397d4f39..4a0977a84 100644 --- a/NosTests/Service/Relay/RelayServiceTests.swift +++ b/NosTests/Service/Relay/RelayServiceTests.swift @@ -16,4 +16,19 @@ class RelayServiceTests: XCTestCase { let resultFilter = try XCTUnwrap(mockSubscriptionManager.queueSubscriptionFilter) XCTAssertEqual(resultFilter, expectedFilter) } + + func test_requestFollowSets_uses_correct_filter() async throws { + let since = Date() + let expectedFilter = Filter( + authorKeys: ["test"], + kinds: [.followSet], + since: since + ) + let mockSubscriptionManager = MockRelaySubscriptionManager() + let subject = RelayService(subscriptionManager: mockSubscriptionManager) + _ = await subject.requestAuthorLists(for: "test", since: since) + + let resultFilter = try XCTUnwrap(mockSubscriptionManager.queueSubscriptionFilter) + XCTAssertEqual(resultFilter, expectedFilter) + } }